Skip to content

Commit 30476cb

Browse files
committed
chore: add fs APIs createAsync,getContentsAsync,isEmptyAsync,unlinkAsync and statAsync with tests
1 parent d8c47b8 commit 30476cb

File tree

10 files changed

+399
-4
lines changed

10 files changed

+399
-4
lines changed

src/document/DocumentManager.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,15 @@ define(function (require, exports, module) {
731731
exports.createUntitledDocument = createUntitledDocument;
732732
exports.getAllOpenDocuments = getAllOpenDocuments;
733733

734+
// public events
735+
exports.EVENT_AFTER_DOCUMENT_CREATE = EVENT_AFTER_DOCUMENT_CREATE;
736+
exports.EVENT_PATH_DELETED = EVENT_PATH_DELETED;
737+
exports.EVENT_FILE_NAME_CHANGE = EVENT_FILE_NAME_CHANGE;
738+
exports.EVENT_BEFORE_DOCUMENT_DELETE = EVENT_BEFORE_DOCUMENT_DELETE;
739+
exports.EVENT_DOCUMENT_REFRESHED = EVENT_DOCUMENT_REFRESHED;
740+
exports.EVENT_DOCUMENT_CHANGE = EVENT_DOCUMENT_CHANGE;
741+
exports.EVENT_DIRTY_FLAG_CHANGED = EVENT_DIRTY_FLAG_CHANGED;
742+
734743
// For internal use only
735744
exports.notifyPathNameChanged = notifyPathNameChanged;
736745
exports.notifyPathDeleted = notifyPathDeleted;
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* GNU AGPL-3.0 License
3+
*
4+
* Copyright (c) 2021 - present core.ai . All rights reserved.
5+
* Original work Copyright (c) 2016 - 2021 Adobe Systems Incorporated. All rights reserved.
6+
*
7+
* This program is free software: you can redistribute it and/or modify it
8+
* under the terms of the GNU Affero General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
15+
* for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
19+
*
20+
*/
21+
22+
/*global path, logger, jsPromise*/
23+
24+
define(function (require, exports, module) {
25+
const NativeApp = brackets.getModule("utils/NativeApp"),
26+
FileSystem = brackets.getModule("filesystem/FileSystem"),
27+
ProjectManager = brackets.getModule("project/ProjectManager"),
28+
FileSystemError = brackets.getModule("filesystem/FileSystemError"),
29+
FileUtils = brackets.getModule("file/FileUtils"),
30+
DocumentManager = brackets.getModule("document/DocumentManager");
31+
32+
const BACKUP_INTERVAL_MS = 3000; // todo change to 20 secs
33+
// todo large number of tracked files performance issues?
34+
const sessionRestoreDir = FileSystem.getDirectoryForPath(
35+
path.normalize(NativeApp.getApplicationSupportDirectory() + "/sessionRestore"));
36+
37+
let trackingProjectRoot = null,
38+
trackingRestoreRoot = null,
39+
trackedProjectFilesMap = {},
40+
trackedFilesChangeTimestamps = {};
41+
42+
function simpleHash(str) {
43+
let hash = 0;
44+
for (let i = 0; i < str.length; i++) {
45+
let char = str.charCodeAt(i);
46+
// eslint-disable-next-line no-bitwise
47+
hash = ((hash << 5) - hash) + char;
48+
// eslint-disable-next-line no-bitwise
49+
hash = hash & hash; // Convert to 32bit integer
50+
}
51+
return Math.abs(hash) + "";
52+
}
53+
54+
function createDir(dir) {
55+
return new Promise((resolve, reject)=>{
56+
dir.create(function (err) {
57+
if (err && err !== FileSystemError.ALREADY_EXISTS) {
58+
console.error("Error creating project crash restore folder " + dir.fullPath, err);
59+
reject(err);
60+
}
61+
resolve();
62+
});
63+
});
64+
}
65+
66+
function silentlyRemoveFile(path) {
67+
return new Promise((resolve)=>{
68+
FileSystem.getFileForPath(path).unlink((err)=>{
69+
if(err) {
70+
console.error(err);
71+
}
72+
resolve();
73+
});
74+
});
75+
}
76+
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);
82+
}
83+
84+
function getRestoreFilePath(projectFilePath) {
85+
if(ProjectManager.isWithinProject(projectFilePath)) {
86+
return path.normalize(
87+
`${trackingRestoreRoot.fullPath}/${ProjectManager.getProjectRelativePath(projectFilePath)}`);
88+
}
89+
return null;
90+
}
91+
92+
// try not to use this
93+
function getProjectFilePath(restoreFilePath) {
94+
if(!restoreFilePath.startsWith(trackingRestoreRoot.fullPath)){
95+
return null;
96+
}
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;
105+
}
106+
let filePathInProject = restoreProjectRelativePath.replace(`${restoreProjectName}/`, ""); // a.html
107+
return path.normalize(`${trackingProjectRoot.fullPath}/${filePathInProject}`);
108+
}
109+
110+
function projectOpened(_event, projectRoot) {
111+
trackingProjectRoot = projectRoot;
112+
setupProjectRestoreRoot(trackingProjectRoot.fullPath);
113+
}
114+
115+
async function writeFileIgnoreFailure(filePath, contents) {
116+
try {
117+
let parentDir = FileSystem.getDirectoryForPath(path.dirname(filePath));
118+
await createDir(parentDir);
119+
let file = FileSystem.getFileForPath(filePath);
120+
await jsPromise(FileUtils.writeText(file, contents, true));
121+
} catch (e) {
122+
console.error(e);
123+
logger.reportError(e); // todo too many error reports prevent every 20 secs
124+
}
125+
}
126+
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;
133+
}
134+
}
135+
136+
async function cleanupUntrackedFiles(docPathsToTrack) {
137+
let allTrackingPaths = Object.keys(trackedProjectFilesMap);
138+
for(let trackedPath of allTrackingPaths){
139+
if(!docPathsToTrack[trackedPath]){
140+
const restoreFile = trackedProjectFilesMap[trackedPath];
141+
await silentlyRemoveFile(restoreFile);
142+
delete trackedProjectFilesMap[trackedPath];
143+
delete trackedFilesChangeTimestamps[trackedPath];
144+
}
145+
}
146+
}
147+
148+
let backupInProgress = false;
149+
async function changeScanner() {
150+
if(backupInProgress || trackingProjectRoot.fullPath === "/"){
151+
// trackingProjectRoot can be "/" if debug>open virtual file system menu is clicked. Don't track root fs
152+
return;
153+
}
154+
backupInProgress = true;
155+
try{
156+
// do backup
157+
const openDocs = DocumentManager.getAllOpenDocuments();
158+
let changedDocs = [], docPathsToTrack = {};
159+
for(let doc of openDocs){
160+
if(doc && doc.isDirty){
161+
docPathsToTrack[doc.file.fullPath] = true;
162+
const lastTrackedTimestamp = trackedFilesChangeTimestamps[doc.file.fullPath];
163+
if(!lastTrackedTimestamp || lastTrackedTimestamp !== doc.lastChangeTimestamp){
164+
// Already backed up, only need to consider it again if its contents changed
165+
changedDocs.push(doc);
166+
}
167+
}
168+
}
169+
await backupChangedDocs(changedDocs);
170+
await cleanupUntrackedFiles(docPathsToTrack);
171+
} catch (e) {
172+
console.error(e);
173+
logger.reportError(e);
174+
}
175+
backupInProgress = false;
176+
}
177+
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;
186+
}
187+
188+
function init() {
189+
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);
192+
createDir(sessionRestoreDir);
193+
if(!window.testEnvironment){
194+
setInterval(changeScanner, BACKUP_INTERVAL_MS);
195+
}
196+
}
197+
198+
exports.init = init;
199+
});

