backupMainService.ts 6.0 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 7
import * as fs from 'fs';
import * as path from 'path';
8
import * as crypto from 'crypto';
9
import * as platform from 'vs/base/common/platform';
10
import * as extfs from 'vs/base/node/extfs';
11
import Uri from 'vs/base/common/uri';
D
Daniel Imms 已提交
12
import { IBackupWorkspacesFormat, IBackupMainService } from 'vs/platform/backup/common/backup';
13
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
14
import { TPromise } from 'vs/base/common/winjs.base';
15

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

	public _serviceBrand: any;

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

23
	private backups: IBackupWorkspacesFormat;
24

25 26
	private mapWindowToBackupFolder: { [windowId: number]: string; };

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

34
		this.loadSync();
35 36
	}

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

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

45 46 47 48 49 50 51 52
	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]));
	}

53
	public registerWindowForBackups(windowId: number, isEmptyWorkspace: boolean, backupFolder?: string, workspacePath?: string): void {
54 55 56 57 58 59 60 61 62 63
		// Backups and hot exit are disabled during extension development
		if (this.environmentService.isExtensionDevelopment) {
			return;
		}

		// Generate a new folder if this is a new empty workspace
		if (!backupFolder) {
			backupFolder = Date.now().toString();
		}

64
		if (workspacePath) {
65
			const caseAwarePath = this.sanitizePath(workspacePath);
66 67 68
			backupFolder = crypto.createHash('md5').update(caseAwarePath).digest('hex');
		}

D
Daniel Imms 已提交
69
		this.mapWindowToBackupFolder[windowId] = backupFolder;
70 71

		if (isEmptyWorkspace) {
D
Daniel Imms 已提交
72 73 74 75
			if (this.backups.emptyWorkspaces.indexOf(backupFolder) === -1) {
				this.backups.emptyWorkspaces.push(backupFolder);
				this.saveSync();
			}
76
		} else {
D
Daniel Imms 已提交
77 78 79 80
			const sanitizedPath = this.sanitizePath(workspacePath);
			if (this.backups.folderWorkspaces.indexOf(sanitizedPath) === -1) {
				this.backups.folderWorkspaces.push(sanitizedPath);
				this.saveSync();
81
			}
82 83 84
		}
	}

85 86
	protected removeWorkspaceBackupPathSync(workspace: Uri): void {
		if (!this.backups.folderWorkspaces) {
87 88
			return;
		}
89
		const index = this.backups.folderWorkspaces.indexOf(workspace.fsPath);
90 91 92
		if (index === -1) {
			return;
		}
93
		this.backups.folderWorkspaces.splice(index, 1);
94 95 96
		this.saveSync();
	}

97
	// TODO: Test
98
	private removeEmptyWorkspaceBackupFolder(backupFolder: string): void {
99 100 101
		if (!this.backups.emptyWorkspaces) {
			return;
		}
D
Daniel Imms 已提交
102
		const index = this.backups.emptyWorkspaces.indexOf(backupFolder);
103 104 105 106 107 108 109
		if (index === -1) {
			return;
		}
		this.backups.emptyWorkspaces.splice(index, 1);
		this.saveSync();
	}

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

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

128 129 130 131 132 133 134 135
		// 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 = [];
136 137 138 139 140 141 142 143 144
		}

		this.backups = backups;

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

	private validateBackupWorkspaces(backups: IBackupWorkspacesFormat): void {
145
		backups.folderWorkspaces.forEach(workspacePath => {
146
			const backupPath = path.join(this.backupHome, this.getWorkspaceHash(workspacePath));
147
			if (!this.hasBackupsSync(backupPath)) {
148 149 150
				extfs.delSync(backupPath);
				const backupWorkspace = Uri.file(this.sanitizePath(workspacePath));
				this.removeWorkspaceBackupPathSync(backupWorkspace);
151 152
			}
		});
153 154 155

		backups.emptyWorkspaces.forEach(backupFolder => {
			const backupPath = path.join(this.backupHome, backupFolder);
156
			if (!this.hasBackupsSync(backupPath)) {
157 158
				extfs.delSync(backupPath);
				this.removeEmptyWorkspaceBackupFolder(backupFolder);
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
			}
		});
	}

	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
179 180 181 182 183 184 185 186 187
		}
	}

	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);
			}
188
			fs.writeFileSync(this.workspacesJsonPath, JSON.stringify(this.backups));
189
		} catch (ex) {
190
			console.error('Could not save workspaces.json', ex);
191 192
		}
	}
193

D
Daniel Imms 已提交
194 195 196 197
	private sanitizePath(p) {
		return platform.isLinux ? p : p.toLowerCase();
	}

198 199
	private getWorkspaceHash(workspacePath: string): string {
		return crypto.createHash('md5').update(this.sanitizePath(workspacePath)).digest('hex');
200
	}
201
}