backupMainService.ts 8.3 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";
17

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

	public _serviceBrand: any;

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

25
	private backups: IBackupWorkspacesFormat;
26

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

35
		this.loadSync();
36 37
	}

38
	public getWorkspaceBackupPaths(): string[] {
39
		const config = this.configurationService.getConfiguration<IFilesConfiguration>();
D
Daniel Imms 已提交
40
		if (config && config.files && config.files.hotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
41
			// Only non-folder windows are restored on main process launch when
D
Daniel Imms 已提交
42
			// hot exit is configured as onExitAndWindowClose.
43 44
			return [];
		}
45
		return this.backups.folderWorkspaces.slice(0); // return a copy
46 47
	}

48
	public getEmptyWindowBackupPaths(): string[] {
49
		return this.backups.emptyWorkspaces.slice(0); // return a copy
50 51
	}

52
	public registerWindowForBackupsSync(windowId: number, isEmptyWindow: boolean, backupFolder?: string, workspacePath?: string): string {
53 54

		// Generate a new folder if this is a new empty workspace
55 56
		if (isEmptyWindow && !backupFolder) {
			backupFolder = this.getRandomEmptyWindowId();
57 58
		}

59
		this.pushBackupPathsSync(isEmptyWindow ? backupFolder : workspacePath, isEmptyWindow);
60

61
		return path.join(this.backupHome, isEmptyWindow ? backupFolder : this.getWorkspaceHash(workspacePath));
62
	}
63

64 65 66
	private pushBackupPathsSync(workspaceIdentifier: string, isEmptyWindow: boolean): string {
		const array = isEmptyWindow ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces;
		if (this.indexOf(workspaceIdentifier, isEmptyWindow) === -1) {
67 68
			array.push(workspaceIdentifier);
			this.saveSync();
69
		}
70 71

		return workspaceIdentifier;
72 73
	}

74 75
	protected removeBackupPathSync(workspaceIdentifier: string, isEmptyWindow: boolean): void {
		const array = isEmptyWindow ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces;
76
		if (!array) {
77 78
			return;
		}
79
		const index = this.indexOf(workspaceIdentifier, isEmptyWindow);
80 81 82
		if (index === -1) {
			return;
		}
83
		array.splice(index, 1);
84 85 86
		this.saveSync();
	}

87 88
	private indexOf(workspaceIdentifier: string, isEmptyWindow: boolean): number {
		const array = isEmptyWindow ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces;
89 90 91 92
		if (!array) {
			return -1;
		}

93
		if (isEmptyWindow) {
94 95 96 97 98 99 100 101
			return array.indexOf(workspaceIdentifier);
		}

		// for backup workspaces, sanitize the workspace identifier to accomodate for case insensitive file systems
		const sanitizedWorkspaceIdentifier = this.sanitizePath(workspaceIdentifier);
		return arrays.firstIndex(array, id => this.sanitizePath(id) === sanitizedWorkspaceIdentifier);
	}

102
	protected loadSync(): void {
103
		let backups: IBackupWorkspacesFormat;
D
Daniel Imms 已提交
104
		try {
105
			backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
D
Daniel Imms 已提交
106
		} catch (error) {
107
			backups = Object.create(null);
D
Daniel Imms 已提交
108 109 110
		}

		// Ensure folderWorkspaces is a string[]
111 112
		if (backups.folderWorkspaces) {
			const fws = backups.folderWorkspaces;
D
Daniel Imms 已提交
113
			if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
114
				backups.folderWorkspaces = [];
D
Daniel Imms 已提交
115
			}
116 117
		} else {
			backups.folderWorkspaces = [];
118 119
		}

120 121 122 123 124 125 126 127
		// 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 = [];
128 129
		}

130
		this.backups = this.dedupeFolderWorkspaces(backups);
131 132 133 134 135

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

136
	protected dedupeFolderWorkspaces(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat {
137 138 139
		// De-duplicate folder workspaces, don't worry about cleaning them up any duplicates as
		// they will be removed when there are no backups.
		backups.folderWorkspaces = arrays.distinct(backups.folderWorkspaces, ws => this.sanitizePath(ws));
140 141

		return backups;
D
Daniel Imms 已提交
142 143 144
	}

	private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void {
145
		const staleBackupWorkspaces: { workspaceIdentifier: string; backupPath: string; isEmptyWindow: boolean }[] = [];
D
Daniel Imms 已提交
146

147
		// Validate Folder Workspaces
148
		backups.folderWorkspaces.forEach(workspacePath => {
149
			const backupPath = path.join(this.backupHome, this.getWorkspaceHash(workspacePath));
150 151 152 153 154 155
			const hasBackups = this.hasBackupsSync(backupPath);
			const missingWorkspace = hasBackups && !fs.existsSync(workspacePath);

			// If the folder has no backups, make sure to delete it
			// If the folder has backups, but the target workspace is missing, convert backups to empty ones
			if (!hasBackups || missingWorkspace) {
156
				staleBackupWorkspaces.push({ workspaceIdentifier: workspacePath, backupPath, isEmptyWindow: false });
157 158

				if (missingWorkspace) {
159 160
					const identifier = this.pushBackupPathsSync(this.getRandomEmptyWindowId(), true /* is empty workspace */);
					const newEmptyWindowBackupPath = path.join(path.dirname(backupPath), identifier);
161
					try {
162
						fs.renameSync(backupPath, newEmptyWindowBackupPath);
163
					} catch (ex) {
B
Benjamin Pasero 已提交
164
						this.logService.error(`Backup: Could not rename backup folder for missing workspace: ${ex.toString()}`);
165 166 167 168

						this.removeBackupPathSync(identifier, true);
					}
				}
169 170
			}
		});
171

172
		// Validate Empty Windows
173 174
		backups.emptyWorkspaces.forEach(backupFolder => {
			const backupPath = path.join(this.backupHome, backupFolder);
175
			if (!this.hasBackupsSync(backupPath)) {
176
				staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, isEmptyWindow: true });
D
Daniel Imms 已提交
177 178 179
			}
		});

180
		// Clean up stale backups
D
Daniel Imms 已提交
181
		staleBackupWorkspaces.forEach(staleBackupWorkspace => {
182
			const { backupPath, workspaceIdentifier, isEmptyWindow } = staleBackupWorkspace;
183 184 185 186

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

190
			this.removeBackupPathSync(workspaceIdentifier, isEmptyWindow);
191 192 193
		});
	}

B
Benjamin Pasero 已提交
194
	private hasBackupsSync(backupPath: string): boolean {
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
		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
210 211 212 213 214 215 216 217 218
		}
	}

	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);
			}
219
			fs.writeFileSync(this.workspacesJsonPath, JSON.stringify(this.backups));
220
		} catch (ex) {
B
Benjamin Pasero 已提交
221
			this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
222 223
		}
	}
224

225
	private getRandomEmptyWindowId(): string {
B
Benjamin Pasero 已提交
226 227 228
		return (Date.now() + Math.round(Math.random() * 1000)).toString();
	}

B
Benjamin Pasero 已提交
229
	private sanitizePath(p: string): string {
D
Daniel Imms 已提交
230 231 232
		return platform.isLinux ? p : p.toLowerCase();
	}

D
Daniel Imms 已提交
233
	protected getWorkspaceHash(workspacePath: string): string {
234
		return crypto.createHash('md5').update(this.sanitizePath(workspacePath)).digest('hex');
235
	}
236
}