@@ -34,10 +34,7 @@ define(function (require, exports, module) {
3434 const sessionRestoreDir = FileSystem . getDirectoryForPath (
3535 path . normalize ( NativeApp . getApplicationSupportDirectory ( ) + "/sessionRestore" ) ) ;
3636
37- let trackingProjectRoot = null ,
38- trackingRestoreRoot = null ,
39- trackedProjectFilesMap = { } ,
40- trackedFilesChangeTimestamps = { } ;
37+ const trackedProjects = { } ;
4138
4239 function simpleHash ( str ) {
4340 let hash = 0 ;
@@ -55,7 +52,7 @@ define(function (require, exports, module) {
5552 return new Promise ( ( resolve , reject ) => {
5653 dir . create ( function ( err ) {
5754 if ( err && err !== FileSystemError . ALREADY_EXISTS ) {
58- console . error ( "Error creating project crash restore folder " + dir . fullPath , err ) ;
55+ console . error ( "[recovery] Error creating project crash restore folder " + dir . fullPath , err ) ;
5956 reject ( err ) ;
6057 }
6158 resolve ( ) ;
@@ -74,42 +71,114 @@ define(function (require, exports, module) {
7471 } ) ;
7572 }
7673
77- function setupProjectRestoreRoot ( projectPath ) {
78- const baseName = path . basename ( projectPath ) ;
79- let restoreRootPath = path . normalize ( `${ sessionRestoreDir . fullPath } /${ baseName } _${ simpleHash ( projectPath ) } ` ) ;
80- trackingRestoreRoot = FileSystem . getDirectoryForPath ( restoreRootPath ) ;
81- createDir ( trackingRestoreRoot ) ;
74+ function silentlyRemoveDirectory ( dir ) {
75+ return new Promise ( ( resolve ) => {
76+ dir . unlink ( ( err ) => {
77+ if ( err ) {
78+ console . error ( err ) ;
79+ }
80+ resolve ( ) ;
81+ } ) ;
82+ } ) ;
8283 }
8384
84- function getRestoreFilePath ( projectFilePath ) {
85- if ( ProjectManager . isWithinProject ( projectFilePath ) ) {
86- return path . normalize (
87- `${ trackingRestoreRoot . fullPath } /${ ProjectManager . getProjectRelativePath ( projectFilePath ) } ` ) ;
85+ function getProjectRestoreRoot ( projectPath ) {
86+ const baseName = path . basename ( projectPath ) ,
87+ restoreRootPath = path . normalize ( `${ sessionRestoreDir . fullPath } /${ baseName } _${ simpleHash ( projectPath ) } ` ) ;
88+ return FileSystem . getDirectoryForPath ( restoreRootPath ) ;
89+ }
90+
91+ function getRestoreFilePath ( projectFilePath , projectRootPath ) {
92+ if ( ! projectFilePath . startsWith ( projectRootPath ) || ! trackedProjects [ projectRootPath ] ) {
93+ console . error ( `[recovery] cannot backed up as ${ projectRootPath } is not in project ${ projectRootPath } ` ) ;
94+ return null ;
8895 }
89- return null ;
96+ let pathWithinProject = projectFilePath . replace ( projectRootPath , "" ) ;
97+ let restoreRoot = trackedProjects [ projectRootPath ] . restoreRoot ;
98+ return path . normalize ( `${ restoreRoot . fullPath } /${ pathWithinProject } ` ) ;
9099 }
91100
92101 // try not to use this
93- function getProjectFilePath ( restoreFilePath ) {
94- if ( ! restoreFilePath . startsWith ( trackingRestoreRoot . fullPath ) ) {
102+ function getProjectFilePath ( restoreFilePath , projectRootPath ) {
103+ const project = trackedProjects [ projectRootPath ] ;
104+ if ( ! project || ! restoreFilePath . startsWith ( project . restoreRoot . fullPath ) ) {
95105 return null ;
96106 }
97- // Eg. trackingRestoreRoot = "/fs/app/sessionRestore/default project_1944444020/"
98- // and restoreProjectRelativePath = "/fs/app/sessionRestore/default project_1944444020/default project/a.html"
99- let restoreProjectRelativePath = restoreFilePath . replace ( trackingRestoreRoot . fullPath , "" ) ;
100- // Eg. default project/a.html
101- let restoreProjectName = restoreProjectRelativePath . split ( "/" ) [ 0 ] , // Eg. default project
102- trackingProjectName = path . basename ( trackingProjectRoot . fullPath ) ; // default project
103- if ( trackingProjectName !== restoreProjectName ) {
104- return null ;
107+
108+ let filePathInProject = restoreFilePath . replace ( project . restoreRoot . fullPath , "" ) ;
109+ return path . normalize ( `${ projectRootPath } /${ filePathInProject } ` ) ;
110+ }
111+
112+ /**
113+ * the restore folder may have empty folders as files get deleted according to backup algorithm. This fn will
114+ * ensure that there are no empty folders and restore folder exists
115+ * @param folder
116+ * @return {Promise<void> }
117+ */
118+ async function ensureFolderIsClean ( folder ) {
119+ await createDir ( folder ) ;
120+ await folder . unlinkEmptyDirectoryAsync ( ) ;
121+ await createDir ( folder ) ;
122+ }
123+
124+ async function loadLastBackedUpFileContents ( projectRootPath ) {
125+ const project = trackedProjects [ projectRootPath ] ;
126+ if ( ! project ) {
127+ console . error ( "[recovery] Cannot load backup, no tracking info of project " + projectRootPath ) ;
128+ return ;
105129 }
106- let filePathInProject = restoreProjectRelativePath . replace ( `${ restoreProjectName } /` , "" ) ; // a.html
107- return path . normalize ( `${ trackingProjectRoot . fullPath } /${ filePathInProject } ` ) ;
130+ const currentProjectLoadCount = project . projectLoadCount ;
131+ let restoreFolder = project . restoreRoot ;
132+ await ensureFolderIsClean ( restoreFolder ) ;
133+ let allEntries = await FileSystem . getAllDirectoryContents ( restoreFolder ) ;
134+ for ( let entry of allEntries ) {
135+ if ( entry . isDirectory ) {
136+ continue ;
137+ }
138+ let text = await jsPromise ( FileUtils . readAsText ( entry ) ) ;
139+ let projectFilePath = getProjectFilePath ( entry . fullPath , projectRootPath ) ;
140+ if ( currentProjectLoadCount !== project . projectLoadCount ) {
141+ // this means that while we were tying to load a project backup, the user switched to another project
142+ // and then switched back to this project, all before the first backup load was complete. so
143+ // we just return without doing anything here. This function will be eventually called on projectOpened
144+ // event handler.
145+ return ;
146+ }
147+ project . lastBackedUpFileContents [ projectFilePath ] = text ;
148+ console . log ( text ) ;
149+ }
150+ project . lastBackedupLoadInProgress = false ;
108151 }
109152
110153 function projectOpened ( _event , projectRoot ) {
111- trackingProjectRoot = projectRoot ;
112- setupProjectRestoreRoot ( trackingProjectRoot . fullPath ) ;
154+ if ( projectRoot . fullPath === '/' ) {
155+ console . error ( "[recovery] Backups will not be done for root folder `/`" ) ;
156+ return ;
157+ }
158+ if ( trackedProjects [ projectRoot . fullPath ] ) {
159+ trackedProjects [ projectRoot . fullPath ] . projectLoadCount ++ ;
160+ trackedProjects [ projectRoot . fullPath ] . lastBackedUpFileContents = { } ;
161+ trackedProjects [ projectRoot . fullPath ] . firstEditHandled = false ;
162+ trackedProjects [ projectRoot . fullPath ] . lastBackedupLoadInProgress = true ;
163+ trackedProjects [ projectRoot . fullPath ] . trackedFileUpdateTimestamps = { } ;
164+ trackedProjects [ projectRoot . fullPath ] . trackedFileContents = { } ;
165+ loadLastBackedUpFileContents ( projectRoot . fullPath ) ;
166+ // todo race condition here frequent switch between projects
167+ return ;
168+ }
169+ trackedProjects [ projectRoot . fullPath ] = {
170+ projectLoadCount : 0 , // we use this to prevent race conditions on frequent project switch before all
171+ // project backup files are loaded.
172+ projectRoot : projectRoot ,
173+ restoreRoot : getProjectRestoreRoot ( projectRoot . fullPath ) ,
174+ lastBackedUpFileContents : { } ,
175+ firstEditHandled : false , // after a project is loaded, has the first edit by user on any file been handled?
176+ lastBackedupLoadInProgress : true , // while the backup is loading, we need to prevent write over the existing
177+ // backup with backup info of the current session
178+ trackedFileUpdateTimestamps : { } ,
179+ trackedFileContents : { }
180+ } ;
181+ loadLastBackedUpFileContents ( projectRoot . fullPath ) ;
113182 }
114183
115184 async function writeFileIgnoreFailure ( filePath , contents ) {
@@ -124,71 +193,81 @@ define(function (require, exports, module) {
124193 }
125194 }
126195
127- async function backupChangedDocs ( changedDocs ) {
128- for ( let doc of changedDocs ) {
129- let restorePath = getRestoreFilePath ( doc . file . fullPath ) ;
130- await writeFileIgnoreFailure ( restorePath , doc . getText ( ) ) ;
131- trackedFilesChangeTimestamps [ doc . file . fullPath ] = doc . lastChangeTimestamp ;
132- trackedProjectFilesMap [ doc . file . fullPath ] = restorePath ;
196+ async function backupChangedDocs ( projectRoot ) {
197+ const project = trackedProjects [ projectRoot . fullPath ] ;
198+ let trackedFilePaths = Object . keys ( project . trackedFileContents ) ;
199+ for ( let trackedFilePath of trackedFilePaths ) {
200+ const restorePath = getRestoreFilePath ( trackedFilePath , projectRoot . fullPath ) ;
201+ const content = project . trackedFileContents [ trackedFilePath ] ;
202+ await writeFileIgnoreFailure ( restorePath , content ) ;
203+ delete project . trackedFileContents [ trackedFilePath ] ;
133204 }
134205 }
135206
136- async function cleanupUntrackedFiles ( docPathsToTrack ) {
137- let allTrackingPaths = Object . keys ( trackedProjectFilesMap ) ;
207+ async function cleanupUntrackedFiles ( docPathsToTrack , projectRoot ) {
208+ const project = trackedProjects [ projectRoot . fullPath ] ;
209+ let allTrackingPaths = Object . keys ( project . trackedFileUpdateTimestamps ) ;
138210 for ( let trackedPath of allTrackingPaths ) {
139211 if ( ! docPathsToTrack [ trackedPath ] ) {
140- const restoreFile = trackedProjectFilesMap [ trackedPath ] ;
212+ const restoreFile = getRestoreFilePath ( trackedPath , projectRoot . fullPath ) ;
141213 await silentlyRemoveFile ( restoreFile ) ;
142- delete trackedProjectFilesMap [ trackedPath ] ;
143- delete trackedFilesChangeTimestamps [ trackedPath ] ;
214+ delete project . trackedFileUpdateTimestamps [ trackedPath ] ;
144215 }
145216 }
146217 }
147218
148219 let backupInProgress = false ;
149220 async function changeScanner ( ) {
150- if ( backupInProgress || trackingProjectRoot . fullPath === "/" ) {
221+ let currentProjectRoot = ProjectManager . getProjectRoot ( ) ;
222+ const project = trackedProjects [ currentProjectRoot . fullPath ] ;
223+ if ( backupInProgress || currentProjectRoot . fullPath === "/" || ! project || project . lastBackedupLoadInProgress ) {
151224 // trackingProjectRoot can be "/" if debug>open virtual file system menu is clicked. Don't track root fs
152225 return ;
153226 }
154227 backupInProgress = true ;
155228 try {
156229 // do backup
157230 const openDocs = DocumentManager . getAllOpenDocuments ( ) ;
158- let changedDocs = [ ] , docPathsToTrack = { } ;
231+ let docPathsToTrack = { } , dirtyDocsExists = false ;
159232 for ( let doc of openDocs ) {
160233 if ( doc && doc . isDirty ) {
234+ dirtyDocsExists = true ;
161235 docPathsToTrack [ doc . file . fullPath ] = true ;
162- const lastTrackedTimestamp = trackedFilesChangeTimestamps [ doc . file . fullPath ] ;
236+ const lastTrackedTimestamp = project . trackedFileUpdateTimestamps [ doc . file . fullPath ] ;
163237 if ( ! lastTrackedTimestamp || lastTrackedTimestamp !== doc . lastChangeTimestamp ) {
164238 // Already backed up, only need to consider it again if its contents changed
165- changedDocs . push ( doc ) ;
239+ project . trackedFileContents [ doc . file . fullPath ] = doc . getText ( ) ;
240+ project . trackedFileUpdateTimestamps [ doc . file . fullPath ] = doc . lastChangeTimestamp ;
166241 }
167242 }
168243 }
169- await backupChangedDocs ( changedDocs ) ;
170- await cleanupUntrackedFiles ( docPathsToTrack ) ;
244+ if ( ! project . firstEditHandled && dirtyDocsExists ) {
245+ // this means that the last backup session has been fully loaded in memory and a new edit has been
246+ // done by the user. The user may not have yet clicked on the restore backup button. But as the user
247+ // made an edit, we should delete the project restore folder to start a new backup session. The user
248+ // can still restore the last backup session from the in memory `project.lastBackedUpFileContents`
249+ await silentlyRemoveDirectory ( project . restoreRoot ) ;
250+ await createDir ( project . restoreRoot ) ;
251+ await backupChangedDocs ( currentProjectRoot ) ;
252+ project . firstEditHandled = true ;
253+ } else {
254+ await backupChangedDocs ( currentProjectRoot ) ;
255+ await cleanupUntrackedFiles ( docPathsToTrack , currentProjectRoot ) ;
256+ }
171257 } catch ( e ) {
172258 console . error ( e ) ;
173259 logger . reportError ( e ) ;
174260 }
175261 backupInProgress = false ;
176262 }
177263
178- function documentChanged ( _event , doc ) {
179- let restorePath = getRestoreFilePath ( doc . file . fullPath ) ;
180- let originalPath = getProjectFilePath ( restorePath ) ;
181- //debugger;
182- }
183-
184- function documentDirtyFlagChanged ( _event , doc ) {
185- //debugger;
264+ function beforeProjectClosed ( ) {
265+ changeScanner ( ) ;
186266 }
187267
188268 function init ( ) {
189269 ProjectManager . on ( ProjectManager . EVENT_AFTER_PROJECT_OPEN , projectOpened ) ;
190- DocumentManager . on ( DocumentManager . EVENT_DOCUMENT_CHANGE , documentChanged ) ;
191- DocumentManager . on ( DocumentManager . EVENT_DIRTY_FLAG_CHANGED , documentDirtyFlagChanged ) ;
270+ ProjectManager . on ( ProjectManager . EVENT_PROJECT_BEFORE_CLOSE , beforeProjectClosed ) ;
192271 createDir ( sessionRestoreDir ) ;
193272 if ( ! window . testEnvironment ) {
194273 setInterval ( changeScanner , BACKUP_INTERVAL_MS ) ;
0 commit comments