提交 092718c5 编写于 作者: B Benjamin Pasero 提交者: GitHub

Merge pull request #16194 from Microsoft/tyriar/hot_exit/15718

Expose IBackupFileService interfaces to get lists of backed up files
......@@ -94,6 +94,72 @@ export function readExactlyByFile(file: string, totalBytes: number, callback: (e
});
}
loop();
});
}
/**
* Reads a file until a matching string is found.
*
* @param file The file to read.
* @param matchingString The string to search for.
* @param chunkBytes The number of bytes to read each iteration.
* @param maximumBytesToRead The maximum number of bytes to read before giving up.
* @param callback The finished callback.
*/
export function readToMatchingString(file: string, matchingString: string, chunkBytes: number, maximumBytesToRead: number, callback: (error: Error, result: string) => void): void {
fs.open(file, 'r', null, (err, fd) => {
if (err) {
return callback(err, null);
}
function end(err: Error, result: string): void {
fs.close(fd, (closeError: Error) => {
if (closeError) {
return callback(closeError, null);
}
if (err && (<any>err).code === 'EISDIR') {
return callback(err, null); // we want to bubble this error up (file is actually a folder)
}
return callback(null, result);
});
}
let buffer = new Buffer(maximumBytesToRead);
let bytesRead = 0;
let zeroAttempts = 0;
function loop(): void {
fs.read(fd, buffer, bytesRead, chunkBytes, null, (err, moreBytesRead) => {
if (err) {
return end(err, null);
}
// Retry up to N times in case 0 bytes where read
if (moreBytesRead === 0) {
if (++zeroAttempts === 10) {
return end(null, null);
}
return loop();
}
bytesRead += moreBytesRead;
const newLineIndex = buffer.indexOf(matchingString);
if (newLineIndex >= 0) {
return end(null, buffer.toString('utf8').substr(0, newLineIndex));
}
if (bytesRead >= maximumBytesToRead) {
return end(new Error(`Could not find ${matchingString} in first ${maximumBytesToRead} bytes of ${file}`), null);
}
return loop();
});
}
loop();
});
}
\ No newline at end of file
......@@ -56,4 +56,26 @@ suite('Stream', () => {
done();
});
});
test('readToMatchingString - ANSI', function (done: () => void) {
var file = require.toUrl('./fixtures/file.css');
stream.readToMatchingString(file, '\n', 10, 100, (error: Error, result: string) => {
assert.equal(error, null);
assert.equal(result, '/*---------------------------------------------------------------------------------------------');
done();
});
});
test('readToMatchingString - empty', function (done: () => void) {
var file = require.toUrl('./fixtures/empty.txt');
stream.readToMatchingString(file, '\n', 10, 100, (error: Error, result: string) => {
assert.equal(error, null);
assert.equal(result, null);
done();
});
});
});
\ No newline at end of file
......@@ -643,23 +643,6 @@ export class TestBackupService implements IBackupService {
export class TestBackupFileService implements IBackupFileService {
public _serviceBrand: any;
public getWorkspaceBackupPaths(): TPromise<string[]> {
return TPromise.as([]);
}
public getWorkspaceBackupPathsSync(): string[] {
return [];
}
public pushWorkspaceBackupPathsSync(workspaces: URI[]): void {
return null;
}
public getWorkspaceTextFilesWithBackupsSync(workspace: URI): string[] {
return [];
}
public hasBackup(resource: URI): TPromise<boolean> {
return TPromise.as(false);
}
......@@ -690,6 +673,14 @@ export class TestBackupFileService implements IBackupFileService {
return TPromise.as(void 0);
}
public getWorkspaceFileBackups(scheme: string): TPromise<URI[]> {
return TPromise.as([]);
}
public parseBackupContent(rawText: IRawTextContent): string {
return rawText.value.lines.join('\n');
}
public discardResourceBackup(resource: URI): TPromise<void> {
return TPromise.as(void 0);
}
......
......@@ -160,7 +160,9 @@ export class UntitledEditorModel extends StringEditorModel implements IEncodingS
// Check for backups first
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 this.textFileService.resolveTextContent(backupResource, BACKUP_FILE_RESOLVE_OPTIONS).then(rawTextContent => {
return this.backupFileService.parseBackupContent(rawTextContent);
});
}
return null;
......
......@@ -8,7 +8,7 @@
import Uri from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import { ITextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextFileEditorModelManager, IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles';
import { IResolveContentOptions, IUpdateContentOptions } from 'vs/platform/files/common/files';
import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
......@@ -57,6 +57,23 @@ export interface IBackupFileService {
*/
backupResource(resource: Uri, content: string, versionId?: number): TPromise<void>;
/**
* Gets a list of file backups for the current workspace.
*
* @param scheme The scheme of the backup.
* @return The list of backups.
*/
getWorkspaceFileBackups(scheme: string): TPromise<Uri[]>;
/**
* Parses backup raw text content into the content, removing the metadata that is also stored
* in the file.
*
* @param rawText The IRawTextContent from a backup resource.
* @return The backup file's backed up content.
*/
parseBackupContent(rawText: IRawTextContent): string;
/**
* Discards the backup associated with a resource if it exists..
*
......
......@@ -13,6 +13,8 @@ import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS } from 'vs/workbench/ser
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
import { readToMatchingString } from 'vs/base/node/stream';
import { IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles';
export interface IBackupFilesModel {
resolve(backupRoot: string): TPromise<IBackupFilesModel>;
......@@ -21,6 +23,7 @@ export interface IBackupFilesModel {
has(resource: Uri, versionId?: number): boolean;
remove(resource: Uri): void;
clear(): void;
getFilesByScheme(scheme: string): Uri[];
}
// TODO@daniel this should resolve the backups with their file names once we have the metadata in place
......@@ -64,6 +67,10 @@ export class BackupFilesModel implements IBackupFilesModel {
return true;
}
public getFilesByScheme(scheme: string): Uri[] {
return Object.keys(this.cache).filter(k => path.basename(path.dirname(k)) === scheme).map(k => Uri.parse(k));
}
public remove(resource: Uri): void {
delete this.cache[resource.toString()];
}
......@@ -147,6 +154,9 @@ export class BackupFileService implements IBackupFileService {
return void 0; // return early if backup version id matches requested one
}
// Add metadata to top of file
content = `${resource.toString()}\n${content}`;
return this.fileService.updateContent(backupResource, content, BACKUP_FILE_UPDATE_OPTIONS).then(() => model.add(backupResource, versionId));
});
}
......@@ -172,13 +182,33 @@ export class BackupFileService implements IBackupFileService {
});
}
public getWorkspaceFileBackups(scheme: string): TPromise<Uri[]> {
return this.ready.then(model => {
let readPromises: TPromise<Uri>[] = [];
model.getFilesByScheme(scheme).forEach(textFile => {
readPromises.push(new TPromise<Uri>((c, e) => {
readToMatchingString(textFile.fsPath, '\n', 2000, 10000, (error, result) => {
if (result === null) {
e(error);
}
c(Uri.parse(result));
});
}));
});
return TPromise.join(readPromises);
});
}
public parseBackupContent(rawText: IRawTextContent): string {
return rawText.value.lines.slice(1).join('\n');
}
protected getBackupResource(resource: Uri): Uri {
if (!this.backupEnabled) {
return null;
}
// 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 backupName = crypto.createHash('md5').update(resource.fsPath).digest('hex');
const backupPath = path.join(this.backupWorkspacePath, resource.scheme, backupName);
return Uri.file(backupPath);
......
......@@ -18,6 +18,8 @@ import { BackupFileService, BackupFilesModel } from 'vs/workbench/services/backu
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';
import { TextModel } from 'vs/editor/common/model/textModel';
import { IRawTextContent } from 'vs/workbench/services/textfile/common/textfiles';
class TestEnvironmentService extends EnvironmentService {
......@@ -48,14 +50,14 @@ suite('BackupFileService', () => {
const backupHome = path.join(parentDir, 'Backups');
const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
const workspaceResource = Uri.file(platform.isWindows ? 'C:\\workspace' : '/workspace');
const workspaceResource = Uri.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
const workspaceBackupPath = path.join(backupHome, crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'));
const fooFile = Uri.file(platform.isWindows ? 'C:\\foo' : '/foo');
const barFile = Uri.file(platform.isWindows ? 'C:\\bar' : '/bar');
const fooFile = Uri.file(platform.isWindows ? 'c:\\foo' : '/foo');
const barFile = Uri.file(platform.isWindows ? 'c:\\bar' : '/bar');
const untitledFile = Uri.from({ scheme: 'untitled', path: 'Untitled-1' });
const fooBackupPath = path.join(workspaceBackupPath, 'file', crypto.createHash('md5').update(fooFile.fsPath).digest('hex'));
const barBackupPath = path.join(workspaceBackupPath, 'file', crypto.createHash('md5').update(barFile.fsPath).digest('hex'));
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', untitledFile.fsPath);
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', crypto.createHash('md5').update(untitledFile.fsPath).digest('hex'));
let service: TestBackupFileService;
......@@ -89,7 +91,8 @@ suite('BackupFileService', () => {
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePath>
const backupResource = Uri.from({ scheme: 'untitled', path: 'Untitled-1' });
const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex');
const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'untitled', backupResource.fsPath)).fsPath;
const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex');
const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath;
assert.equal(service.getBackupResource(backupResource).fsPath, expectedPath);
});
......@@ -108,7 +111,7 @@ suite('BackupFileService', () => {
service.backupResource(fooFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
assert.equal(fs.existsSync(fooBackupPath), true);
assert.equal(fs.readFileSync(fooBackupPath), 'test');
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
done();
});
});
......@@ -117,7 +120,7 @@ suite('BackupFileService', () => {
service.backupResource(untitledFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
assert.equal(fs.existsSync(untitledBackupPath), true);
assert.equal(fs.readFileSync(untitledBackupPath), 'test');
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
done();
});
});
......@@ -170,6 +173,62 @@ suite('BackupFileService', () => {
});
});
test('getWorkspaceFileBackups("file") - text file', done => {
service.backupResource(fooFile, `test`).then(() => {
service.getWorkspaceFileBackups('file').then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath]);
service.backupResource(barFile, `test`).then(() => {
service.getWorkspaceFileBackups('file').then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath, barFile.fsPath]);
done();
});
});
});
});
});
test('getWorkspaceFileBackups("file") - untitled file', done => {
service.backupResource(untitledFile, `test`).then(() => {
service.getWorkspaceFileBackups('file').then(textFiles => {
assert.deepEqual(textFiles, []);
done();
});
});
});
test('getWorkspaceFileBackups("untitled") - text file', done => {
service.backupResource(fooFile, `test`).then(() => {
service.backupResource(barFile, `test`).then(() => {
service.getWorkspaceFileBackups('untitled').then(textFiles => {
assert.deepEqual(textFiles, []);
done();
});
});
});
});
test('getWorkspaceFileBackups("untitled") - untitled file', done => {
service.backupResource(untitledFile, `test`).then(() => {
service.getWorkspaceFileBackups('untitled').then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), ['Untitled-1']);
done();
});
});
});
test('parseBackupContent', () => {
const rawTextContent: IRawTextContent = {
resource: null,
name: null,
mtime: null,
etag: null,
encoding: null,
value: TextModel.toRawText('metadata\ncontent', TextModel.DEFAULT_CREATION_OPTIONS),
valueLogicalHash: null
};
assert.equal(service.parseBackupContent(rawTextContent), 'content');
});
test('BackupFilesModel - simple', () => {
const model = new BackupFilesModel();
......@@ -230,4 +289,22 @@ suite('BackupFileService', () => {
});
});
});
test('BackupFilesModel - getFilesByScheme', () => {
const model = new BackupFilesModel();
assert.deepEqual(model.getFilesByScheme('file'), []);
assert.deepEqual(model.getFilesByScheme('untitled'), []);
const file1 = Uri.file('/root/file/foo.html');
const file2 = Uri.file('/root/file/bar.html');
const untitled = Uri.file('/root/untitled/bar.html');
model.add(file1);
model.add(file2);
model.add(untitled);
assert.deepEqual(model.getFilesByScheme('file').map(f => f.fsPath), [file1.fsPath, file2.fsPath]);
assert.deepEqual(model.getFilesByScheme('untitled').map(f => f.fsPath), [untitled.fsPath]);
});
});
\ No newline at end of file
......@@ -290,11 +290,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
diag('load() - created text editor model', this.resource, new Date());
return this.backupFileService.loadBackupResource(this.resource).then(backupResource => {
let resolveBackupPromise: TPromise<IRawText>;
let resolveBackupPromise: TPromise<string | IRawText>;
// Try get restore content, if there is an issue fallback silently to the original file's content
if (backupResource) {
resolveBackupPromise = this.textFileService.resolveTextContent(backupResource, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => backup.value, error => content.value);
resolveBackupPromise = this.textFileService.resolveTextContent(backupResource, BACKUP_FILE_RESOLVE_OPTIONS).then(backup => {
// The first line of a backup text file is the file name
return this.backupFileService.parseBackupContent(backup);
}, error => content.value);
} else {
resolveBackupPromise = TPromise.as(content.value);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册