Skip to content

Commit dc22c86

Browse files
committed
feat: project backup and restore working
1 parent 04d3ab2 commit dc22c86

File tree

4 files changed

+124
-19
lines changed

4 files changed

+124
-19
lines changed

src/extensions/default/NavigationAndHistory/FileRecovery.js

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,22 @@ define(function (require, exports, module) {
2525
const NativeApp = brackets.getModule("utils/NativeApp"),
2626
FileSystem = brackets.getModule("filesystem/FileSystem"),
2727
ProjectManager = brackets.getModule("project/ProjectManager"),
28+
MainViewManager = brackets.getModule("view/MainViewManager"),
2829
FileSystemError = brackets.getModule("filesystem/FileSystemError"),
2930
FileUtils = brackets.getModule("file/FileUtils"),
30-
DocumentManager = brackets.getModule("document/DocumentManager");
31+
DocumentManager = brackets.getModule("document/DocumentManager"),
32+
NotificationUI = brackets.getModule("widgets/NotificationUI"),
33+
Mustache = brackets.getModule("thirdparty/mustache/mustache"),
34+
Strings = brackets.getModule("strings"),
35+
FileViewController = brackets.getModule("project/FileViewController"),
36+
recoveryTemplate = require("text!html/recovery-template.html"),
37+
EventDispatcher = brackets.getModule("utils/EventDispatcher"),
38+
EventManager = brackets.getModule("utils/EventManager");
3139

32-
const BACKUP_INTERVAL_MS = 3000; // todo change to 20 secs
33-
// todo large number of tracked files performance issues?
40+
EventDispatcher.makeEventDispatcher(exports);
41+
EventManager.registerEventHandler("ph-recovery", exports);
42+
43+
const BACKUP_INTERVAL_MS = 5000;
3444
const sessionRestoreDir = FileSystem.getDirectoryForPath(
3545
path.normalize(NativeApp.getApplicationSupportDirectory() + "/sessionRestore"));
3646

@@ -131,6 +141,7 @@ define(function (require, exports, module) {
131141
let restoreFolder = project.restoreRoot;
132142
await ensureFolderIsClean(restoreFolder);
133143
let allEntries = await FileSystem.getAllDirectoryContents(restoreFolder);
144+
let backupExists = false;
134145
for(let entry of allEntries){
135146
if(entry.isDirectory){
136147
continue;
@@ -145,9 +156,20 @@ define(function (require, exports, module) {
145156
return;
146157
}
147158
project.lastBackedUpFileContents[projectFilePath] = text;
148-
console.log(text);
159+
backupExists = true;
149160
}
150161
project.lastBackedupLoadInProgress = false;
162+
if(backupExists) {
163+
let notificationHTML = Mustache.render(recoveryTemplate, {
164+
Strings: Strings,
165+
PROJECT_TO_RECOVER: projectRootPath
166+
});
167+
project.restoreNotification = NotificationUI.createToastFromTemplate( Strings.RECOVER_UNSAVED_FILES_TITLE,
168+
notificationHTML, {
169+
dismissOnClick: false,
170+
toastStyle: NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.SUCCESS
171+
});
172+
}
151173
}
152174

153175
function projectOpened(_event, projectRoot) {
@@ -156,14 +178,21 @@ define(function (require, exports, module) {
156178
return;
157179
}
158180
if(trackedProjects[projectRoot.fullPath]){
159-
trackedProjects[projectRoot.fullPath].projectLoadCount++;
181+
if(trackedProjects[projectRoot.fullPath].restoreNotification){
182+
trackedProjects[projectRoot.fullPath].restoreNotification.close();
183+
trackedProjects[projectRoot.fullPath].restoreNotification = null;
184+
}
185+
trackedProjects[projectRoot.fullPath].projectLoadCount++;// we use this to prevent race conditions
186+
// on frequent project switch before all project backup files are loaded.
160187
trackedProjects[projectRoot.fullPath].lastBackedUpFileContents = {};
161188
trackedProjects[projectRoot.fullPath].firstEditHandled = false;
162189
trackedProjects[projectRoot.fullPath].lastBackedupLoadInProgress = true;
163190
trackedProjects[projectRoot.fullPath].trackedFileUpdateTimestamps = {};
164191
trackedProjects[projectRoot.fullPath].trackedFileContents = {};
165-
loadLastBackedUpFileContents(projectRoot.fullPath);
166-
// todo race condition here frequent switch between projects
192+
trackedProjects[projectRoot.fullPath].changeErrorReported = false;
193+
loadLastBackedUpFileContents(projectRoot.fullPath).catch(err=>{
194+
console.error("[recovery] loadLastBackedUpFileContents failed ", err);
195+
});
167196
return;
168197
}
169198
trackedProjects[projectRoot.fullPath] = {
@@ -176,9 +205,13 @@ define(function (require, exports, module) {
176205
lastBackedupLoadInProgress: true, // while the backup is loading, we need to prevent write over the existing
177206
// backup with backup info of the current session
178207
trackedFileUpdateTimestamps: {},
179-
trackedFileContents: {}
208+
trackedFileContents: {},
209+
restoreNotification: null,
210+
changeErrorReported: false // we only report change errors once to prevent too many Bugsnag reports
180211
};
181-
loadLastBackedUpFileContents(projectRoot.fullPath);
212+
loadLastBackedUpFileContents(projectRoot.fullPath).catch(err=>{
213+
console.error("[recovery] loadLastBackedUpFileContents failed ", err);
214+
});
182215
}
183216

184217
async function writeFileIgnoreFailure(filePath, contents) {
@@ -189,7 +222,6 @@ define(function (require, exports, module) {
189222
await jsPromise(FileUtils.writeText(file, contents, true));
190223
} catch (e) {
191224
console.error(e);
192-
logger.reportError(e); // todo too many error reports prevent every 20 secs
193225
}
194226
}
195227

@@ -217,6 +249,12 @@ define(function (require, exports, module) {
217249
}
218250

219251
let backupInProgress = false;
252+
253+
/**
254+
* This gets executed every 5 seconds and should be as light-weight as possible. If there are no changes to be
255+
* backed up, then this function should return as soon as possible without waiting for any async flows.
256+
* @return {Promise<void>}
257+
*/
220258
async function changeScanner() {
221259
let currentProjectRoot = ProjectManager.getProjectRoot();
222260
const project = trackedProjects[currentProjectRoot.fullPath];
@@ -255,21 +293,71 @@ define(function (require, exports, module) {
255293
await cleanupUntrackedFiles(docPathsToTrack, currentProjectRoot);
256294
}
257295
} catch (e) {
258-
console.error(e);
259-
logger.reportError(e);
296+
console.error("[recovery] changeScanner error", e);
297+
if(!project.changeErrorReported){
298+
project.changeErrorReported = true;
299+
// we only report change errors once to prevent too many Bugsnag reports
300+
logger.reportError(e);
301+
}
260302
}
261303
backupInProgress = false;
262304
}
263305

264306
function beforeProjectClosed() {
265-
changeScanner();
307+
let currentProjectRoot = ProjectManager.getProjectRoot();
308+
const project = trackedProjects[currentProjectRoot.fullPath];
309+
if(project.restoreNotification) {
310+
project.restoreNotification.close();
311+
}
312+
changeScanner().catch(err=>{
313+
console.error("[recovery] beforeProjectClosed failed which scanning for changes to backup", err);
314+
});
315+
}
316+
317+
async function ensureOpenEditors(pathList) {
318+
let allOpenFiles = MainViewManager.getAllOpenFiles();
319+
let openFilePaths = {};
320+
for(let file of allOpenFiles){
321+
openFilePaths[file.fullPath] = true;
322+
}
323+
for(let path of pathList) {
324+
if(!openFilePaths[path]){
325+
let file = FileSystem.getFileForPath(path);
326+
await jsPromise(FileViewController.openFileAndAddToWorkingSet(file.fullPath));
327+
}
328+
}
329+
}
330+
331+
async function restoreBtnClicked(_event, projectToRestore) {
332+
let currentProjectRoot = ProjectManager.getProjectRoot();
333+
const project = trackedProjects[currentProjectRoot.fullPath];
334+
if(!project || projectToRestore !== currentProjectRoot.fullPath){
335+
console.error(`[recovery] current project ${currentProjectRoot.fullPath} != restore ${projectToRestore}`);
336+
return;
337+
}
338+
let pathsToRestore = Object.keys(project.lastBackedUpFileContents);
339+
await ensureOpenEditors(pathsToRestore);
340+
for(let filePath of pathsToRestore){
341+
if(ProjectManager.isWithinProject(filePath)) {
342+
console.log("restoring", filePath);
343+
let document = await jsPromise(DocumentManager.getDocumentForPath(filePath));
344+
document.setText(project.lastBackedUpFileContents[filePath]);
345+
} else {
346+
console.error("[recovery] Skipping restore of non project file: ", filePath);
347+
}
348+
}
349+
if(project.restoreNotification){
350+
project.restoreNotification.close();
351+
project.restoreNotification = null;
352+
}
266353
}
267354

268355
function init() {
269-
ProjectManager.on(ProjectManager.EVENT_AFTER_PROJECT_OPEN, projectOpened);
270-
ProjectManager.on(ProjectManager.EVENT_PROJECT_BEFORE_CLOSE, beforeProjectClosed);
271-
createDir(sessionRestoreDir);
272356
if(!window.testEnvironment){
357+
ProjectManager.on(ProjectManager.EVENT_AFTER_PROJECT_OPEN, projectOpened);
358+
ProjectManager.on(ProjectManager.EVENT_PROJECT_BEFORE_CLOSE, beforeProjectClosed);
359+
exports.on("restoreProject", restoreBtnClicked);
360+
createDir(sessionRestoreDir);
273361
setInterval(changeScanner, BACKUP_INTERVAL_MS);
274362
}
275363
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div>
2+
<span>{{Strings.RECOVER_UNSAVED_FILES_MESSAGE}}</span>
3+
<br />
4+
<button
5+
style="float: right; margin-right: 6px"
6+
class="btn btn-mini remove primary"
7+
data-project="{{PROJECT_TO_RECOVER}}"
8+
onclick="EventManager.triggerEvent('ph-recovery', 'restoreProject', this.getAttribute('data-project'))"
9+
>
10+
Restore
11+
</button>
12+
</div>

src/nls/root/strings.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,8 @@ define({
795795
"RECENT_FILES_DLG_HEADER": "Recent Files",
796796
"RECENT_FILES_DLG_CLEAR_BUTTON_LABEL": "Clear",
797797
"RECENT_FILES_DLG_CLEAR_BUTTON_TITLE": "Clear files not in Working Set",
798+
"RECOVER_UNSAVED_FILES_TITLE": "Recover Unsaved Files?",
799+
"RECOVER_UNSAVED_FILES_MESSAGE": "Restore unsaved files from your previous session with this project?",
798800

799801
// Descriptions of core preferences
800802
"DESCRIPTION_CLOSE_BRACKETS": "true to automatically close braces, brackets and parentheses",

src/utils/EventManager.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@
3838
* EventManager = brackets.getModule("utils/EventManager");
3939
* EventDispatcher.makeEventDispatcher(exports);
4040
*
41-
* EventManager.registerEventHandler("drawImageHandler", exports);
41+
* EventManager.registerEventHandler("drawImage-Handler", exports);
4242
* ```
4343
* Once the event handler is registered, we can trigger events on the named handler anywhere in phoenix
4444
* (inside or outside the extension) by using:
4545
*
4646
* ```js
47-
* EventManager.triggerEvent("drawImageHandler", "someEventName", "param1", "param2", ...);
47+
* EventManager.triggerEvent("drawImage-Handler", "someEventName", "param1", "param2", ...);
4848
* ```
4949
* @module utils/EventManager
5050
*/
@@ -60,7 +60,10 @@ define(function (require, exports, module) {
6060
* const EventDispatcher = brackets.getModule("utils/EventDispatcher"),
6161
* EventDispatcher.makeEventDispatcher(exports);
6262
*
63-
* EventManager.registerEventHandler("closeDialogueHandler", exports);
63+
* // Note: for event handler names, please change the <extensionName> to your extension name
64+
* // to prevent collisions. EventHandlers starting with `ph-` and `br-` are reserved as system handlers
65+
* // and not available for use in extensions.
66+
* EventManager.registerEventHandler("<extensionName>-closeDialogueHandler", exports);
6467
* // Once the event handler is registered, see triggerEvent API on how to raise events
6568
*
6669
* @param {string} handlerName a unique name of the handler.

0 commit comments

Comments
 (0)