提交 0afa144c 编写于 作者: B Benjamin Pasero 提交者: GitHub

Merge pull request #15892 from Microsoft/ben/hot-exit-model

Introduce a backup file model inside IBackupFileService
......@@ -668,6 +668,16 @@ export class TestBackupFileService implements IBackupFileService {
return TPromise.as(false);
}
public loadBackupResource(resource: URI): TPromise<URI> {
return this.hasBackup(resource).then(hasBackup => {
if (hasBackup) {
return this.getBackupResource(resource);
}
return void 0;
});
}
public registerResourceForBackup(resource: URI): TPromise<void> {
return TPromise.as(void 0);
}
......
......@@ -158,9 +158,9 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
public load(): TPromise<EditorModel> {
// Check for backups first
return this.backupFileService.hasBackup(this.resource).then(hasBackup => {
if (hasBackup) {
return this.textFileService.resolveTextContent(this.backupFileService.getBackupResource(this.resource), BACKUP_FILE_RESOLVE_OPTIONS).then(rawTextContent => rawTextContent.value.lines.join('\n'));
return this.backupFileService.loadBackupResource(this.resource).then(backupResource => {
if (backupResource) {
return this.textFileService.resolveTextContent(backupResource, BACKUP_FILE_RESOLVE_OPTIONS).then(rawTextContent => rawTextContent.value.lines.join('\n'));
}
return null;
......
......@@ -56,7 +56,7 @@ export class BackupModelTracker implements IWorkbenchContribution {
} else if (event.kind === StateChange.CONTENT_CHANGE) {
if (this.backupService.isHotExitEnabled) {
const model = this.textFileService.models.get(event.resource);
this.backupFileService.backupResource(model.getResource(), model.getValue());
this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId());
}
}
}
......@@ -65,7 +65,7 @@ export class BackupModelTracker implements IWorkbenchContribution {
if (this.backupService.isHotExitEnabled) {
const input = this.untitledEditorService.get(resource);
if (input.isDirty()) {
input.resolve().then(model => this.backupFileService.backupResource(resource, model.getValue()));
input.resolve().then(model => this.backupFileService.backupResource(resource, model.getValue(), model.getVersionId()));
} else {
this.discardBackup(resource);
}
......
......@@ -55,8 +55,8 @@ export class BackupRestorer implements IWorkbenchContribution {
});
TPromise.join(Object.keys(fileResources).map(resource => {
return this.backupFileService.hasBackup(URI.parse(resource)).then(hasBackup => {
if (hasBackup) {
return this.backupFileService.loadBackupResource(URI.parse(resource)).then(backupResource => {
if (backupResource) {
return fileResources[resource].resolve();
}
});
......
......@@ -40,28 +40,21 @@ export interface IBackupFileService {
_serviceBrand: any;
/**
* Gets whether a text file has a backup to restore.
*
* @param resource The resource to check.
* @returns Whether the file has a backup.
*/
hasBackup(resource: Uri): TPromise<boolean>;
/**
* Gets the backup resource for a particular resource within the current workspace.
* Loads the backup resource for a particular resource within the current workspace.
*
* @param resource The resource that is backed up.
* @return The backup resource.
* @return The backup resource if any.
*/
getBackupResource(resource: Uri): Uri;
loadBackupResource(resource: Uri): TPromise<Uri>;
/**
* Backs up a resource.
*
* @param resource The resource to back up.
* @param content THe content of the resource.
* @param content The content of the resource.
* @param versionId The version id of the resource to backup.
*/
backupResource(resource: Uri, content: string): TPromise<void>;
backupResource(resource: Uri, content: string, versionId?: number): TPromise<void>;
/**
* Discards the backup associated with a resource if it exists..
......
......@@ -14,6 +14,65 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { IFileService } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
export interface IBackupFilesModel {
resolve(backupRoot: string): TPromise<IBackupFilesModel>;
add(resource: Uri, versionId?: number): void;
has(resource: Uri, versionId?: number): boolean;
remove(resource: Uri): void;
clear(): void;
}
// TODO@daniel this should resolve the backups with their file names once we have the metadata in place
export class BackupFilesModel implements IBackupFilesModel {
private cache: { [resource: string]: number /* version ID */ } = Object.create(null);
public resolve(backupRoot: string): TPromise<IBackupFilesModel> {
return pfs.readDirsInDir(backupRoot).then(backupSchemas => {
// For all supported schemas
return TPromise.join(backupSchemas.map(backupSchema => {
// Read backup directory for backups
const backupSchemaPath = path.join(backupRoot, backupSchema);
return pfs.readdir(backupSchemaPath).then(backupHashes => {
// Remember known backups in our caches
backupHashes.forEach(backupHash => {
const backupResource = Uri.file(path.join(backupSchemaPath, backupHash));
this.add(backupResource);
});
});
}));
}).then(() => this, error => this);
}
public add(resource: Uri, versionId = 0): void {
this.cache[resource.toString()] = versionId;
}
public has(resource: Uri, versionId?: number): boolean {
const cachedVersionId = this.cache[resource.toString()];
if (typeof cachedVersionId !== 'number') {
return false; // unknown resource
}
if (typeof versionId === 'number') {
return versionId === cachedVersionId; // if we are asked with a specific version ID, make sure to test for it
}
return true;
}
public remove(resource: Uri): void {
delete this.cache[resource.toString()];
}
public clear(): void {
this.cache = Object.create(null);
}
}
export class BackupFileService implements IBackupFileService {
public _serviceBrand: any;
......@@ -21,6 +80,9 @@ export class BackupFileService implements IBackupFileService {
protected backupHome: string;
protected workspacesJsonPath: string;
private backupWorkspacePath: string;
private ready: TPromise<IBackupFilesModel>;
constructor(
private currentWorkspace: Uri,
@IEnvironmentService private environmentService: IEnvironmentService,
......@@ -28,70 +90,97 @@ export class BackupFileService implements IBackupFileService {
) {
this.backupHome = environmentService.backupHome;
this.workspacesJsonPath = environmentService.backupWorkspacesPath;
if (this.currentWorkspace) {
const workspaceHash = crypto.createHash('md5').update(this.currentWorkspace.fsPath).digest('hex');
this.backupWorkspacePath = path.join(this.backupHome, workspaceHash);
}
this.ready = this.init();
}
private get backupEnabled(): boolean {
return this.currentWorkspace && !this.environmentService.isExtensionDevelopment; // Hot exit is disabled for empty workspaces and when doing extension development
}
public hasBackup(resource: Uri): TPromise<boolean> {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return TPromise.as(false);
}
private init(): TPromise<IBackupFilesModel> {
const model = new BackupFilesModel();
return pfs.exists(backupResource.fsPath);
}
private getBackupHash(resource: Uri): string {
if (!this.backupEnabled) {
return null;
return TPromise.as(model);
}
// Only hash the file path if the file is not untitled
return resource.scheme === 'untitled' ? resource.fsPath : crypto.createHash('md5').update(resource.fsPath).digest('hex');
return model.resolve(this.backupWorkspacePath);
}
public getBackupResource(resource: Uri): Uri {
const backupHash = this.getBackupHash(resource);
if (!backupHash) {
return null;
}
const backupPath = path.join(this.getWorkspaceBackupDirectory(), resource.scheme, backupHash);
return Uri.file(backupPath);
public hasBackup(resource: Uri): TPromise<boolean> {
return this.ready.then(model => {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return TPromise.as(false);
}
return model.has(backupResource);
});
}
private getWorkspaceBackupDirectory(): string {
const workspaceHash = crypto.createHash('md5').update(this.currentWorkspace.fsPath).digest('hex');
public loadBackupResource(resource: Uri): TPromise<Uri> {
return this.ready.then(() => {
return this.hasBackup(resource).then(hasBackup => {
if (hasBackup) {
return this.getBackupResource(resource);
}
return path.join(this.backupHome, workspaceHash);
return void 0;
});
});
}
public backupResource(resource: Uri, content: string): TPromise<void> {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return TPromise.as(void 0);
}
public backupResource(resource: Uri, content: string, versionId?: number): TPromise<void> {
return this.ready.then(model => {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return void 0;
}
return this.fileService.updateContent(backupResource, content, BACKUP_FILE_UPDATE_OPTIONS).then(() => void 0);
if (model.has(backupResource, versionId)) {
return void 0; // return early if backup version id matches requested one
}
return this.fileService.updateContent(backupResource, content, BACKUP_FILE_UPDATE_OPTIONS).then(() => model.add(backupResource, versionId));
});
}
public discardResourceBackup(resource: Uri): TPromise<void> {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return TPromise.as(void 0);
}
return this.fileService.del(backupResource);
return this.ready.then(model => {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return void 0;
}
return this.fileService.del(backupResource).then(() => model.remove(backupResource));
});
}
public discardAllWorkspaceBackups(): TPromise<void> {
return this.ready.then(model => {
if (!this.backupEnabled) {
return void 0;
}
return this.fileService.del(Uri.file(this.backupWorkspacePath)).then(() => model.clear());
});
}
protected getBackupResource(resource: Uri): Uri {
if (!this.backupEnabled) {
return TPromise.as(void 0);
return null;
}
return this.fileService.del(Uri.file(this.getWorkspaceBackupDirectory()));
// Only hash the file path if the file is not untitled
const backupName = resource.scheme === 'untitled' ? resource.fsPath : crypto.createHash('md5').update(resource.fsPath).digest('hex');
const backupPath = path.join(this.backupWorkspacePath, resource.scheme, backupName);
return Uri.file(backupPath);
}
}
\ No newline at end of file
......@@ -73,7 +73,7 @@ export class BackupService implements IBackupService {
private doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: Uri[]): TPromise<void> {
// Handle file resources first
return TPromise.join(dirtyFileModels.map(model => {
return this.backupFileService.backupResource(model.getResource(), model.getValue());
return this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId());
})).then(results => {
// Handle untitled resources
const untitledModelPromises = untitledResources.map(untitledResource => this.untitledEditorService.get(untitledResource))
......@@ -82,7 +82,7 @@ export class BackupService implements IBackupService {
return TPromise.join(untitledModelPromises).then(untitledModels => {
const untitledBackupPromises = untitledModels.map(model => {
return this.backupFileService.backupResource(model.getResource(), model.getValue());
return this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId());
});
return TPromise.join(untitledBackupPromises).then(() => void 0);
});
......
......@@ -14,7 +14,7 @@ import path = require('path');
import extfs = require('vs/base/node/extfs');
import pfs = require('vs/base/node/pfs');
import Uri from 'vs/base/common/uri';
import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileService';
import { BackupFileService, BackupFilesModel } from 'vs/workbench/services/backup/node/backupFileService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs } from 'vs/platform/environment/node/argv';
......@@ -23,9 +23,6 @@ class TestEnvironmentService extends EnvironmentService {
constructor(private _backupHome: string, private _backupWorkspacesPath: string) {
super(parseArgs(process.argv), process.execPath);
this._backupHome = this._backupHome || this.backupHome;
this._backupWorkspacesPath = this._backupWorkspacesPath || this.backupWorkspacesPath;
}
get backupHome(): string { return this._backupHome; }
......@@ -40,6 +37,10 @@ class TestBackupFileService extends BackupFileService {
super(workspace, testEnvironmentService, fileService);
}
public getBackupResource(resource: Uri): Uri {
return super.getBackupResource(resource);
}
}
suite('BackupFileService', () => {
......@@ -56,7 +57,7 @@ suite('BackupFileService', () => {
const barBackupPath = path.join(workspaceBackupPath, 'file', crypto.createHash('md5').update(barFile.fsPath).digest('hex'));
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', untitledFile.fsPath);
let service: BackupFileService;
let service: TestBackupFileService;
setup(done => {
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
......@@ -93,14 +94,12 @@ suite('BackupFileService', () => {
});
test('doesTextFileHaveBackup should return whether a backup resource exists', done => {
service.hasBackup(fooFile).then(exists => {
assert.equal(exists, false);
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
service.hasBackup(fooFile).then(exists2 => {
assert.equal(exists2, true);
done();
});
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
service.hasBackup(fooFile).then(exists2 => {
assert.equal(exists2, true);
done();
});
});
});
......@@ -170,4 +169,65 @@ suite('BackupFileService', () => {
});
});
});
test('BackupFilesModel - simple', () => {
const model = new BackupFilesModel();
const resource1 = Uri.file('test.html');
assert.equal(model.has(resource1), false);
model.add(resource1);
assert.equal(model.has(resource1), true);
assert.equal(model.has(resource1, 0), true);
assert.equal(model.has(resource1, 1), false);
model.remove(resource1);
assert.equal(model.has(resource1), false);
model.add(resource1);
assert.equal(model.has(resource1), true);
assert.equal(model.has(resource1, 0), true);
assert.equal(model.has(resource1, 1), false);
model.clear();
assert.equal(model.has(resource1), false);
model.add(resource1, 1);
assert.equal(model.has(resource1), true);
assert.equal(model.has(resource1, 0), false);
assert.equal(model.has(resource1, 1), true);
const resource2 = Uri.file('test1.html');
const resource3 = Uri.file('test2.html');
const resource4 = Uri.file('test3.html');
model.add(resource2);
model.add(resource3);
model.add(resource4);
assert.equal(model.has(resource1), true);
assert.equal(model.has(resource2), true);
assert.equal(model.has(resource3), true);
assert.equal(model.has(resource4), true);
});
test('BackupFilesModel - resolve', (done) => {
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
const model = new BackupFilesModel();
model.resolve(workspaceBackupPath).then(model => {
assert.equal(model.has(Uri.file(fooBackupPath)), true);
done();
});
});
});
});
\ No newline at end of file
......@@ -289,13 +289,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
else {
diag('load() - created text editor model', this.resource, new Date());
return this.backupFileService.hasBackup(this.resource).then(backupExists => {
return this.backupFileService.loadBackupResource(this.resource).then(backupResource => {
let resolveBackupPromise: TPromise<IRawText>;
// Try get restore content, if there is an issue fallback silently to the original file's content
if (backupExists) {
const restoreResource = this.backupFileService.getBackupResource(this.resource);
resolveBackupPromise = this.textFileService.resolveTextContent(restoreResource, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => backup.value, error => content.value);
if (backupResource) {
resolveBackupPromise = this.textFileService.resolveTextContent(backupResource, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => backup.value, error => content.value);
} else {
resolveBackupPromise = TPromise.as(content.value);
}
......@@ -304,7 +303,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return this.createTextEditorModel(fileContent, content.resource).then(() => {
this.createTextEditorModelPromise = null;
if (backupExists) {
if (backupResource) {
this.makeDirty();
} else {
this.setDirty(false); // Ensure we are not tracking a stale state
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册