src/extensions/default/NavigationAndHistory/main.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ define(function (require, exports, module) {
4444
KeyBindingManager = brackets.getModule("command/KeyBindingManager"),
4545
ExtensionUtils = brackets.getModule("utils/ExtensionUtils"),
4646
Mustache = brackets.getModule("thirdparty/mustache/mustache"),
47-
NavigationProvider = require("NavigationProvider");
47+
NavigationProvider = require("NavigationProvider"),
48+
FileRecovery = require("FileRecovery");
4849

4950
var KeyboardPrefs = JSON.parse(require("text!keyboard.json"));
5051

@@ -831,6 +832,7 @@ define(function (require, exports, module) {
831832
});
832833

833834
AppInit.appReady(function () {
835+
FileRecovery.init();
834836
ExtensionUtils.loadStyleSheet(module, "styles/recent-files.css");
835837
NavigationProvider.init();
836838
});

src/file/FileUtils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ define(function (require, exports, module) {
5858
var MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024;
5959

6060
/**
61-
* @const {List} list of File Extensions which will be opened in external Application
61+
* @var {List} list of File Extensions which will be opened in external Application
6262
*/
6363
var extListToBeOpenedInExtApp = [];
6464

src/filesystem/Directory.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,51 @@ define(function (require, exports, module) {
120120
}
121121
}
122122

123+
/**
124+
* Read the contents of a Directory, returns a promise. If this Directory is under a watch root,
125+
* the listing will exclude any items filtered out by the watch root's filter
126+
* function.
127+
*
128+
* @return {Promise<{entries: FileSystemEntry, contentStats: FileSystemStats, contentsStatsErrors}>} An object
129+
* with attributes - entries(an array of file system entries), contentStats and contentsStatsErrors(a map from
130+
* content name to error if there is any).
131+
*/
132+
Directory.prototype.getContentsAsync = function () {
133+
let that = this;
134+
return new Promise((resolve, reject)=>{
135+
that.getContents((err, contents, contentStats, contentsStatsErrors) =>{
136+
if(err){
137+
reject(err);
138+
return;
139+
}
140+
resolve({entries: contents, contentStats, contentsStatsErrors});
141+
});
142+
});
143+
};
144+
145+
/**
146+
* Returns true if is a directory exists and is empty.
147+
*
148+
* @return {Promise<boolean>} True if directory is empty and it exists, else false.
149+
*/
150+
Directory.prototype.isEmptyAsync = function () {
151+
let that = this;
152+
return new Promise((resolve, reject)=>{
153+
that.getContents((err, contents) =>{
154+
if(err){
155+
reject(err);
156+
return;
157+
}
158+
resolve(contents.length === 0);
159+
});
160+
});
161+
};
162+
123163
/**
124164
* Read the contents of a Directory. If this Directory is under a watch root,
125165
* the listing will exclude any items filtered out by the watch root's filter
126166
* function.
127167
*
128-
* @param {Directory} directory Directory whose contents you want to get
129168
* @param {function (?string, Array.<FileSystemEntry>=, Array.<FileSystemStats>=, Object.<string, string>=)} callback
130169
* Callback that is passed an error code or the stat-able contents
131170
* of the directory along with the stats for these entries and a
@@ -212,6 +251,24 @@ define(function (require, exports, module) {
212251
}.bind(this));
213252
};
214253

254+
/**
255+
* Create a directory and returns a promise that will resolve to a stat
256+
*
257+
* @return {Promise<FileSystemStats>} resolves to the stats of the newly created dir.
258+
*/
259+
Directory.prototype.createAsync = function () {
260+
let that = this;
261+
return new Promise((resolve, reject)=>{
262+
that.create((err, stat)=>{
263+
if(err){
264+
reject(err);
265+
return;
266+
}
267+
resolve(stat);
268+
});
269+
});
270+
};
271+
215272
/**
216273
* Create a directory
217274
*

src/filesystem/FileSystem.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,7 @@ define(function (require, exports, module) {
10501050
* @param {Directory} directory To get all descendant contents from
10511051
* @return {Promise<Array[File|Directory]>} A promise that resolves with the file and directory contents
10521052
*/
1053-
FileSystem.prototype.getAllDirectoryContents = function (directory, _traversedPaths= []) {
1053+
FileSystem.prototype.getAllDirectoryContents = function (directory) {
10541054
return new Promise((resolve, reject)=>{
10551055
let contents = [];
10561056
function visitor(entry) {

src/filesystem/FileSystemEntry.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,24 @@ define(function (require, exports, module) {
341341
}.bind(this));
342342
};
343343

344+
/**
345+
* Returns a promise that resolves to the stats for the entry.
346+
*
347+
* @return {Promise<FileSystemStats>}
348+
*/
349+
FileSystemEntry.prototype.statAsync = async function () {
350+
let that = this;
351+
return new Promise((resolve, reject)=>{
352+
that.stat((err, stat)=>{
353+
if(err){
354+
reject(err);
355+
} else {
356+
resolve(stat);
357+
}
358+
});
359+
});
360+
};
361+
344362
function _ensureTrailingSlash(path) {
345363
if (path[path.length - 1] !== "/") {
346364
path += "/";
@@ -392,6 +410,25 @@ define(function (require, exports, module) {
392410
}.bind(this));
393411
};
394412

413+
/**
414+
* Permanently delete this entry. For Directories, this will delete the directory
415+
* and all of its contents. For reversible delete, see moveToTrash().
416+
*
417+
* @return {Promise<>} a promise that resolves when delete is success or rejects.
418+
*/
419+
FileSystemEntry.prototype.unlinkAsync = function () {
420+
let that = this;
421+
return new Promise((resolve, reject)=>{
422+
that.unlink((err)=>{
423+
if(err){
424+
reject(err);
425+
} else {
426+
resolve();
427+
}
428+
});
429+
});
430+
};
431+
395432
/**
396433
* Permanently delete this entry. For Directories, this will delete the directory
397434
* and all of its contents. For reversible delete, see moveToTrash().

0 commit comments

Comments
 (0)