forked from phcode-dev/staging.phcode.dev
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathFileRecovery.js
More file actions
1 lines (1 loc) · 12 KB
/
Copy pathFileRecovery.js
File metadata and controls
1 lines (1 loc) · 12 KB
1
define(function(require,exports,module){const NativeApp=require("utils/NativeApp"),FileSystem=require("filesystem/FileSystem"),ProjectManager=require("project/ProjectManager"),MainViewManager=require("view/MainViewManager"),FileSystemError=require("filesystem/FileSystemError"),FileUtils=require("file/FileUtils"),DocumentManager=require("document/DocumentManager"),NotificationUI=require("widgets/NotificationUI"),Mustache=require("thirdparty/mustache/mustache"),Strings=require("strings"),FileViewController=require("project/FileViewController"),recoveryTemplate=require("text!./html/recovery-template.html"),EventDispatcher=require("utils/EventDispatcher"),Metrics=require("utils/Metrics"),EventManager=require("utils/EventManager");EventDispatcher.makeEventDispatcher(exports),EventManager.registerEventHandler("ph-recovery",exports);const BACKUP_INTERVAL_MS=5e3;let sessionRestoreDir=FileSystem.getDirectoryForPath(path.normalize(NativeApp.getApplicationSupportDirectory()+"/sessionRestore"));const trackedProjects={};function simpleHash(str){let hash=0;for(let i=0;i<str.length;i++){let char;hash=(hash<<5)-hash+str.charCodeAt(i),hash&=hash}return Math.abs(hash)+""}function createDir(dir){return new Promise((resolve,reject)=>{dir.create(function(err){err&&err!==FileSystemError.ALREADY_EXISTS&&(console.error("[recovery] Error creating project crash restore folder "+dir.fullPath,err),reject(err)),resolve()})})}function silentlyRemoveFile(path){return new Promise(resolve=>{FileSystem.getFileForPath(path).unlink(err=>{err&&console.error(err),resolve()})})}function silentlyRemoveDirectory(dir){return new Promise(resolve=>{dir.unlink(err=>{err&&console.error(err),resolve()})})}function getProjectRestoreRoot(projectPath){const baseName=path.basename(projectPath),restoreRootPath=path.normalize(`${sessionRestoreDir.fullPath}/${baseName}_${simpleHash(projectPath)}`);return FileSystem.getDirectoryForPath(restoreRootPath)}function getRestoreFilePath(projectFilePath,projectRootPath){if(!projectFilePath.startsWith(projectRootPath)||!trackedProjects[projectRootPath])return console.error(`[recovery] cannot backed up as ${projectFilePath} is not in project ${projectRootPath}`),null;let pathWithinProject=projectFilePath.replace(projectRootPath,""),restoreRoot=trackedProjects[projectRootPath].restoreRoot;return path.normalize(`${restoreRoot.fullPath}/${pathWithinProject}`)}function getProjectFilePath(restoreFilePath,projectRootPath){const project=trackedProjects[projectRootPath];if(!project||!restoreFilePath.startsWith(project.restoreRoot.fullPath))return null;let filePathInProject=restoreFilePath.replace(project.restoreRoot.fullPath,"");return path.normalize(`${projectRootPath}/${filePathInProject}`)}async function ensureFolderIsClean(folder){await createDir(folder),await folder.unlinkEmptyDirectoryAsync(),await createDir(folder)}function integrityCheck(input){if(!input)return null;const firstCommaIndex=input.indexOf(",");if(-1===firstCommaIndex||0===firstCommaIndex)return null;const expectedLengthPart=input.slice(0,firstCommaIndex),actualString=input.slice(firstCommaIndex+1),expectedLength=parseInt(expectedLengthPart,10);return isNaN(expectedLength)?null:actualString.length===expectedLength?actualString:null}async function loadLastBackedUpFileContents(projectRootPath){const project=trackedProjects[projectRootPath];if(!project)return void console.error("[recovery] Cannot load backup, no tracking info of project "+projectRootPath);const currentProjectLoadCount=project.projectLoadCount;let restoreFolder=project.restoreRoot;await ensureFolderIsClean(restoreFolder);let allEntries=await FileSystem.getAllDirectoryContents(restoreFolder),backupExists=!1;for(let entry of allEntries){if(entry.isDirectory)continue;let text=integrityCheck(await jsPromise(FileUtils.readAsText(entry)));if(!text)continue;let projectFilePath=getProjectFilePath(entry.fullPath,projectRootPath);if(currentProjectLoadCount!==project.projectLoadCount)return;project.lastBackedUpFileContents[projectFilePath]=text,backupExists=!0}if(project.lastBackedupLoadInProgress=!1,backupExists){let notificationHTML=Mustache.render(recoveryTemplate,{Strings:Strings,PROJECT_TO_RECOVER:projectRootPath});project.restoreNotification&&(project.restoreNotification.close(),project.restoreNotification=null),project.restoreNotification=NotificationUI.createToastFromTemplate(Strings.RECOVER_UNSAVED_FILES_TITLE,notificationHTML,{dismissOnClick:!1,toastStyle:NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.SUCCESS}),Metrics.countEvent(Metrics.EVENT_TYPE.PROJECT,"recovery","notified")}else Metrics.countEvent(Metrics.EVENT_TYPE.PROJECT,"recovery","none")}let currentProjectRoot=null;function projectOpened(_event,projectRoot){if("/"!==projectRoot.fullPath){if(!currentProjectRoot||currentProjectRoot.fullPath!==projectRoot.fullPath){if(currentProjectRoot=projectRoot,trackedProjects[projectRoot.fullPath])return trackedProjects[projectRoot.fullPath].restoreNotification&&(trackedProjects[projectRoot.fullPath].restoreNotification.close(),trackedProjects[projectRoot.fullPath].restoreNotification=null),trackedProjects[projectRoot.fullPath].projectLoadCount++,trackedProjects[projectRoot.fullPath].lastBackedUpFileContents={},trackedProjects[projectRoot.fullPath].firstEditHandled=!1,trackedProjects[projectRoot.fullPath].lastBackedupLoadInProgress=!0,trackedProjects[projectRoot.fullPath].trackedFileUpdateTimestamps={},trackedProjects[projectRoot.fullPath].trackedFileContents={},trackedProjects[projectRoot.fullPath].changeErrorReported=!1,void loadLastBackedUpFileContents(projectRoot.fullPath).catch(err=>{console.error("[recovery] loadLastBackedUpFileContents failed ",err)});trackedProjects[projectRoot.fullPath]={projectLoadCount:0,projectRoot:projectRoot,restoreRoot:getProjectRestoreRoot(projectRoot.fullPath),lastBackedUpFileContents:{},firstEditHandled:!1,lastBackedupLoadInProgress:!0,trackedFileUpdateTimestamps:{},trackedFileContents:{},restoreNotification:null,changeErrorReported:!1},loadLastBackedUpFileContents(projectRoot.fullPath).catch(err=>{console.error("[recovery] loadLastBackedUpFileContents failed ",err)})}}else console.error("[recovery] Backups will not be done for root folder `/`")}async function writeFileIgnoreFailure(filePath,contents){try{let parentDir=FileSystem.getDirectoryForPath(path.dirname(filePath));await createDir(parentDir);let file=FileSystem.getFileForPath(filePath);const restoreContentsWithIntegrity=contents.length+","+contents;await jsPromise(FileUtils.writeText(file,restoreContentsWithIntegrity,!0))}catch(e){console.error(e)}}async function backupChangedDocs(projectRoot){const project=trackedProjects[projectRoot.fullPath];let trackedFilePaths=Object.keys(project.trackedFileContents);for(let trackedFilePath of trackedFilePaths){const restorePath=getRestoreFilePath(trackedFilePath,projectRoot.fullPath);if(restorePath){const content=project.trackedFileContents[trackedFilePath];await writeFileIgnoreFailure(restorePath,content)}delete project.trackedFileContents[trackedFilePath]}}async function cleanupUntrackedFiles(docPathsToTrack,projectRoot){const project=trackedProjects[projectRoot.fullPath];let allTrackingPaths=Object.keys(project.trackedFileUpdateTimestamps);for(let trackedPath of allTrackingPaths)if(!docPathsToTrack[trackedPath]){const restoreFile=getRestoreFilePath(trackedPath,projectRoot.fullPath);restoreFile&&await silentlyRemoveFile(restoreFile),delete project.trackedFileUpdateTimestamps[trackedPath]}}let backupInProgress=!1;async function changeScanner(){let currentProjectRoot=ProjectManager.getProjectRoot();const project=trackedProjects[currentProjectRoot.fullPath];if(!backupInProgress&&"/"!==currentProjectRoot.fullPath&&project&&!project.lastBackedupLoadInProgress){backupInProgress=!0;try{const openDocs=DocumentManager.getAllOpenDocuments();let docPathsToTrack={},dirtyDocsExists=!1;for(let doc of openDocs)if(doc&&doc.isDirty){dirtyDocsExists=!0,docPathsToTrack[doc.file.fullPath]=!0;const lastTrackedTimestamp=project.trackedFileUpdateTimestamps[doc.file.fullPath];lastTrackedTimestamp&&lastTrackedTimestamp===doc.lastChangeTimestamp||(project.trackedFileContents[doc.file.fullPath]=doc.getText(),project.trackedFileUpdateTimestamps[doc.file.fullPath]=doc.lastChangeTimestamp)}!project.firstEditHandled&&dirtyDocsExists?(console.log("Discarding old backup for restore..."),await silentlyRemoveDirectory(project.restoreRoot),await createDir(project.restoreRoot),await backupChangedDocs(currentProjectRoot),project.firstEditHandled=!0,project.restoreNotification&&$("#DISCARD_UNSAVED_FILES_RESTORE").addClass("forced-hidden")):(await backupChangedDocs(currentProjectRoot),await cleanupUntrackedFiles(docPathsToTrack,currentProjectRoot))}catch(e){console.error("[recovery] changeScanner error",e),project.changeErrorReported||(project.changeErrorReported=!0,logger.reportError(e))}backupInProgress=!1}}function beforeProjectClosed(){let currentProjectRoot=ProjectManager.getProjectRoot();const project=trackedProjects[currentProjectRoot.fullPath];project.restoreNotification&&(project.restoreNotification.close(),project.restoreNotification=null),changeScanner().catch(err=>{console.error("[recovery] beforeProjectClosed failed which scanning for changes to backup",err)})}async function ensureOpenEditors(pathList){let allOpenFiles=MainViewManager.getAllOpenFiles(),openFilePaths={};for(let file of allOpenFiles)openFilePaths[file.fullPath]=!0;for(let path of pathList)if(!openFilePaths[path]){let file=FileSystem.getFileForPath(path);await jsPromise(FileViewController.openFileAndAddToWorkingSet(file.fullPath))}}async function restoreBtnClicked(_event,projectToRestore){let currentProjectRoot=ProjectManager.getProjectRoot();const project=trackedProjects[currentProjectRoot.fullPath];if(Metrics.countEvent(Metrics.EVENT_TYPE.PROJECT,"recovery","restoreClick"),!project||projectToRestore!==currentProjectRoot.fullPath)return void console.error(`[recovery] current project ${currentProjectRoot.fullPath} != restore ${projectToRestore}`);let pathsToRestore=Object.keys(project.lastBackedUpFileContents);await ensureOpenEditors(pathsToRestore);for(let filePath of pathsToRestore)if(ProjectManager.isWithinProject(filePath)){let document;console.log("restoring",filePath),(await jsPromise(DocumentManager.getDocumentForPath(filePath))).setText(project.lastBackedUpFileContents[filePath])}else console.error("[recovery] Skipping restore of non project file: ",filePath);project.restoreNotification&&(project.restoreNotification.close(),project.restoreNotification=null)}async function discardBtnClicked(_event,projectToRestore){let currentProjectRoot=ProjectManager.getProjectRoot();const project=trackedProjects[currentProjectRoot.fullPath];Metrics.countEvent(Metrics.EVENT_TYPE.PROJECT,"recovery","discardClick"),project&&projectToRestore===currentProjectRoot.fullPath?(trackedProjects[currentProjectRoot.fullPath].lastBackedUpFileContents={},trackedProjects[currentProjectRoot.fullPath].firstEditHandled=!0,project.restoreNotification&&(project.restoreNotification.close(),project.restoreNotification=null),await silentlyRemoveDirectory(project.restoreRoot),await createDir(project.restoreRoot),await backupChangedDocs(currentProjectRoot)):console.error(`[recovery] current project ${currentProjectRoot.fullPath} != restore ${projectToRestore}`)}function initWith(scanIntervalMs,restoreDir){ProjectManager.on(ProjectManager.EVENT_AFTER_PROJECT_OPEN,projectOpened),ProjectManager.on(ProjectManager.EVENT_PROJECT_BEFORE_CLOSE,beforeProjectClosed),exports.on("restoreProject",restoreBtnClicked),exports.on("discardProject",discardBtnClicked),createDir(sessionRestoreDir=restoreDir),setInterval(changeScanner,scanIntervalMs);let currentProjectRoot=ProjectManager.getProjectRoot();currentProjectRoot&&projectOpened(null,currentProjectRoot),ProjectManager.on("beforeAppClose",(_evt,waitPromises)=>{let exitProjectRoot=ProjectManager.getProjectRoot();if(exitProjectRoot){const restoreRoot=getProjectRestoreRoot(exitProjectRoot.fullPath);waitPromises.push(silentlyRemoveDirectory(restoreRoot))}})}function init(){window.testEnvironment?(exports.getProjectRestoreRoot=getProjectRestoreRoot,exports.initWith=initWith,window._FileRecoveryExtensionForTests=exports):initWith(BACKUP_INTERVAL_MS,sessionRestoreDir)}exports.init=init});