backupMainService.ts 11.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 arrays from 'vs/base/common/arrays';
7 8
import * as fs from 'fs';
import * as path from 'path';
9
import * as crypto from 'crypto';
10
import * as platform from 'vs/base/common/platform';
11
import * as extfs from 'vs/base/node/extfs';
D
Daniel Imms 已提交
12
import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/common/backup';
13
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
14 15
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
B
Benjamin Pasero 已提交
16
import { ILogService } from 'vs/platform/log/common/log';
B
Benjamin Pasero 已提交
17
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
18

D
Daniel Imms 已提交
19
export class BackupMainService implements IBackupMainService {
20 21 22 23

	public _serviceBrand: any;

	protected backupHome: string;
D
Daniel Imms 已提交
24
	protected workspacesJsonPath: string;
25

26
	protected backups: IBackupWorkspacesFormat;
27

28
	constructor(
29
		@IEnvironmentService environmentService: IEnvironmentService,
B
Benjamin Pasero 已提交
30
		@IConfigurationService private configurationService: IConfigurationService,
B
Benjamin Pasero 已提交
31
		@ILogService private logService: ILogService
32 33
	) {
		this.backupHome = environmentService.backupHome;
D
Daniel Imms 已提交
34
		this.workspacesJsonPath = environmentService.backupWorkspacesPath;
D
Daniel Imms 已提交
35

36
		this.loadSync();
37 38
	}

39
	public getWorkspaceBackups(): IWorkspaceIdentifier[] {
40 41 42 43 44
		if (this.isHotExitOnExitAndWindowClose()) {
			// Only non-folder windows are restored on main process launch when
			// hot exit is configured as onExitAndWindowClose.
			return [];
		}
45

46 47 48 49 50
		return this.backups.rootWorkspaces.slice(0); // return a copy
	}

	public getFolderBackupPaths(): string[] {
		if (this.isHotExitOnExitAndWindowClose()) {
51
			// Only non-folder windows are restored on main process launch when
D
Daniel Imms 已提交
52
			// hot exit is configured as onExitAndWindowClose.
53 54
			return [];
		}
55

56
		return this.backups.folderWorkspaces.slice(0); // return a copy
57 58
	}

59 60 61 62
	public isHotExitEnabled(): boolean {
		return this.getHotExitConfig() !== HotExitConfiguration.OFF;
	}

63
	private isHotExitOnExitAndWindowClose(): boolean {
64 65 66 67
		return this.getHotExitConfig() === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE;
	}

	private getHotExitConfig(): string {
68
		const config = this.configurationService.getValue<IFilesConfiguration>();
69

70
		return (config && config.files && config.files.hotExit) || HotExitConfiguration.ON_EXIT;
71 72
	}

73
	public getEmptyWindowBackupPaths(): string[] {
74
		return this.backups.emptyWorkspaces.slice(0); // return a copy
75 76
	}

77
	public registerWorkspaceBackupSync(workspace: IWorkspaceIdentifier, migrateFrom?: string): string {
78
		this.pushBackupPathsSync(workspace, this.backups.rootWorkspaces);
79

80 81 82 83 84 85 86 87 88 89
		const backupPath = path.join(this.backupHome, workspace.id);

		if (migrateFrom) {
			this.moveBackupFolderSync(backupPath, migrateFrom);
		}

		return backupPath;
	}

