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

'use strict';

import * as path from 'path';
import * as crypto from 'crypto';
10
import * as platform from 'vs/base/common/platform';
11 12
import pfs = require('vs/base/node/pfs');
import Uri from 'vs/base/common/uri';
13
import { Queue } from 'vs/base/common/async';
14
import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS } from 'vs/workbench/services/backup/common/backup';
15 16
import { IFileService } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
17
import { readToMatchingString } from 'vs/base/node/stream';
A
Alex Dima 已提交
18 19
import { TextSource, IRawTextSource } from 'vs/editor/common/model/textSource';
import { DefaultEndOfLine } from 'vs/editor/common/editorCommon';
20

B
Benjamin Pasero 已提交
21 22
export interface IBackupFilesModel {
	resolve(backupRoot: string): TPromise<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
export class BackupFilesModel implements IBackupFilesModel {
B
Benjamin Pasero 已提交
33 34
	private cache: { [resource: string]: number /* version ID */ } = Object.create(null);

B
Benjamin Pasero 已提交
35
	public resolve(backupRoot: string): TPromise<IBackupFilesModel> {
B
Benjamin Pasero 已提交
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
		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);
	}

B
Benjamin Pasero 已提交
55
	public add(resource: Uri, versionId = 0): void {
B
Benjamin Pasero 已提交
56 57 58
		this.cache[resource.toString()] = versionId;
	}

59 60 61 62
	public count(): number {
		return Object.keys(this.cache).length;
	}

B
Benjamin Pasero 已提交
63
	public has(resource: Uri, versionId?: number): boolean {
B
Benjamin Pasero 已提交
64 65 66 67 68 69 70 71 72 73 74 75
		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;
	}

76 77
	public get(): Uri[] {
		return Object.keys(this.cache).map(k => Uri.parse(k));
78 79
	}

B
Benjamin Pasero 已提交
80
	public remove(resource: Uri): void {
B
Benjamin Pasero 已提交
81 82 83
		delete this.cache[resource.toString()];
	}

B
Benjamin Pasero 已提交
84
	public clear(): void {
B
Benjamin Pasero 已提交
85 86 87 88
		this.cache = Object.create(null);
	}
}

89 90 91 92
export class BackupFileService implements IBackupFileService {

