@@ -77,94 +77,107 @@ export class LocalStorageFS extends AbstractFileSystem {
7777 localStorage . setItem ( "fs" , JSON . stringify ( state ) ) ;
7878 }
7979
80- async move_dir_direct ( src : string , dest : string , no_overwrite : boolean , move_inside : boolean ) {
81- const state = JSON . parse ( localStorage . getItem ( "fs" ) ) ;
82-
83- // using unix style rules, i.e
84-
85- // mv dir1 dir2 -> rename dir1 to dir2, or move dir1 into dir2 if dir2 already exists (THIS IS WHEN MOVE_INSIDE IS FALSE)
86- // overwrite any files in the destination directory if they exist in the source directory if no_overwrite is false
87- // and of course move across any files from the source directory to the destination directory and leave any only in the destination directory alone
88-
89- // mv dir1 dir2/ -> move dir1 into dir2 (dir2 must exist, dir1 must not exist in dir2) (THIS IS WHEN MOVE_INSIDE IS TRUE, THERE WILL NOT BE A TRAILING / IN THE DESTINATION PATH)
90-
91- // split path into parts, if root, use single empty string to avoid doubling
92- const src_parts = src === this . _root ? [ "" ] : src . split ( "/" ) ;
93- const dest_parts = dest . split ( "/" ) ;
94-
95- // get directory for each part inside the previous one
96- let current_dir = state ;
97- let current_dir_parent = null ;
98- for ( const part of src_parts ) {
99- if ( ! current_dir [ part ] ) {
100- throw new PathNotFoundError ( src ) ;
80+ async move_dir_direct ( src : string , dest : string , move_inside : boolean ) {
81+ // (yes, this was ai generated, i just wanted to bring this very outdated fs_impl up to par with opfs. although its idea to make a helper was good, i should have done that years ago)
82+
83+ const state = JSON . parse ( localStorage . getItem ( "fs" ) || "{}" ) ;
84+
85+ // Helper to traverse to a path and return the parent and the target key
86+ // This allows us to modify the parent (delete/assign) later
87+ const get_node = ( path : string ) => {
88+ const parts = path . split ( "/" ) . filter ( p => p . length > 0 ) ;
89+ const basename = parts [ parts . length - 1 ] ;
90+ let current = state ;
91+
92+ // Traverse to parent
93+ for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
94+ const part = parts [ i ] ;
95+ if ( ! current [ part ] || typeof current [ part ] !== "object" ) {
96+ throw new Error ( `Path not found: ${ path } ` ) ; // Simulates Linux "No such file or directory"
97+ }
98+ current = current [ part ] ;
10199 }
102- current_dir_parent = current_dir ;
103- current_dir = current_dir [ part ] ;
104- }
105-
106- // check if source is a directory
107- if ( typeof current_dir !== "object" ) {
108- throw new PathNotFoundError ( src ) ;
109- }
110100
111- // get directory for each part inside the previous one
112- let dest_current_dir = state ;
113- //let dest_current_dir_parent = null;
114- for ( const part of dest_parts ) {
115- if ( ! dest_current_dir [ part ] ) {
116- // if this is the last part, create the directory, otherwise throw an error
117- // TODO: is this correct? it acts correct, but is it too lax?
118- if ( part === dest_parts [ dest_parts . length - 1 ] ) {
119- dest_current_dir [ part ] = { } ;
120- } else {
121- throw new PathNotFoundError ( dest ) ;
122- }
101+ return { parent : current , basename : basename , value : current [ basename ] } ;
102+ } ;
103+
104+ // 1. Resolve Source
105+ // We need the parent so we can 'delete' the entry later
106+ const src_node = get_node ( src ) ;
107+ if ( ! src_node . value ) throw new PathNotFoundError ( src ) ;
108+ if ( typeof src_node . value !== "object" ) throw new PathNotFoundError ( src ) ;
109+
110+ // 2. Resolve Destination Parent
111+ // Unlike your original code, we do NOT auto-create the destination path.
112+ // Linux 'mv' fails if you try to move to 'a/b/c' and 'a/b' doesn't exist.
113+ const dest_parts = dest . split ( "/" ) . filter ( p => p . length > 0 ) ;
114+ const dest_basename = dest_parts [ dest_parts . length - 1 ] ;
115+
116+ // Check if the generic destination path exists (e.g. is 'dest' already there?)
117+ let dest_exists = false ;
118+ let dest_is_dir = false ;
119+ let dest_parent_obj = state ; // Default to root
120+
121+ // Traverse to the parent of the destination
122+ for ( let i = 0 ; i < dest_parts . length - 1 ; i ++ ) {
123+ const part = dest_parts [ i ] ;
124+ if ( ! dest_parent_obj [ part ] || typeof dest_parent_obj [ part ] !== "object" ) {
125+ throw new Error ( `Destination parent path not found: ${ dest } ` ) ;
123126 }
124- //dest_current_dir_parent = dest_current_dir;
125- dest_current_dir = dest_current_dir [ part ] ;
127+ dest_parent_obj = dest_parent_obj [ part ] ;
126128 }
127129
128- // check if destination is a directory
129- if ( typeof dest_current_dir !== "object" ) {
130- throw new PathNotFoundError ( dest ) ;
130+ // Check the actual destination node
131+ if ( dest_parent_obj [ dest_basename ] ) {
132+ dest_exists = true ;
133+ dest_is_dir = typeof dest_parent_obj [ dest_basename ] === "object" ;
131134 }
132135
133- // if we have equivalent paths, do nothing (so we don't accidentally delete the directory when calling delete after move)
134- if ( src === dest ) {
135- console . warn ( "source and destination are the same" ) ;
136- return ;
137- }
136+ // 3. Apply Logic (Rename vs Move Into)
137+ let final_parent ;
138+ let final_name ;
138139
139- // TODO: significant fixes required! moving directories is just a mess
140- // TODO: need to consolidate exactly when we should be merging directories. its not exactly clear and chatgpt contradicts itself when asking for a formal definition!
140+ if ( move_inside || ( dest_exists && dest_is_dir ) ) {
141+ // RULE: Move 'src' INTO 'dest'
141142
142- if ( move_inside ) {
143- // if moving inside, check that the directory named the same as the source does not exist in the destination
144- if ( dest_current_dir [ src_parts [ src_parts . length - 1 ] ] ) {
145- throw new Error ( `Directory already exists in destination: ${ dest } ` ) ;
143+ if ( ! dest_exists ) {
144+ // "mv dir1 dir2/" but dir2 missing -> Error
145+ throw new Error ( `Destination directory not found: ${ dest } ` ) ;
146146 }
147147
148- // move directory inside destination
149- dest_current_dir [ src_parts [ src_parts . length - 1 ] ] = current_dir ;
150-
151- // delete source directory
152- delete current_dir_parent [ src_parts [ src_parts . length - 1 ] ] ;
148+ // Our new parent is the destination folder itself
149+ final_parent = dest_parent_obj [ dest_basename ] ;
150+ final_name = src_node . basename ; // Keep original name
153151 } else {
154- // not moving inside, so merge files and directories from source into destination
155- for ( const key of Object . keys ( current_dir ) ) {
156- if ( dest_current_dir [ key ] && no_overwrite ) {
157- throw new Error ( `File or directory already exists in destination: ${ dest } ` ) ;
158- }
152+ // RULE: Rename 'src' TO 'dest'
159153
160- dest_current_dir [ key ] = current_dir [ key ] ;
154+ if ( dest_exists && ! dest_is_dir ) {
155+ // Trying to overwrite a file with a directory -> Error
156+ throw new Error ( `Cannot overwrite non-directory '${ dest } ' with directory.` ) ;
161157 }
162158
163- // delete source directory
164- delete current_dir_parent [ src_parts [ src_parts . length - 1 ] ] ;
159+ // Our new parent is the destination's parent
160+ final_parent = dest_parent_obj ;
161+ final_name = dest_basename ; // New name
165162 }
166163
167- // save state
164+ // 4. Collision Check (Strict Linux: No Merging)
165+ if ( final_parent [ final_name ] ) {
166+ // In Linux, 'mv' fails if the target directory is not empty.
167+ // Since we are moving a directory, we strictly fail here.
168+ throw new Error ( `Directory not empty: ${ final_name } already exists in destination.` ) ;
169+ }
170+
171+ // 5. Execute Move (Atomic Reference Change)
172+ // This is the beauty of LocalStorage/JSON: No recursive copy needed.
173+ // Just point the new key to the old object.
174+
175+ final_parent [ final_name ] = src_node . value ;
176+
177+ // 6. Delete Source
178+ delete src_node . parent [ src_node . basename ] ;
179+
180+ // 7. Save State
168181 localStorage . setItem ( "fs" , JSON . stringify ( state ) ) ;
169182 }
170183
0 commit comments