	private moveBackupFolderSync(backupPath: string, moveFromPath: string): void {
90 91 92 93

		// Target exists: make sure to convert existing backups to empty window backups
		if (fs.existsSync(backupPath)) {
			this.convertToEmptyWindowBackup(backupPath);
94 95
		}

96 97 98 99 100 101 102
		// When we have data to migrate from, move it over to the target location
		if (fs.existsSync(moveFromPath)) {
			try {
				fs.renameSync(moveFromPath, backupPath);
			} catch (ex) {
				this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`);
			}
103
		}
104 105
	}

B
Benjamin Pasero 已提交
106
	public registerFolderBackupSync(folderPath: string): string {
107
		this.pushBackupPathsSync(folderPath, this.backups.folderWorkspaces);
B
Benjamin Pasero 已提交
108

109
		return path.join(this.backupHome, this.getFolderHash(folderPath));
B
Benjamin Pasero 已提交
110 111 112
	}

	public registerEmptyWindowBackupSync(backupFolder?: string): string {
113 114

		// Generate a new folder if this is a new empty workspace
B
Benjamin Pasero 已提交
115
		if (!backupFolder) {
116
			backupFolder = this.getRandomEmptyWindowId();
117 118
		}

119
		this.pushBackupPathsSync(backupFolder, this.backups.emptyWorkspaces);
120

B
Benjamin Pasero 已提交
121
		return path.join(this.backupHome, backupFolder);
122
	}
123

124
	private pushBackupPathsSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void {
125 126
		if (this.indexOf(workspaceIdentifier, target) === -1) {
			target.push(workspaceIdentifier);
127
			this.saveSync();
128 129 130
		}
	}

131
	protected removeBackupPathSync(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): void {
132
		if (!target) {
133 134
			return;
		}
135

136
		const index = this.indexOf(workspaceIdentifier, target);
137 138 139
		if (index === -1) {
			return;
		}
140

141
		target.splice(index, 1);
142 143 144
		this.saveSync();
	}

145
	private indexOf(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[]): number {
146
		if (!target) {
147 148 149
			return -1;
		}

150 151 152 153 154
		const sanitizedWorkspaceIdentifier = this.sanitizeId(workspaceIdentifier);

		return arrays.firstIndex(target, id => this.sanitizeId(id) === sanitizedWorkspaceIdentifier);
	}

155
	private sanitizeId(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string {
156
		if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
157 158
			return this.sanitizePath(workspaceIdentifier);
		}
159

160
		return workspaceIdentifier.id;
161 162
	}

163
	protected loadSync(): void {
164
		let backups: IBackupWorkspacesFormat;
D
Daniel Imms 已提交
165
		try {
166
			backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
D
Daniel Imms 已提交
167
		} catch (error) {
168
			backups = Object.create(null);
D
Daniel Imms 已提交
169 170
		}

171
		// Ensure rootWorkspaces is a object[]
172 173
		if (backups.rootWorkspaces) {
			const rws = backups.rootWorkspaces;
174
			if (!Array.isArray(rws) || rws.some(r => typeof r !== 'object')) {
175 176 177 178 179 180
				backups.rootWorkspaces = [];
			}
		} else {
			backups.rootWorkspaces = [];
		}

D
Daniel Imms 已提交
181
		// Ensure folderWorkspaces is a string[]
182 183
		if (backups.folderWorkspaces) {
			const fws = backups.folderWorkspaces;
D
Daniel Imms 已提交
184
			if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
185
				backups.folderWorkspaces = [];
D
Daniel Imms 已提交
186
			}
187 188
		} else {
			backups.folderWorkspaces = [];
189 190
		}

191 192 193 194 195 196 197 198
		// Ensure emptyWorkspaces is a string[]
		if (backups.emptyWorkspaces) {
			const fws = backups.emptyWorkspaces;
			if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
				backups.emptyWorkspaces = [];
			}
		} else {
			backups.emptyWorkspaces = [];
199 200
		}

201
		this.backups = this.dedupeBackups(backups);
202 203 204 205 206

		// Validate backup workspaces
		this.validateBackupWorkspaces(backups);
	}

207 208 209
	protected dedupeBackups(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat {

		// De-duplicate folder/workspace backups. don't worry about cleaning them up any duplicates as
210 211
		// they will be removed when there are no backups.
		backups.folderWorkspaces = arrays.distinct(backups.folderWorkspaces, ws => this.sanitizePath(ws));
212
		backups.rootWorkspaces = arrays.distinct(backups.rootWorkspaces, ws => this.sanitizePath(ws.id));
213 214

		return backups;
D
Daniel Imms 已提交
215 216 217
	}

	private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void {
218
		const staleBackupWorkspaces: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; backupPath: string; target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = [];
219

220
		const workspaceAndFolders: { workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, target: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier)[] }[] = [];
221 222
		workspaceAndFolders.push(...backups.rootWorkspaces.map(r => ({ workspaceIdentifier: r, target: backups.rootWorkspaces })));
		workspaceAndFolders.push(...backups.folderWorkspaces.map(f => ({ workspaceIdentifier: f, target: backups.folderWorkspaces })));
D
Daniel Imms 已提交
223

224 225
		// Validate Workspace and Folder Backups
		workspaceAndFolders.forEach(workspaceOrFolder => {
226
			const workspaceId = workspaceOrFolder.workspaceIdentifier;
227 228
			const workspacePath = isSingleFolderWorkspaceIdentifier(workspaceId) ? workspaceId : workspaceId.configPath;
			const backupPath = path.join(this.backupHome, isSingleFolderWorkspaceIdentifier(workspaceId) ? this.getFolderHash(workspaceId) : workspaceId.id);
229
			const hasBackups = this.hasBackupsSync(backupPath);
230 231
			const missingWorkspace = hasBackups && !fs.existsSync(workspacePath);

232 233
			// If the workspace/folder has no backups, make sure to delete it
			// If the workspace/folder has backups, but the target workspace is missing, convert backups to empty ones
234
			if (!hasBackups || missingWorkspace) {
235
				staleBackupWorkspaces.push({ workspaceIdentifier: workspaceId, backupPath, target: workspaceOrFolder.target });
236 237

				if (missingWorkspace) {
238
					this.convertToEmptyWindowBackup(backupPath);
239
				}
240 241
			}
		});
242

243
		// Validate Empty Windows
244 245
		backups.emptyWorkspaces.forEach(backupFolder => {
			const backupPath = path.join(this.backupHome, backupFolder);
246
			if (!this.hasBackupsSync(backupPath)) {
247
				staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, target: backups.emptyWorkspaces });
D
Daniel Imms 已提交
248 249 250
			}
		});

251
		// Clean up stale backups
D
Daniel Imms 已提交
252
		staleBackupWorkspaces.forEach(staleBackupWorkspace => {
253
			const { backupPath, workspaceIdentifier, target } = staleBackupWorkspace;
254 255 256 257

			try {
				extfs.delSync(backupPath);
			} catch (ex) {
B
Benjamin Pasero 已提交
258
				this.logService.error(`Backup: Could not delete stale backup: ${ex.toString()}`);
259 260
			}

261
			this.removeBackupPathSync(workspaceIdentifier, target);
262 263 264
		});
	}

265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
	private convertToEmptyWindowBackup(backupPath: string): boolean {

		// New empty window backup
		const identifier = this.getRandomEmptyWindowId();
		this.pushBackupPathsSync(identifier, this.backups.emptyWorkspaces);

		// Rename backupPath to new empty window backup path
		const newEmptyWindowBackupPath = path.join(this.backupHome, identifier);
		try {
			fs.renameSync(backupPath, newEmptyWindowBackupPath);
		} catch (ex) {
			this.logService.error(`Backup: Could not rename backup folder: ${ex.toString()}`);

			this.removeBackupPathSync(identifier, this.backups.emptyWorkspaces);

			return false;
		}

		return true;
	}

B
Benjamin Pasero 已提交
286
	private hasBackupsSync(backupPath: string): boolean {
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
		try {
			const backupSchemas = extfs.readdirSync(backupPath);
			if (backupSchemas.length === 0) {
				return false; // empty backups
			}

			return backupSchemas.some(backupSchema => {
				try {
					return extfs.readdirSync(path.join(backupPath, backupSchema)).length > 0;
				} catch (error) {
					return false; // invalid folder
				}
			});
		} catch (error) {
			return false; // backup path does not exist
302 303 304 305 306 307 308 309 310
		}
	}

	private saveSync(): void {
		try {
			// The user data directory must exist so only the Backup directory needs to be checked.
			if (!fs.existsSync(this.backupHome)) {
				fs.mkdirSync(this.backupHome);
			}
311

312
			extfs.writeFileAndFlushSync(this.workspacesJsonPath, JSON.stringify(this.backups));
313
		} catch (ex) {
B
Benjamin Pasero 已提交
314
			this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
315 316
		}
	}
317

318
	private getRandomEmptyWindowId(): string {
B
Benjamin Pasero 已提交
319 320 321
		return (Date.now() + Math.round(Math.random() * 1000)).toString();
	}

B
Benjamin Pasero 已提交
322
	private sanitizePath(p: string): string {
D
Daniel Imms 已提交
323 324 325
		return platform.isLinux ? p : p.toLowerCase();
	}

326 327
	protected getFolderHash(folderPath: string): string {
		return crypto.createHash('md5').update(this.sanitizePath(folderPath)).digest('hex');
328
	}
329
}