	public _serviceBrand: any;

B
Benjamin Pasero 已提交
93 94
	private static readonly META_MARKER = '\n';

95
	private isShuttingDown: boolean;
B
Benjamin Pasero 已提交
96
	private ready: TPromise<IBackupFilesModel>;
97 98 99 100
	/**
	 * Ensure IO operations on individual files are performed in order, this could otherwise lead
	 * to unexpected behavior when backups are persisted and discarded in the wrong order.
	 */
D
Daniel Imms 已提交
101
	private ioOperationQueues: { [path: string]: Queue<void> };
B
Benjamin Pasero 已提交
102

103
	constructor(
104 105
		private backupWorkspacePath: string,
		@IFileService private fileService: IFileService
106
	) {
107
		this.isShuttingDown = false;
D
Daniel Imms 已提交
108
		this.ioOperationQueues = {};
109
		this.ready = this.init();
D
Daniel Imms 已提交
110 111
	}

B
Benjamin Pasero 已提交
112
	private get backupEnabled(): boolean {
113
		return !!this.backupWorkspacePath; // Hot exit requires a backup path
B
Benjamin Pasero 已提交
114 115
	}

116
	private init(): TPromise<IBackupFilesModel> {
B
Benjamin Pasero 已提交
117
		const model = new BackupFilesModel();
118

B
Benjamin Pasero 已提交
119
		if (!this.backupEnabled) {
B
Benjamin Pasero 已提交
120
			return TPromise.as(model);
121 122
		}

123
		return model.resolve(this.backupWorkspacePath);
B
Benjamin Pasero 已提交
124 125
	}

126 127 128 129 130 131
	public hasBackups(): TPromise<boolean> {
		return this.ready.then(model => {
			return model.count() > 0;
		});
	}

132
	public loadBackupResource(resource: Uri): TPromise<Uri> {
B
Benjamin Pasero 已提交
133 134 135
		return this.ready.then(model => {
			const backupResource = this.getBackupResource(resource);
			if (!backupResource) {
136
				return void 0;
B
Benjamin Pasero 已提交
137
			}
B
Benjamin Pasero 已提交
138

139 140 141 142
			// Return directly if we have a known backup with that resource
			if (model.has(backupResource)) {
				return backupResource;
			}
143

144 145 146 147 148 149 150
			// Otherwise: on Windows and Mac pre v1.11 we used to store backups in lowercase format
			// Therefor we also want to check if we have backups of this old format hanging around
			// TODO@Ben migration
			if (platform.isWindows || platform.isMacintosh) {
				const legacyBackupResource = this.getBackupResource(resource, true /* legacyMacWindowsFormat */);
				if (model.has(legacyBackupResource)) {
					return legacyBackupResource;
B
Benjamin Pasero 已提交
151
				}
152
			}
B
Benjamin Pasero 已提交
153

154
			return void 0;
B
Benjamin Pasero 已提交
155
		});
156 157
	}

B
Benjamin Pasero 已提交
158
	public backupResource(resource: Uri, content: string, versionId?: number): TPromise<void> {
159 160 161 162
		if (this.isShuttingDown) {
			return TPromise.as(void 0);
		}

B
Benjamin Pasero 已提交
163
		return this.ready.then(model => {
B
Benjamin Pasero 已提交
164 165
			const backupResource = this.getBackupResource(resource);
			if (!backupResource) {
B
Benjamin Pasero 已提交
166
				return void 0;
B
Benjamin Pasero 已提交
167 168
			}

B
Benjamin Pasero 已提交
169
			if (model.has(backupResource, versionId)) {
B
Benjamin Pasero 已提交
170
				return void 0; // return early if backup version id matches requested one
B
Benjamin Pasero 已提交
171
			}
172

173
			// Add metadata to top of file
B
Benjamin Pasero 已提交
174
			content = `${resource.toString()}${BackupFileService.META_MARKER}${content}`;
175

D
Daniel Imms 已提交
176
			return this.getResourceIOQueue(backupResource).queue(() => {
177 178
				return this.fileService.updateContent(backupResource, content, BACKUP_FILE_UPDATE_OPTIONS).then(() => model.add(backupResource, versionId));
			});
B
Benjamin Pasero 已提交
179
		});
180 181
	}

182
	public discardResourceBackup(resource: Uri): TPromise<void> {
B
Benjamin Pasero 已提交
183
		return this.ready.then(model => {
B
Benjamin Pasero 已提交
184 185
			const backupResource = this.getBackupResource(resource);
			if (!backupResource) {
B
Benjamin Pasero 已提交
186
				return void 0;
B
Benjamin Pasero 已提交
187
			}
B
Benjamin Pasero 已提交
188

D
Daniel Imms 已提交
189
			return this.getResourceIOQueue(backupResource).queue(() => {
190
				return pfs.del(backupResource.fsPath).then(() => model.remove(backupResource));
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
			}).then(() => {

				// On Windows and Mac pre v1.11 we used to store backups in lowercase format
				// Therefor we also want to check if we have backups of this old format laying around
				// TODO@Ben migration
				if (platform.isWindows || platform.isMacintosh) {
					const legacyBackupResource = this.getBackupResource(resource, true /* legacyMacWindowsFormat */);
					if (model.has(legacyBackupResource)) {
						return this.getResourceIOQueue(legacyBackupResource).queue(() => {
							return pfs.del(legacyBackupResource.fsPath).then(() => model.remove(legacyBackupResource));
						});
					}
				}

				return TPromise.as(void 0);
206
			});
B
Benjamin Pasero 已提交
207
		});
208 209
	}

D
Daniel Imms 已提交
210 211 212
	private getResourceIOQueue(resource: Uri) {
		const key = resource.toString();
		if (!this.ioOperationQueues[key]) {
213
			const queue = new Queue<void>();
D
Daniel Imms 已提交
214 215 216 217
			queue.onFinished(() => {
				queue.dispose();
				delete this.ioOperationQueues[key];
			});
218
			this.ioOperationQueues[key] = queue;
D
Daniel Imms 已提交
219 220 221 222
		}
		return this.ioOperationQueues[key];
	}

223
	public discardAllWorkspaceBackups(): TPromise<void> {
224 225
		this.isShuttingDown = true;

B
Benjamin Pasero 已提交
226
		return this.ready.then(model => {
B
Benjamin Pasero 已提交
227
			if (!this.backupEnabled) {
B
Benjamin Pasero 已提交
228
				return void 0;
B
Benjamin Pasero 已提交
229
			}
B
Benjamin Pasero 已提交
230

231
			return pfs.del(this.backupWorkspacePath).then(() => model.clear());
B
Benjamin Pasero 已提交
232 233 234
		});
	}

235
	public getWorkspaceFileBackups(): TPromise<Uri[]> {
236
		return this.ready.then(model => {
B
Benjamin Pasero 已提交
237 238
			const readPromises: TPromise<Uri>[] = [];

239
			model.get().forEach(fileBackup => {
K
katainaka0503 已提交
240 241 242 243
				readPromises.push(
					readToMatchingString(fileBackup.fsPath, BackupFileService.META_MARKER, 2000, 10000)
						.then(Uri.parse)
				);
244
			});
B
Benjamin Pasero 已提交
245

246 247 248 249
			return TPromise.join(readPromises);
		});
	}

A
Alex Dima 已提交
250 251 252
	public parseBackupContent(rawTextSource: IRawTextSource): string {
		const textSource = TextSource.fromRawTextSource(rawTextSource, DefaultEndOfLine.LF);
		return textSource.lines.slice(1).join(textSource.EOL); // The first line of a backup text file is the file name
253 254
	}

255
	protected getBackupResource(resource: Uri, legacyMacWindowsFormat?: boolean): Uri {
B
Benjamin Pasero 已提交
256
		if (!this.backupEnabled) {
B
Benjamin Pasero 已提交
257
			return null;
258 259
		}

260
		return Uri.file(path.join(this.backupWorkspacePath, resource.scheme, this.hashPath(resource, legacyMacWindowsFormat)));
D
Daniel Imms 已提交
261
	}
262

263 264 265 266
	private hashPath(resource: Uri, legacyMacWindowsFormat?: boolean): string {
		const caseAwarePath = legacyMacWindowsFormat ? resource.fsPath.toLowerCase() : resource.fsPath;

		return crypto.createHash('md5').update(caseAwarePath).digest('hex');
267
	}
268
}