backupMainService.ts 7.5 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';
12
import Uri from 'vs/base/common/uri';
D
Daniel Imms 已提交
13
import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/common/backup';
14
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
15
import { TPromise } from 'vs/base/common/winjs.base';
16

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

	public _serviceBrand: any;

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

24
	private backups: IBackupWorkspacesFormat;
25

26
	protected mapWindowToBackupFolder: { [windowId: number]: string; };
27

28
	constructor(
29
		@IEnvironmentService private environmentService: IEnvironmentService
30 31
	) {
		this.backupHome = environmentService.backupHome;
D
Daniel Imms 已提交
32
		this.workspacesJsonPath = environmentService.backupWorkspacesPath;
33
		this.mapWindowToBackupFolder = Object.create(null);
D
Daniel Imms 已提交
34

35
		this.loadSync();
36 37
	}

38 39 40 41 42 43 44 45
	public get workspaceBackupPaths(): string[] {
		return this.backups.folderWorkspaces;
	}

	public get emptyWorkspaceBackupPaths(): string[] {
		return this.backups.emptyWorkspaces;
	}

46 47 48 49 50 51 52 53
	public getBackupPath(windowId: number): TPromise<string> {
		if (!this.mapWindowToBackupFolder[windowId]) {
			throw new Error(`Unknown backup workspace for window ${windowId}`);
		}

		return TPromise.as(path.join(this.backupHome, this.mapWindowToBackupFolder[windowId]));
	}

54
	public registerWindowForBackups(windowId: number, isEmptyWorkspace: boolean, backupFolder?: string, workspacePath?: string): void {
55
		// Generate a new folder if this is a new empty workspace
D
Daniel Imms 已提交
56
		if (isEmptyWorkspace && !backupFolder) {
57
			backupFolder = this.getRandomEmptyWorkspaceId();
58 59
		}

60
		this.mapWindowToBackupFolder[windowId] = isEmptyWorkspace ? backupFolder : this.getWorkspaceHash(workspacePath);
D
Daniel Imms 已提交
61
		this.pushBackupPathsSync(isEmptyWorkspace ? backupFolder : workspacePath, isEmptyWorkspace);
62
	}
63

64 65 66 67
	private getRandomEmptyWorkspaceId(): string {
		return (Date.now() + Math.round(Math.random() * 1000)).toString();
	}

68
	protected pushBackupPathsSync(workspaceIdentifier: string, isEmptyWorkspace: boolean): string {
69 70 71 72 73 74 75
		if (!isEmptyWorkspace) {
			workspaceIdentifier = this.sanitizePath(workspaceIdentifier);
		}
		const array = isEmptyWorkspace ? this.backups.emptyWorkspaces : this.backups.folderWorkspaces;
		if (array.indexOf(workspaceIdentifier) === -1) {
			array.push(workspaceIdentifier);
			this.saveSync();
76
		}
77 78

		return workspaceIdentifier;
79 80
	}

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

94
	protected loadSync(): void {
95
		let backups: IBackupWorkspacesFormat;
D
Daniel Imms 已提交
96
		try {
97
			backups = JSON.parse(fs.readFileSync(this.workspacesJsonPath, 'utf8').toString()); // invalid JSON or permission issue can happen here
D
Daniel Imms 已提交
98
		} catch (error) {
99
			backups = Object.create(null);
D
Daniel Imms 已提交
100 101 102
		}

		// Ensure folderWorkspaces is a string[]
103 104
		if (backups.folderWorkspaces) {
			const fws = backups.folderWorkspaces;
D
Daniel Imms 已提交
105
			if (!Array.isArray(fws) || fws.some(f => typeof f !== 'string')) {
106
				backups.folderWorkspaces = [];
D
Daniel Imms 已提交
107
			}
108 109
		} else {
			backups.folderWorkspaces = [];
110 111
		}

112 113 114 115 116 117 118 119
		// 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 = [];
120 121 122 123 124 125 126 127
		}

		this.backups = backups;

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

D
Daniel Imms 已提交
128
	protected sanitizeFolderWorkspaces(backups: IBackupWorkspacesFormat): void {
129 130 131
		// Merge duplicates for folder workspaces, don't worry about cleaning them up as they will
		// be removed when there are no backups.
		backups.folderWorkspaces = arrays.distinct(backups.folderWorkspaces.map(w => this.sanitizePath(w)));
D
Daniel Imms 已提交
132 133 134 135 136 137
	}

	private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void {
		const staleBackupWorkspaces: { workspaceIdentifier: string; backupPath: string; isEmptyWorkspace: boolean }[] = [];

		this.sanitizeFolderWorkspaces(backups);
138

139
		backups.folderWorkspaces.forEach(workspacePath => {
140
			const backupPath = path.join(this.backupHome, this.getWorkspaceHash(workspacePath));
141 142 143 144 145 146
			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) {
D
Daniel Imms 已提交
147
				const backupWorkspace = this.sanitizePath(workspacePath);
148
				staleBackupWorkspaces.push({ workspaceIdentifier: Uri.file(backupWorkspace).fsPath, backupPath, isEmptyWorkspace: false });
149 150

				if (missingWorkspace) {
151
					const identifier = this.pushBackupPathsSync(this.getRandomEmptyWorkspaceId(), true /* is empty workspace */);
152 153 154 155 156 157 158 159 160
					const newEmptyWorkspaceBackupPath = path.join(path.dirname(backupPath), identifier);
					try {
						fs.renameSync(backupPath, newEmptyWorkspaceBackupPath);
					} catch (ex) {
						console.error(`Backup: Could not rename backup folder for missing workspace: ${ex.toString()}`);

						this.removeBackupPathSync(identifier, true);
					}
				}
161 162
			}
		});
163 164 165

		backups.emptyWorkspaces.forEach(backupFolder => {
			const backupPath = path.join(this.backupHome, backupFolder);
166
			if (!this.hasBackupsSync(backupPath)) {
D
Daniel Imms 已提交
167 168 169 170 171 172
				staleBackupWorkspaces.push({ workspaceIdentifier: backupFolder, backupPath, isEmptyWorkspace: true });
			}
		});

		staleBackupWorkspaces.forEach(staleBackupWorkspace => {
			const {backupPath, workspaceIdentifier, isEmptyWorkspace} = staleBackupWorkspace;
173 174 175 176 177 178 179

			try {
				extfs.delSync(backupPath);
			} catch (ex) {
				console.error(`Backup: Could not delete stale backup: ${ex.toString()}`);
			}

180
			this.removeBackupPathSync(workspaceIdentifier, isEmptyWorkspace);
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
		});
	}

	private hasBackupsSync(backupPath): boolean {
		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
200 201 202 203 204 205 206 207 208
		}
	}

	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);
			}
209
			fs.writeFileSync(this.workspacesJsonPath, JSON.stringify(this.backups));
210
		} catch (ex) {
211
			console.error(`Backup: Could not save workspaces.json: ${ex.toString()}`);
212 213
		}
	}
214

D
Daniel Imms 已提交
215 216 217 218
	private sanitizePath(p) {
		return platform.isLinux ? p : p.toLowerCase();
	}

D
Daniel Imms 已提交
219
	protected getWorkspaceHash(workspacePath: string): string {
220
		return crypto.createHash('md5').update(this.sanitizePath(workspacePath)).digest('hex');
221
	}
222
}