mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 (#6381)
* Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 * disable strict null check
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, relativePath } from 'vs/base/common/resources';
|
||||
|
||||
export const IBackupFileService = createDecorator<IBackupFileService>('backupFileService');
|
||||
|
||||
@@ -18,13 +20,19 @@ export interface IResolvedBackup<T extends object> {
|
||||
* A service that handles any I/O and state associated with the backup system.
|
||||
*/
|
||||
export interface IBackupFileService {
|
||||
_serviceBrand: any;
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
/**
|
||||
* Finds out if there are any backups stored.
|
||||
*/
|
||||
hasBackups(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Finds out if the provided resource with the given version is backed up.
|
||||
*/
|
||||
hasBackupSync(resource: URI, versionId?: number): boolean;
|
||||
|
||||
/**
|
||||
* Loads the backup resource for a particular resource within the current workspace.
|
||||
*
|
||||
@@ -80,3 +88,7 @@ export interface IBackupFileService {
|
||||
*/
|
||||
discardAllWorkspaceBackups(): Promise<void>;
|
||||
}
|
||||
|
||||
export function toBackupWorkspaceResource(backupWorkspacePath: string, environmentService: IEnvironmentService): URI {
|
||||
return joinPath(environmentService.userRoamingDataHome, relativePath(URI.file(environmentService.userDataPath), URI.file(backupWorkspacePath))!);
|
||||
}
|
||||
443
src/vs/workbench/services/backup/common/backupFileService.ts
Normal file
443
src/vs/workbench/services/backup/common/backupFileService.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { equals, deepClone } from 'vs/base/common/objects';
|
||||
import { ResourceQueue } from 'vs/base/common/async';
|
||||
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
||||
import { keys, ResourceMap } from 'vs/base/common/map';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { TextSnapshotReadable } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export interface IBackupFilesModel {
|
||||
resolve(backupRoot: URI): Promise<IBackupFilesModel>;
|
||||
|
||||
add(resource: URI, versionId?: number, meta?: object): void;
|
||||
has(resource: URI, versionId?: number, meta?: object): boolean;
|
||||
get(): URI[];
|
||||
remove(resource: URI): void;
|
||||
count(): number;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
interface IBackupCacheEntry {
|
||||
versionId?: number;
|
||||
meta?: object;
|
||||
}
|
||||
|
||||
export class BackupFilesModel implements IBackupFilesModel {
|
||||
private cache: ResourceMap<IBackupCacheEntry> = new ResourceMap();
|
||||
|
||||
constructor(private fileService: IFileService) { }
|
||||
|
||||
async resolve(backupRoot: URI): Promise<IBackupFilesModel> {
|
||||
try {
|
||||
const backupRootStat = await this.fileService.resolve(backupRoot);
|
||||
if (backupRootStat.children) {
|
||||
await Promise.all(backupRootStat.children
|
||||
.filter(child => child.isDirectory)
|
||||
.map(async backupSchema => {
|
||||
|
||||
// Read backup directory for backups
|
||||
const backupSchemaStat = await this.fileService.resolve(backupSchema.resource);
|
||||
|
||||
// Remember known backups in our caches
|
||||
if (backupSchemaStat.children) {
|
||||
backupSchemaStat.children.forEach(backupHash => this.add(backupHash.resource));
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore any errors
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
add(resource: URI, versionId = 0, meta?: object): void {
|
||||
this.cache.set(resource, { versionId, meta: deepClone(meta) }); // make sure to not store original meta in our cache...
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
has(resource: URI, versionId?: number, meta?: object): boolean {
|
||||
const entry = this.cache.get(resource);
|
||||
if (!entry) {
|
||||
return false; // unknown resource
|
||||
}
|
||||
|
||||
if (typeof versionId === 'number' && versionId !== entry.versionId) {
|
||||
return false; // different versionId
|
||||
}
|
||||
|
||||
if (meta && !equals(meta, entry.meta)) {
|
||||
return false; // different metadata
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get(): URI[] {
|
||||
return this.cache.keys();
|
||||
}
|
||||
|
||||
remove(resource: URI): void {
|
||||
this.cache.delete(resource);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class BackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
private impl: IBackupFileService;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IFileService fileService: IFileService
|
||||
) {
|
||||
const backupWorkspaceResource = environmentService.configuration.backupWorkspaceResource;
|
||||
if (backupWorkspaceResource) {
|
||||
this.impl = new BackupFileServiceImpl(backupWorkspaceResource, this.hashPath, fileService);
|
||||
} else {
|
||||
this.impl = new InMemoryBackupFileService(this.hashPath);
|
||||
}
|
||||
}
|
||||
|
||||
protected hashPath(resource: URI): string {
|
||||
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
|
||||
|
||||
return hash(str).toString(16);
|
||||
}
|
||||
|
||||
initialize(backupWorkspaceResource: URI): void {
|
||||
if (this.impl instanceof BackupFileServiceImpl) {
|
||||
this.impl.initialize(backupWorkspaceResource);
|
||||
}
|
||||
}
|
||||
|
||||
hasBackups(): Promise<boolean> {
|
||||
return this.impl.hasBackups();
|
||||
}
|
||||
|
||||
hasBackupSync(resource: URI, versionId?: number): boolean {
|
||||
return this.impl.hasBackupSync(resource, versionId);
|
||||
}
|
||||
|
||||
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
return this.impl.loadBackupResource(resource);
|
||||
}
|
||||
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
return this.impl.backupResource(resource, content, versionId, meta);
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
return this.impl.discardResourceBackup(resource);
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
return this.impl.discardAllWorkspaceBackups();
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return this.impl.getWorkspaceFileBackups();
|
||||
}
|
||||
|
||||
resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
return this.impl.resolveBackupContent(backup);
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return this.impl.toBackupResource(resource);
|
||||
}
|
||||
}
|
||||
|
||||
class BackupFileServiceImpl implements IBackupFileService {
|
||||
|
||||
private static readonly PREAMBLE_END_MARKER = '\n';
|
||||
private static readonly PREAMBLE_META_SEPARATOR = ' '; // using a character that is know to be escaped in a URI as separator
|
||||
private static readonly PREAMBLE_MAX_LENGTH = 10000;
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
private backupWorkspacePath: URI;
|
||||
|
||||
private isShuttingDown: boolean;
|
||||
private ioOperationQueues: ResourceQueue; // queue IO operations to ensure write order
|
||||
|
||||
private ready: Promise<IBackupFilesModel>;
|
||||
private model: IBackupFilesModel;
|
||||
|
||||
constructor(
|
||||
backupWorkspaceResource: URI,
|
||||
private readonly hashPath: (resource: URI) => string,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
this.isShuttingDown = false;
|
||||
this.ioOperationQueues = new ResourceQueue();
|
||||
|
||||
this.initialize(backupWorkspaceResource);
|
||||
}
|
||||
|
||||
initialize(backupWorkspaceResource: URI): void {
|
||||
this.backupWorkspacePath = backupWorkspaceResource;
|
||||
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
private init(): Promise<IBackupFilesModel> {
|
||||
this.model = new BackupFilesModel(this.fileService);
|
||||
|
||||
return this.model.resolve(this.backupWorkspacePath);
|
||||
}
|
||||
|
||||
async hasBackups(): Promise<boolean> {
|
||||
const model = await this.ready;
|
||||
|
||||
return model.count() > 0;
|
||||
}
|
||||
|
||||
hasBackupSync(resource: URI, versionId?: number): boolean {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.model.has(backupResource, versionId);
|
||||
}
|
||||
|
||||
async loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const model = await this.ready;
|
||||
|
||||
// Return directly if we have a known backup with that resource
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource)) {
|
||||
return backupResource;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = await this.ready;
|
||||
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource, versionId, meta)) {
|
||||
return; // return early if backup version id matches requested one
|
||||
}
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
||||
let preamble: string | undefined = undefined;
|
||||
|
||||
// With Metadata: URI + META-START + Meta + END
|
||||
if (meta) {
|
||||
const preambleWithMeta = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_META_SEPARATOR}${JSON.stringify(meta)}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
||||
if (preambleWithMeta.length < BackupFileServiceImpl.PREAMBLE_MAX_LENGTH) {
|
||||
preamble = preambleWithMeta;
|
||||
}
|
||||
}
|
||||
|
||||
// Without Metadata: URI + END
|
||||
if (!preamble) {
|
||||
preamble = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
||||
}
|
||||
|
||||
// Update content with value
|
||||
await this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble));
|
||||
|
||||
// Update model
|
||||
model.add(backupResource, versionId, meta);
|
||||
});
|
||||
}
|
||||
|
||||
async discardResourceBackup(resource: URI): Promise<void> {
|
||||
const model = await this.ready;
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
||||
await this.fileService.del(backupResource, { recursive: true });
|
||||
|
||||
model.remove(backupResource);
|
||||
});
|
||||
}
|
||||
|
||||
async discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
const model = await this.ready;
|
||||
|
||||
await this.fileService.del(this.backupWorkspacePath, { recursive: true });
|
||||
|
||||
model.clear();
|
||||
}
|
||||
|
||||
async getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
const model = await this.ready;
|
||||
|
||||
const backups = await Promise.all(model.get().map(async fileBackup => {
|
||||
const backupPreamble = await this.readToMatchingString(fileBackup, BackupFileServiceImpl.PREAMBLE_END_MARKER, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH);
|
||||
if (!backupPreamble) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Preamble with metadata: URI + META-START + Meta + END
|
||||
const metaStartIndex = backupPreamble.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
|
||||
if (metaStartIndex > 0) {
|
||||
return URI.parse(backupPreamble.substring(0, metaStartIndex));
|
||||
}
|
||||
|
||||
// Preamble without metadata: URI + END
|
||||
else {
|
||||
return URI.parse(backupPreamble);
|
||||
}
|
||||
}));
|
||||
|
||||
return coalesce(backups);
|
||||
}
|
||||
|
||||
private async readToMatchingString(file: URI, matchingString: string, maximumBytesToRead: number): Promise<string> {
|
||||
const contents = (await this.fileService.readFile(file, { length: maximumBytesToRead })).value.toString();
|
||||
|
||||
const newLineIndex = contents.indexOf(matchingString);
|
||||
if (newLineIndex >= 0) {
|
||||
return contents.substr(0, newLineIndex);
|
||||
}
|
||||
|
||||
throw new Error(`Could not find ${JSON.stringify(matchingString)} in first ${maximumBytesToRead} bytes of ${file}`);
|
||||
}
|
||||
|
||||
async resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
|
||||
// Metadata extraction
|
||||
let metaRaw = '';
|
||||
let metaEndFound = false;
|
||||
|
||||
// Add a filter method to filter out everything until the meta end marker
|
||||
const metaPreambleFilter = (chunk: VSBuffer) => {
|
||||
const chunkString = chunk.toString();
|
||||
|
||||
if (!metaEndFound) {
|
||||
const metaEndIndex = chunkString.indexOf(BackupFileServiceImpl.PREAMBLE_END_MARKER);
|
||||
if (metaEndIndex === -1) {
|
||||
metaRaw += chunkString;
|
||||
|
||||
return VSBuffer.fromString(''); // meta not yet found, return empty string
|
||||
}
|
||||
|
||||
metaEndFound = true;
|
||||
metaRaw += chunkString.substring(0, metaEndIndex); // ensure to get last chunk from metadata
|
||||
|
||||
return VSBuffer.fromString(chunkString.substr(metaEndIndex + 1)); // meta found, return everything after
|
||||
}
|
||||
|
||||
return chunk;
|
||||
};
|
||||
|
||||
// Read backup into factory
|
||||
const content = await this.fileService.readFileStream(backup);
|
||||
const factory = await createTextBufferFactoryFromStream(content.value, metaPreambleFilter);
|
||||
|
||||
// Trigger read for meta data extraction from the filter above
|
||||
factory.getFirstLineText(1);
|
||||
|
||||
let meta: T | undefined;
|
||||
const metaStartIndex = metaRaw.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
|
||||
if (metaStartIndex !== -1) {
|
||||
try {
|
||||
meta = JSON.parse(metaRaw.substr(metaStartIndex + 1));
|
||||
} catch (error) {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return { value: factory, meta };
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return joinPath(this.backupWorkspacePath, resource.scheme, this.hashPath(resource));
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryBackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
private backups: Map<string, ITextSnapshot> = new Map();
|
||||
|
||||
constructor(private readonly hashPath: (resource: URI) => string) { }
|
||||
|
||||
hasBackups(): Promise<boolean> {
|
||||
return Promise.resolve(this.backups.size > 0);
|
||||
}
|
||||
|
||||
hasBackupSync(resource: URI, versionId?: number): boolean {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.backups.has(backupResource.toString());
|
||||
}
|
||||
|
||||
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (this.backups.has(backupResource.toString())) {
|
||||
return Promise.resolve(backupResource);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
this.backups.set(backupResource.toString(), content);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
resolveBackupContent<T extends object>(backupResource: URI): Promise<IResolvedBackup<T>> {
|
||||
const snapshot = this.backups.get(backupResource.toString());
|
||||
if (snapshot) {
|
||||
return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
|
||||
}
|
||||
|
||||
return Promise.reject('Unexpected backup resource to resolve');
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return Promise.resolve(keys(this.backups).map(key => URI.parse(key)));
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
this.backups.delete(this.toBackupResource(resource).toString());
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.backups.clear();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return URI.file(join(resource.scheme, this.hashPath(resource)));
|
||||
}
|
||||
}
|
||||
@@ -3,407 +3,17 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { join } from 'vs/base/common/path';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { createHash } from 'crypto';
|
||||
import { BackupFileService as CommonBackupFileService } from 'vs/workbench/services/backup/common/backupFileService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { equals, deepClone } from 'vs/base/common/objects';
|
||||
import { ResourceQueue } from 'vs/base/common/async';
|
||||
import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { readToMatchingString } from 'vs/base/node/stream';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
||||
import { keys, ResourceMap } from 'vs/base/common/map';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { TextSnapshotReadable } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export interface IBackupFilesModel {
|
||||
resolve(backupRoot: URI): Promise<IBackupFilesModel>;
|
||||
export class BackupFileService extends CommonBackupFileService {
|
||||
|
||||
add(resource: URI, versionId?: number, meta?: object): void;
|
||||
has(resource: URI, versionId?: number, meta?: object): boolean;
|
||||
get(): URI[];
|
||||
remove(resource: URI): void;
|
||||
count(): number;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
interface IBackupCacheEntry {
|
||||
versionId?: number;
|
||||
meta?: object;
|
||||
}
|
||||
|
||||
export class BackupFilesModel implements IBackupFilesModel {
|
||||
private cache: ResourceMap<IBackupCacheEntry> = new ResourceMap();
|
||||
|
||||
constructor(private fileService: IFileService) { }
|
||||
|
||||
async resolve(backupRoot: URI): Promise<IBackupFilesModel> {
|
||||
try {
|
||||
const backupRootStat = await this.fileService.resolve(backupRoot);
|
||||
if (backupRootStat.children) {
|
||||
await Promise.all(backupRootStat.children
|
||||
.filter(child => child.isDirectory)
|
||||
.map(async backupSchema => {
|
||||
|
||||
// Read backup directory for backups
|
||||
const backupSchemaStat = await this.fileService.resolve(backupSchema.resource);
|
||||
|
||||
// Remember known backups in our caches
|
||||
if (backupSchemaStat.children) {
|
||||
backupSchemaStat.children.forEach(backupHash => this.add(backupHash.resource));
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore any errors
|
||||
}
|
||||
|
||||
return this;
|
||||
protected hashPath(resource: URI): string {
|
||||
return hashPath(resource);
|
||||
}
|
||||
|
||||
add(resource: URI, versionId = 0, meta?: object): void {
|
||||
this.cache.set(resource, { versionId, meta: deepClone(meta) }); // make sure to not store original meta in our cache...
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
has(resource: URI, versionId?: number, meta?: object): boolean {
|
||||
const entry = this.cache.get(resource);
|
||||
if (!entry) {
|
||||
return false; // unknown resource
|
||||
}
|
||||
|
||||
if (typeof versionId === 'number' && versionId !== entry.versionId) {
|
||||
return false; // different versionId
|
||||
}
|
||||
|
||||
if (meta && !equals(meta, entry.meta)) {
|
||||
return false; // different metadata
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get(): URI[] {
|
||||
return this.cache.keys();
|
||||
}
|
||||
|
||||
remove(resource: URI): void {
|
||||
this.cache.delete(resource);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class BackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<IBackupFileService>;
|
||||
|
||||
private impl: IBackupFileService;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IFileService fileService: IFileService
|
||||
) {
|
||||
const backupWorkspacePath = environmentService.configuration.backupPath;
|
||||
if (backupWorkspacePath) {
|
||||
this.impl = new BackupFileServiceImpl(backupWorkspacePath, fileService);
|
||||
} else {
|
||||
this.impl = new InMemoryBackupFileService();
|
||||
}
|
||||
}
|
||||
|
||||
initialize(backupWorkspacePath: string): void {
|
||||
if (this.impl instanceof BackupFileServiceImpl) {
|
||||
this.impl.initialize(backupWorkspacePath);
|
||||
}
|
||||
}
|
||||
|
||||
hasBackups(): Promise<boolean> {
|
||||
return this.impl.hasBackups();
|
||||
}
|
||||
|
||||
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
return this.impl.loadBackupResource(resource);
|
||||
}
|
||||
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
return this.impl.backupResource(resource, content, versionId, meta);
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
return this.impl.discardResourceBackup(resource);
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
return this.impl.discardAllWorkspaceBackups();
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return this.impl.getWorkspaceFileBackups();
|
||||
}
|
||||
|
||||
resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
return this.impl.resolveBackupContent(backup);
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return this.impl.toBackupResource(resource);
|
||||
}
|
||||
}
|
||||
|
||||
class BackupFileServiceImpl implements IBackupFileService {
|
||||
|
||||
private static readonly PREAMBLE_END_MARKER = '\n';
|
||||
private static readonly PREAMBLE_META_SEPARATOR = ' '; // using a character that is know to be escaped in a URI as separator
|
||||
private static readonly PREAMBLE_MAX_LENGTH = 10000;
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private backupWorkspacePath: URI;
|
||||
|
||||
private isShuttingDown: boolean;
|
||||
private ready: Promise<IBackupFilesModel>;
|
||||
private ioOperationQueues: ResourceQueue; // queue IO operations to ensure write order
|
||||
|
||||
constructor(
|
||||
backupWorkspacePath: string,
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
this.isShuttingDown = false;
|
||||
this.ioOperationQueues = new ResourceQueue();
|
||||
|
||||
this.initialize(backupWorkspacePath);
|
||||
}
|
||||
|
||||
initialize(backupWorkspacePath: string): void {
|
||||
this.backupWorkspacePath = URI.file(backupWorkspacePath);
|
||||
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
private init(): Promise<IBackupFilesModel> {
|
||||
const model = new BackupFilesModel(this.fileService);
|
||||
|
||||
return model.resolve(this.backupWorkspacePath);
|
||||
}
|
||||
|
||||
async hasBackups(): Promise<boolean> {
|
||||
const model = await this.ready;
|
||||
|
||||
return model.count() > 0;
|
||||
}
|
||||
|
||||
async loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const model = await this.ready;
|
||||
|
||||
// Return directly if we have a known backup with that resource
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource)) {
|
||||
return backupResource;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = await this.ready;
|
||||
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource, versionId, meta)) {
|
||||
return; // return early if backup version id matches requested one
|
||||
}
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
||||
let preamble: string | undefined = undefined;
|
||||
|
||||
// With Metadata: URI + META-START + Meta + END
|
||||
if (meta) {
|
||||
const preambleWithMeta = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_META_SEPARATOR}${JSON.stringify(meta)}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
||||
if (preambleWithMeta.length < BackupFileServiceImpl.PREAMBLE_MAX_LENGTH) {
|
||||
preamble = preambleWithMeta;
|
||||
}
|
||||
}
|
||||
|
||||
// Without Metadata: URI + END
|
||||
if (!preamble) {
|
||||
preamble = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`;
|
||||
}
|
||||
|
||||
// Update content with value
|
||||
await this.fileService.writeFile(backupResource, new TextSnapshotReadable(content, preamble));
|
||||
|
||||
// Update model
|
||||
model.add(backupResource, versionId, meta);
|
||||
});
|
||||
}
|
||||
|
||||
async discardResourceBackup(resource: URI): Promise<void> {
|
||||
const model = await this.ready;
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(async () => {
|
||||
await this.fileService.del(backupResource, { recursive: true });
|
||||
|
||||
model.remove(backupResource);
|
||||
});
|
||||
}
|
||||
|
||||
async discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
const model = await this.ready;
|
||||
|
||||
await this.fileService.del(this.backupWorkspacePath, { recursive: true });
|
||||
|
||||
model.clear();
|
||||
}
|
||||
|
||||
async getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
const model = await this.ready;
|
||||
|
||||
const backups = await Promise.all(model.get().map(async fileBackup => {
|
||||
const backupPreamble = await readToMatchingString(fileBackup.fsPath, BackupFileServiceImpl.PREAMBLE_END_MARKER, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH / 5, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH);
|
||||
if (!backupPreamble) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Preamble with metadata: URI + META-START + Meta + END
|
||||
const metaStartIndex = backupPreamble.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
|
||||
if (metaStartIndex > 0) {
|
||||
return URI.parse(backupPreamble.substring(0, metaStartIndex));
|
||||
}
|
||||
|
||||
// Preamble without metadata: URI + END
|
||||
else {
|
||||
return URI.parse(backupPreamble);
|
||||
}
|
||||
}));
|
||||
|
||||
return coalesce(backups);
|
||||
}
|
||||
|
||||
async resolveBackupContent<T extends object>(backup: URI): Promise<IResolvedBackup<T>> {
|
||||
|
||||
// Metadata extraction
|
||||
let metaRaw = '';
|
||||
let metaEndFound = false;
|
||||
|
||||
// Add a filter method to filter out everything until the meta end marker
|
||||
const metaPreambleFilter = (chunk: VSBuffer) => {
|
||||
const chunkString = chunk.toString();
|
||||
|
||||
if (!metaEndFound) {
|
||||
const metaEndIndex = chunkString.indexOf(BackupFileServiceImpl.PREAMBLE_END_MARKER);
|
||||
if (metaEndIndex === -1) {
|
||||
metaRaw += chunkString;
|
||||
|
||||
return VSBuffer.fromString(''); // meta not yet found, return empty string
|
||||
}
|
||||
|
||||
metaEndFound = true;
|
||||
metaRaw += chunkString.substring(0, metaEndIndex); // ensure to get last chunk from metadata
|
||||
|
||||
return VSBuffer.fromString(chunkString.substr(metaEndIndex + 1)); // meta found, return everything after
|
||||
}
|
||||
|
||||
return chunk;
|
||||
};
|
||||
|
||||
// Read backup into factory
|
||||
const content = await this.fileService.readFileStream(backup);
|
||||
const factory = await createTextBufferFactoryFromStream(content.value, metaPreambleFilter);
|
||||
|
||||
// Trigger read for meta data extraction from the filter above
|
||||
factory.getFirstLineText(1);
|
||||
|
||||
let meta: T | undefined;
|
||||
const metaStartIndex = metaRaw.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR);
|
||||
if (metaStartIndex !== -1) {
|
||||
try {
|
||||
meta = JSON.parse(metaRaw.substr(metaStartIndex + 1));
|
||||
} catch (error) {
|
||||
// ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return { value: factory, meta };
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return joinPath(this.backupWorkspacePath, resource.scheme, hashPath(resource));
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryBackupFileService implements IBackupFileService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private backups: Map<string, ITextSnapshot> = new Map();
|
||||
|
||||
hasBackups(): Promise<boolean> {
|
||||
return Promise.resolve(this.backups.size > 0);
|
||||
}
|
||||
|
||||
loadBackupResource(resource: URI): Promise<URI | undefined> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (this.backups.has(backupResource.toString())) {
|
||||
return Promise.resolve(backupResource);
|
||||
}
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
backupResource<T extends object>(resource: URI, content: ITextSnapshot, versionId?: number, meta?: T): Promise<void> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
this.backups.set(backupResource.toString(), content);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
resolveBackupContent<T extends object>(backupResource: URI): Promise<IResolvedBackup<T>> {
|
||||
const snapshot = this.backups.get(backupResource.toString());
|
||||
if (snapshot) {
|
||||
return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
|
||||
}
|
||||
|
||||
return Promise.reject('Unexpected backup resource to resolve');
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): Promise<URI[]> {
|
||||
return Promise.resolve(keys(this.backups).map(key => URI.parse(key)));
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: URI): Promise<void> {
|
||||
this.backups.delete(this.toBackupResource(resource).toString());
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.backups.clear();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return URI.file(join(resource.scheme, hashPath(resource)));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -411,7 +21,5 @@ export class InMemoryBackupFileService implements IBackupFileService {
|
||||
*/
|
||||
export function hashPath(resource: URI): string {
|
||||
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
|
||||
return createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
|
||||
registerSingleton(IBackupFileService, BackupFileService);
|
||||
return crypto.createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
@@ -11,22 +11,26 @@ import * as fs from 'fs';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { BackupFileService, BackupFilesModel, hashPath } from 'vs/workbench/services/backup/node/backupFileService';
|
||||
import { BackupFilesModel } from 'vs/workbench/services/backup/common/backupFileService';
|
||||
import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { DefaultEndOfLine } from 'vs/editor/common/model';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
|
||||
import { FileService } from 'vs/workbench/services/files/common/fileService';
|
||||
import { FileService } from 'vs/platform/files/common/fileService';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { DiskFileSystemProvider } from 'vs/workbench/services/files/node/diskFileSystemProvider';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
|
||||
import { parseArgs } from 'vs/platform/environment/node/argv';
|
||||
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { hashPath, BackupFileService } from 'vs/workbench/services/backup/node/backupFileService';
|
||||
import { BACKUPS } from 'vs/platform/environment/common/environment';
|
||||
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
|
||||
|
||||
const parentDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
|
||||
const backupHome = path.join(parentDir, 'Backups');
|
||||
const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backupfileservice');
|
||||
const appSettingsHome = path.join(userdataDir, 'User');
|
||||
const backupHome = path.join(userdataDir, 'Backups');
|
||||
const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
|
||||
|
||||
const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
|
||||
@@ -43,18 +47,10 @@ const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(u
|
||||
|
||||
class TestBackupEnvironmentService extends WorkbenchEnvironmentService {
|
||||
|
||||
private config: IWindowConfiguration;
|
||||
|
||||
constructor(workspaceBackupPath: string) {
|
||||
super(parseArgs(process.argv) as IWindowConfiguration, process.execPath);
|
||||
|
||||
this.config = Object.create(null);
|
||||
this.config.backupPath = workspaceBackupPath;
|
||||
constructor(backupPath: string) {
|
||||
super({ ...parseArgs(process.argv), ...{ backupPath, 'user-data-dir': userdataDir } } as IWindowConfiguration, process.execPath);
|
||||
}
|
||||
|
||||
get configuration(): IWindowConfiguration {
|
||||
return this.config;
|
||||
}
|
||||
}
|
||||
|
||||
class TestBackupFileService extends BackupFileService {
|
||||
@@ -62,9 +58,11 @@ class TestBackupFileService extends BackupFileService {
|
||||
readonly fileService: IFileService;
|
||||
|
||||
constructor(workspace: URI, backupHome: string, workspacesJsonPath: string) {
|
||||
const fileService = new FileService(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
|
||||
const environmentService = new TestBackupEnvironmentService(workspaceBackupPath);
|
||||
const fileService = new FileService(new NullLogService());
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService));
|
||||
|
||||
super(environmentService, fileService);
|
||||
|
||||
@@ -124,8 +122,8 @@ suite('BackupFileService', () => {
|
||||
const backupResource = fooFile;
|
||||
const workspaceHash = hashPath(workspaceResource);
|
||||
const filePathHash = hashPath(backupResource);
|
||||
const expectedPath = URI.file(path.join(backupHome, workspaceHash, 'file', filePathHash)).fsPath;
|
||||
assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
|
||||
const expectedPath = URI.file(path.join(appSettingsHome, BACKUPS, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.userData }).toString();
|
||||
assert.equal(service.toBackupResource(backupResource).toString(), expectedPath);
|
||||
});
|
||||
|
||||
test('should get the correct backup path for untitled files', () => {
|
||||
@@ -133,8 +131,8 @@ suite('BackupFileService', () => {
|
||||
const backupResource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
|
||||
const workspaceHash = hashPath(workspaceResource);
|
||||
const filePathHash = hashPath(backupResource);
|
||||
const expectedPath = URI.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath;
|
||||
assert.equal(service.toBackupResource(backupResource).fsPath, expectedPath);
|
||||
const expectedPath = URI.file(path.join(appSettingsHome, BACKUPS, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.userData }).toString();
|
||||
assert.equal(service.toBackupResource(backupResource).toString(), expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,6 +155,16 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
});
|
||||
|
||||
test('text file (with version)', async () => {
|
||||
await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false), 666);
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
assert.ok(!service.hasBackupSync(fooFile, 555));
|
||||
assert.ok(service.hasBackupSync(fooFile, 666));
|
||||
});
|
||||
|
||||
test('text file (with meta)', async () => {
|
||||
@@ -164,6 +172,7 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath).toString(), `${fooFile.toString()} {"etag":"678","orphaned":true}\ntest`);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
});
|
||||
|
||||
test('untitled file', async () => {
|
||||
@@ -171,6 +180,7 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
|
||||
assert.ok(service.hasBackupSync(untitledFile));
|
||||
});
|
||||
|
||||
test('text file (ITextSnapshot)', async () => {
|
||||
@@ -180,6 +190,8 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
@@ -190,6 +202,7 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
@@ -201,6 +214,8 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.equal(fs.existsSync(fooBackupPath), true);
|
||||
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\n${largeString}`);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
@@ -212,6 +227,8 @@ suite('BackupFileService', () => {
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
assert.equal(fs.existsSync(untitledBackupPath), true);
|
||||
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\n${largeString}`);
|
||||
assert.ok(service.hasBackupSync(untitledFile));
|
||||
|
||||
model.dispose();
|
||||
});
|
||||
});
|
||||
@@ -220,9 +237,12 @@ suite('BackupFileService', () => {
|
||||
test('text file', async () => {
|
||||
await service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
assert.ok(service.hasBackupSync(fooFile));
|
||||
|
||||
await service.discardResourceBackup(fooFile);
|
||||
assert.equal(fs.existsSync(fooBackupPath), false);
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
|
||||
assert.ok(!service.hasBackupSync(fooFile));
|
||||
});
|
||||
|
||||
test('untitled file', async () => {
|
||||
|
||||
Reference in New Issue
Block a user