backupFileService.ts 10.4 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import * as path from 'vs/base/common/path';
7
import * as crypto from 'crypto';
8
import * as pfs from 'vs/base/node/pfs';
9
import { URI as Uri } from 'vs/base/common/uri';
10
import { ResourceQueue } from 'vs/base/common/async';
11
import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup';
12
import { IFileService, ITextSnapshot } from 'vs/platform/files/common/files';
13
import { readToMatchingString } from 'vs/base/node/stream';
14
import { ITextBufferFactory } from 'vs/editor/common/model';
B
Benjamin Pasero 已提交
15 16
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { keys } from 'vs/base/common/map';
17
import { Schemas } from 'vs/base/common/network';
B
Benjamin Pasero 已提交
18 19
import { IWindowService } from 'vs/platform/windows/common/windows';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
20

B
Benjamin Pasero 已提交
21
export interface IBackupFilesModel {
J
Johannes Rieken 已提交
22
	resolve(backupRoot: string): Promise<IBackupFilesModel>;
B
Benjamin Pasero 已提交
23 24 25

	add(resource: Uri, versionId?: number): void;
	has(resource: Uri, versionId?: number): boolean;
26
	get(): Uri[];
B
Benjamin Pasero 已提交
27
	remove(resource: Uri): void;
28
	count(): number;
B
Benjamin Pasero 已提交
29 30 31
	clear(): void;
}

B
Benjamin Pasero 已提交
32 33 34
export class BackupSnapshot implements ITextSnapshot {
	private preambleHandled: boolean;

B
Benjamin Pasero 已提交
35
	constructor(private snapshot: ITextSnapshot, private preamble: string) { }
B
Benjamin Pasero 已提交
36

M
Matt Bierner 已提交
37
	read(): string | null {
B
Benjamin Pasero 已提交
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
		let value = this.snapshot.read();
		if (!this.preambleHandled) {
			this.preambleHandled = true;

			if (typeof value === 'string') {
				value = this.preamble + value;
			} else {
				value = this.preamble;
			}
		}

		return value;
	}
}

