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
	}

B
Benjamin Pasero 已提交
52 53 54 55 56 57 58
	public registerWorkspaceBackupSync(workspacePath: string): string {
		this.pushBackupPathsSync(workspacePath);

		return path.join(this.backupHome, this.getWorkspaceHash(workspacePath));
	}

	public registerEmptyWindowBackupSync(backupFolder?: string): string {
59 60

		// Generate a new folder if this is a new empty workspace
B
Benjamin Pasero 已提交
61
		if (!backupFolder) {
62
			backupFolder = this.getRandomEmptyWindowId();
63 64
		}

B
Benjamin Pasero 已提交
65
		this.pushBackupPathsSync(backupFolder, true);
66

B
Benjamin Pasero 已提交
67
		return path.join(this.backupHome, backupFolder);
68
	}
69

B
Benjamin Pasero 已提交
70
	private pushBackupPathsSync(workspaceIdentifier: string, isEmptyWindow?: boolean): string {
71 72
		const array = isEmptyWindow ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces;
		if (this.indexOf(workspaceIdentifier, isEmptyWindow) === -1) {
73 74
			array.push(workspaceIdentifier);
			this.saveSync();
75
		}
76 77

		return workspaceIdentifier;
78 79
	}

80 81
	protected removeBackupPathSync(workspaceIdentifier: string, isEmptyWindow: boolean): void {
		const array = isEmptyWindow ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces;
82
		if (!array) {
83 84
			return;
		}
85
		const index = this.indexOf(workspaceIdentifier, isEmptyWindow);
86 87 88
		if (index === -1) {
			return;
		}
89
		array.splice(index, 1);
90 91 92
		this.saveSync();
	}

93 94
	private indexOf(workspaceIdentifier: string, isEmptyWindow: boolean): number {
		const array = isEmptyWindow ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces;
95 96 97 98
		if (!array) {
			return -1;
		}

99
		if (isEmptyWindow) {
100 101 102 103 104 105 106 107
			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);
	}

108
	protected loadSync(): void {
109
		let backups: IBackupWorkspacesFormat;
D
Daniel Imms 已提交
110
		try {
111
			backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
D
Daniel Imms 已提交
112
		} catch (error) {
113
			backups = Object.create(null);
D
Daniel Imms 已提交
114 115 116
		}

		// Ensure folderWorkspaces is a string[]
117 118
		if (backups.folderWorkspaces) {
			const fws = backups.folderWorkspaces;
D
Daniel Imms 已提交
119
			if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
120
				backups.folderWorkspaces = [];
D
Daniel Imms 已提交
121
			}
122 123
		} else {
			backups.folderWorkspaces = [];
124 125
		}

126 127 128 129 130 131 132 133
		// 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 = [];
134 135
		}

136
		this.backups = this.dedupeFolderWorkspaces(backups);
137 138 139 140 141

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

142
	protected dedupeFolderWorkspaces(backups: IBackupWorkspacesFormat): IBackupWorkspacesFormat {
143 144 145
		// 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));
146 147

		return backups;
D
Daniel Imms 已提交
148 149 150
	}

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

153
		// Validate Folder Workspaces
154
		backups.folderWorkspaces.forEach(workspacePath => {
155
			const backupPath = path.join(this.backupHome, this.getWorkspaceHash(workspacePath));
156 157 158 159 160 161
			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) {
162
				staleBackupWorkspaces.push({ workspaceIdentifier: workspacePath, backupPath, isEmptyWindow: false });
163 164

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

						this.removeBackupPathSync(identifier, true);
					}
				}
175 176
			}
		});
177

178
		// Validate Empty Windows
179 180
		backups.emptyWorkspaces.forEach(backupFolder => {
			const backupPath = path.join(this.backupHome, backupFolder);
181
			if (!this.hasBackupsSync(backupPath)) {
182
				staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, isEmptyWindow: true });
D
Daniel Imms 已提交
183 184 185
			}
		});

186
		// Clean up stale backups
D
Daniel Imms 已提交
187
		staleBackupWorkspaces.forEach(staleBackupWorkspace => {
188
			const { backupPath, workspaceIdentifier, isEmptyWindow } = staleBackupWorkspace;
189 190 191 192

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

196
			this.removeBackupPathSync(workspaceIdentifier, isEmptyWindow);
197 198 199
		});
	}

B
Benjamin Pasero 已提交
200
	private hasBackupsSync(backupPath: string): boolean {
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
		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
216 217 218 219 220 221 222 223 224
		}
	}

	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);
			}
225
			fs.writeFileSync(this.workspacesJsonPath, JSON.stringify(this.backups));
226
		} catch (ex) {
B
Benjamin Pasero 已提交
227
			this.logService.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
228 229
		}
	}
230

231
	private getRandomEmptyWindowId(): string {
B
Benjamin Pasero 已提交
232 233 234
		return (Date.now() + Math.round(Math.random() * 1000)).toString();
	}

B
Benjamin Pasero 已提交
235
	private sanitizePath(p: string): string {
D
Daniel Imms 已提交
236 237 238
		return platform.isLinux ? p : p.toLowerCase();
	}

D
Daniel Imms 已提交
239
	protected getWorkspaceHash(workspacePath: string): string {
240
		return crypto.createHash('md5').update(this.sanitizePath(workspacePath)).digest('hex');
241
	}
242
}