B
Benjamin Pasero 已提交
53
export class BackupFilesModel implements IBackupFilesModel {
B
Benjamin Pasero 已提交
54 55
	private cache: { [resource: string]: number /* version ID */ } = Object.create(null);

J
Johannes Rieken 已提交
56
	resolve(backupRoot: string): Promise<IBackupFilesModel> {
B
Benjamin Pasero 已提交
57 58 59
		return pfs.readDirsInDir(backupRoot).then(backupSchemas => {

			// For all supported schemas
60
			return Promise.all(backupSchemas.map(backupSchema => {
B
Benjamin Pasero 已提交
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75

				// 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);
	}

B
Benjamin Pasero 已提交
76
	add(resource: Uri, versionId = 0): void {
B
Benjamin Pasero 已提交
77 78 79
		this.cache[resource.toString()] = versionId;
	}

B
Benjamin Pasero 已提交
80
	count(): number {
81 82 83
		return Object.keys(this.cache).length;
	}

B
Benjamin Pasero 已提交
84
	has(resource: Uri, versionId?: number): boolean {
B
Benjamin Pasero 已提交
85 86 87 88 89 90 91 92 93 94 95 96
		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;
	}

B
Benjamin Pasero 已提交
97
	get(): Uri[] {
98
		return Object.keys(this.cache).map(k => Uri.parse(k));
99 100
	}

B
Benjamin Pasero 已提交
101
	remove(resource: Uri): void {
B
Benjamin Pasero 已提交
102 103 104
		delete this.cache[resource.toString()];
	}

B
Benjamin Pasero 已提交
105
	clear(): void {
B
Benjamin Pasero 已提交
106 107 108 109
		this.cache = Object.create(null);
	}
}

110 111
export class BackupFileService implements IBackupFileService {

B
Benjamin Pasero 已提交
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
	_serviceBrand: any;

	private impl: IBackupFileService;

	constructor(
		@IWindowService windowService: IWindowService,
		@IFileService fileService: IFileService
	) {
		const backupWorkspacePath = windowService.getConfiguration().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(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void> {
		return this.impl.backupResource(resource, content, versionId);
	}

	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(backup: Uri): Promise<ITextBufferFactory> {
		return this.impl.resolveBackupContent(backup);
	}

	toBackupResource(resource: Uri): Uri {
		return this.impl.toBackupResource(resource);
	}
}

class BackupFileServiceImpl implements IBackupFileService {

169 170
	private static readonly META_MARKER = '\n';

B
Benjamin Pasero 已提交
171
	_serviceBrand: any;
172

173
	private backupWorkspacePath: string;
B
Benjamin Pasero 已提交
174

175
	private isShuttingDown: boolean;
J
Johannes Rieken 已提交
176
	private ready: Promise<IBackupFilesModel>;
B
Benjamin Pasero 已提交
177
	private ioOperationQueues: ResourceQueue; // queue IO operations to ensure write order
B
Benjamin Pasero 已提交
178

179
	constructor(
180
		backupWorkspacePath: string,
181
		@IFileService private readonly fileService: IFileService
182
	) {
183
		this.isShuttingDown = false;
B
Benjamin Pasero 已提交
184
		this.ioOperationQueues = new ResourceQueue();
185 186 187 188

		this.initialize(backupWorkspacePath);
	}

B
Benjamin Pasero 已提交
189
	initialize(backupWorkspacePath: string): void {
190 191
		this.backupWorkspacePath = backupWorkspacePath;

192
		this.ready = this.init();
D
Daniel Imms 已提交
193 194
	}

J
Johannes Rieken 已提交
195
	private init(): Promise<IBackupFilesModel> {
B
Benjamin Pasero 已提交
196
		const model = new BackupFilesModel();
197

198
		return model.resolve(this.backupWorkspacePath);
B
Benjamin Pasero 已提交
199 200
	}

J
Johannes Rieken 已提交
201
	hasBackups(): Promise<boolean> {
202 203 204 205 206
		return this.ready.then(model => {
			return model.count() > 0;
		});
	}

J
Johannes Rieken 已提交
207
	loadBackupResource(resource: Uri): Promise<Uri | undefined> {
B
Benjamin Pasero 已提交
208
		return this.ready.then(model => {
B
Benjamin Pasero 已提交
209

210
			// Return directly if we have a known backup with that resource
B
Benjamin Pasero 已提交
211
			const backupResource = this.toBackupResource(resource);
212 213 214
			if (model.has(backupResource)) {
				return backupResource;
			}
215

R
Rob Lourens 已提交
216
			return undefined;
B
Benjamin Pasero 已提交
217
		});
218 219
	}

J
Johannes Rieken 已提交
220
	backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void> {
221
		if (this.isShuttingDown) {
B
Benjamin Pasero 已提交
222
			return Promise.resolve();
223 224
		}

B
Benjamin Pasero 已提交
225
		return this.ready.then(model => {
226
			const backupResource = this.toBackupResource(resource);
B
Benjamin Pasero 已提交
227
			if (model.has(backupResource, versionId)) {
R
Rob Lourens 已提交
228
				return undefined; // return early if backup version id matches requested one
B
Benjamin Pasero 已提交
229
			}
230

231
			return this.ioOperationQueues.queueFor(backupResource).queue(() => {
B
Benjamin Pasero 已提交
232
				const preamble = `${resource.toString()}${BackupFileServiceImpl.META_MARKER}`;
233 234

				// Update content with value
235
				return this.fileService.updateContent(backupResource, new BackupSnapshot(content, preamble), BACKUP_FILE_UPDATE_OPTIONS).then(() => model.add(backupResource, versionId));
236
			});
B
Benjamin Pasero 已提交
237
		});
238 239
	}

J
Johannes Rieken 已提交
240
	discardResourceBackup(resource: Uri): Promise<void> {
B
Benjamin Pasero 已提交
241
		return this.ready.then(model => {
242
			const backupResource = this.toBackupResource(resource);
B
Benjamin Pasero 已提交
243

244
			return this.ioOperationQueues.queueFor(backupResource).queue(() => {
245 246
				return pfs.del(backupResource.fsPath).then(() => model.remove(backupResource));
			});
B
Benjamin Pasero 已提交
247
		});
248 249
	}

J
Johannes Rieken 已提交
250
	discardAllWorkspaceBackups(): Promise<void> {
251 252
		this.isShuttingDown = true;

B
Benjamin Pasero 已提交
253
		return this.ready.then(model => {
254
			return pfs.del(this.backupWorkspacePath).then(() => model.clear());
B
Benjamin Pasero 已提交
255 256 257
		});
	}

J
Johannes Rieken 已提交
258
	getWorkspaceFileBackups(): Promise<Uri[]> {
259
		return this.ready.then(model => {
J
Johannes Rieken 已提交
260
			const readPromises: Promise<Uri>[] = [];
B
Benjamin Pasero 已提交
261

262
			model.get().forEach(fileBackup => {
K
katainaka0503 已提交
263
				readPromises.push(
B
Benjamin Pasero 已提交
264
					readToMatchingString(fileBackup.fsPath, BackupFileServiceImpl.META_MARKER, 2000, 10000).then(Uri.parse)
K
katainaka0503 已提交
265
				);
266
			});
B
Benjamin Pasero 已提交
267

268
			return Promise.all(readPromises);
269 270 271
		});
	}

J
Johannes Rieken 已提交
272
	resolveBackupContent(backup: Uri): Promise<ITextBufferFactory> {
273 274 275 276 277 278
		return this.fileService.resolveStreamContent(backup, BACKUP_FILE_RESOLVE_OPTIONS).then(content => {

			// Add a filter method to filter out everything until the meta marker
			let metaFound = false;
			const metaPreambleFilter = (chunk: string) => {
				if (!metaFound && chunk) {
B
Benjamin Pasero 已提交
279
					const metaIndex = chunk.indexOf(BackupFileServiceImpl.META_MARKER);
280 281 282 283 284 285 286 287 288 289 290 291 292
					if (metaIndex === -1) {
						return ''; // meta not yet found, return empty string
					}

					metaFound = true;
					return chunk.substr(metaIndex + 1); // meta found, return everything after
				}

				return chunk;
			};

			return createTextBufferFactoryFromStream(content.value, metaPreambleFilter);
		});
293 294
	}

B
Benjamin Pasero 已提交
295
	toBackupResource(resource: Uri): Uri {
296
		return Uri.file(path.join(this.backupWorkspacePath, resource.scheme, hashPath(resource)));
297
	}
298
}
B
Benjamin Pasero 已提交
299 300 301

export class InMemoryBackupFileService implements IBackupFileService {

B
Benjamin Pasero 已提交
302
	_serviceBrand: any;
B
Benjamin Pasero 已提交
303 304 305

	private backups: Map<string, ITextSnapshot> = new Map();

J
Johannes Rieken 已提交
306
	hasBackups(): Promise<boolean> {
B
Benjamin Pasero 已提交
307
		return Promise.resolve(this.backups.size > 0);
B
Benjamin Pasero 已提交
308 309
	}

J
Johannes Rieken 已提交
310
	loadBackupResource(resource: Uri): Promise<Uri | undefined> {
B
Benjamin Pasero 已提交
311 312
		const backupResource = this.toBackupResource(resource);
		if (this.backups.has(backupResource.toString())) {
B
Benjamin Pasero 已提交
313
			return Promise.resolve(backupResource);
B
Benjamin Pasero 已提交
314 315
		}

J
Johannes Rieken 已提交
316
		return Promise.resolve(undefined);
B
Benjamin Pasero 已提交
317 318
	}

J
Johannes Rieken 已提交
319
	backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void> {
B
Benjamin Pasero 已提交
320 321 322
		const backupResource = this.toBackupResource(resource);
		this.backups.set(backupResource.toString(), content);

B
Benjamin Pasero 已提交
323
		return Promise.resolve();
B
Benjamin Pasero 已提交
324 325
	}

J
Johannes Rieken 已提交
326
	resolveBackupContent(backupResource: Uri): Promise<ITextBufferFactory | undefined> {
B
Benjamin Pasero 已提交
327 328
		const snapshot = this.backups.get(backupResource.toString());
		if (snapshot) {
B
Benjamin Pasero 已提交
329
			return Promise.resolve(createTextBufferFactoryFromSnapshot(snapshot));
B
Benjamin Pasero 已提交
330 331
		}

J
Johannes Rieken 已提交
332
		return Promise.resolve(undefined);
B
Benjamin Pasero 已提交
333 334
	}

J
Johannes Rieken 已提交
335
	getWorkspaceFileBackups(): Promise<Uri[]> {
B
Benjamin Pasero 已提交
336
		return Promise.resolve(keys(this.backups).map(key => Uri.parse(key)));
B
Benjamin Pasero 已提交
337 338
	}

J
Johannes Rieken 已提交
339
	discardResourceBackup(resource: Uri): Promise<void> {
B
Benjamin Pasero 已提交
340 341
		this.backups.delete(this.toBackupResource(resource).toString());

B
Benjamin Pasero 已提交
342
		return Promise.resolve();
B
Benjamin Pasero 已提交
343 344
	}

J
Johannes Rieken 已提交
345
	discardAllWorkspaceBackups(): Promise<void> {
B
Benjamin Pasero 已提交
346 347
		this.backups.clear();

B
Benjamin Pasero 已提交
348
		return Promise.resolve();
B
Benjamin Pasero 已提交
349 350 351
	}

	toBackupResource(resource: Uri): Uri {
352
		return Uri.file(path.join(resource.scheme, hashPath(resource)));
B
Benjamin Pasero 已提交
353 354
	}

355 356 357 358 359 360 361 362
}

/*
 * Exported only for testing
 */
export function hashPath(resource: Uri): string {
	const str = resource.scheme === Schemas.file ? resource.fsPath : resource.toString();
	return crypto.createHash('md5').update(str).digest('hex');
J
Johannes Rieken 已提交
363
}
B
Benjamin Pasero 已提交
364 365

registerSingleton(IBackupFileService, BackupFileService);