workspacesMainService.ts 9.2 KB
Newer Older
B
Benjamin Pasero 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8
import { IWorkspacesMainService, IWorkspaceIdentifier, IStoredWorkspace, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace } from 'vs/platform/workspaces/common/workspaces';
9
import { TPromise } from 'vs/base/common/winjs.base';
B
Benjamin Pasero 已提交
10 11
import { isParent } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
12
import { extname, join, dirname, isAbsolute, resolve, relative } from 'path';
B
Benjamin Pasero 已提交
13
import { mkdirp, writeFile, readFile } from 'vs/base/node/pfs';
14
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
B
Benjamin Pasero 已提交
15
import { isLinux } from 'vs/base/common/platform';
16
import { delSync, readdirSync } from 'vs/base/node/extfs';
B
Benjamin Pasero 已提交
17 18
import Event, { Emitter } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
19
import { isEqual, isEqualOrParent } from 'vs/base/common/paths';
B
Benjamin Pasero 已提交
20
import { coalesce } from 'vs/base/common/arrays';
21
import { createHash } from 'crypto';
22
import * as json from 'vs/base/common/json';
23

B
Benjamin Pasero 已提交
24 25 26 27
export class WorkspacesMainService implements IWorkspacesMainService {

	public _serviceBrand: any;

B
Benjamin Pasero 已提交
28
	protected workspacesHome: string;
B
Benjamin Pasero 已提交
29

30
	private _onWorkspaceSaved: Emitter<IWorkspaceSavedEvent>;
31
	private _onUntitledWorkspaceDeleted: Emitter<IWorkspaceIdentifier>;
32

33 34
	constructor(
		@IEnvironmentService private environmentService: IEnvironmentService,
35
		@ILogService private logService: ILogService
36
	) {
B
Benjamin Pasero 已提交
37
		this.workspacesHome = environmentService.workspacesHome;
38

39
		this._onWorkspaceSaved = new Emitter<IWorkspaceSavedEvent>();
40
		this._onUntitledWorkspaceDeleted = new Emitter<IWorkspaceIdentifier>();
41 42 43 44
	}

	public get onWorkspaceSaved(): Event<IWorkspaceSavedEvent> {
		return this._onWorkspaceSaved.event;
B
Benjamin Pasero 已提交
45 46
	}

47 48
	public get onUntitledWorkspaceDeleted(): Event<IWorkspaceIdentifier> {
		return this._onUntitledWorkspaceDeleted.event;
49 50
	}

B
Benjamin Pasero 已提交
51 52 53 54 55 56 57 58
	public resolveWorkspace(path: string): TPromise<IResolvedWorkspace> {
		if (!this.isWorkspacePath(path)) {
			return TPromise.as(null); // does not look like a valid workspace config file
		}

		return readFile(path).then(contents => this.doResolveWorkspace(path, contents.toString()));
	}

59
	public resolveWorkspaceSync(path: string): IResolvedWorkspace {
B
Benjamin Pasero 已提交
60
		if (!this.isWorkspacePath(path)) {
61 62 63
			return null; // does not look like a valid workspace config file
		}

B
Benjamin Pasero 已提交
64 65 66 67 68 69 70 71
		return this.doResolveWorkspace(path, readFileSync(path, 'utf8'));
	}

	private isWorkspacePath(path: string): boolean {
		return this.isInsideWorkspacesHome(path) || extname(path) === `.${WORKSPACE_EXTENSION}`;
	}

	private doResolveWorkspace(path: string, contents: string): IResolvedWorkspace {
72
		try {
73
			const workspace = this.doParseStoredWorkspace(path, contents);
74

75
			// relative paths get resolved against the workspace location
76
			workspace.folders.forEach(folder => {
77
				if (!isAbsolute(folder.path)) {
78
					folder.path = resolve(dirname(path), folder.path);
79 80 81 82 83 84
				}
			});

			return {
				id: this.getWorkspaceId(path),
				configPath: path,
85
				folders: workspace.folders
86
			};
87
		} catch (error) {
88 89 90 91 92 93 94
			this.logService.log(error.toString());
		}

		return null;
	}

	private doParseStoredWorkspace(path: string, contents: string): IStoredWorkspace {
95 96

		// Parse workspace file
97 98
		let storedWorkspace: IStoredWorkspace;
		try {
99
			storedWorkspace = json.parse(contents); // use fault tolerant parser
100 101 102
		} catch (error) {
			throw new Error(`${path} cannot be parsed as JSON file (${error}).`);
		}
103

104 105 106 107 108 109
		// Filter out folders which do not have a path set
		if (Array.isArray(storedWorkspace.folders)) {
			storedWorkspace.folders = storedWorkspace.folders.filter(folder => !!folder.path);
		}

		// Validate
110 111
		if (!Array.isArray(storedWorkspace.folders) || storedWorkspace.folders.length === 0) {
			throw new Error(`${path} looks like an invalid workspace file.`);
112
		}
113 114

		return storedWorkspace;
B
Benjamin Pasero 已提交
115 116
	}

117 118 119 120
	private isInsideWorkspacesHome(path: string): boolean {
		return isParent(path, this.environmentService.workspacesHome, !isLinux /* ignore case */);
	}

B
Benjamin Pasero 已提交
121
	public createWorkspace(folders: string[]): TPromise<IWorkspaceIdentifier> {
122 123 124 125 126 127 128 129 130 131 132 133
		const { workspace, configParent, storedWorkspace } = this.createUntitledWorkspace(folders);

		return mkdirp(configParent).then(() => {
			return writeFile(workspace.configPath, JSON.stringify(storedWorkspace, null, '\t')).then(() => workspace);
		});
	}

	public createWorkspaceSync(folders: string[]): IWorkspaceIdentifier {
		const { workspace, configParent, storedWorkspace } = this.createUntitledWorkspace(folders);

		if (!existsSync(this.workspacesHome)) {
			mkdirSync(this.workspacesHome);
B
Benjamin Pasero 已提交
134 135
		}

136 137 138 139 140 141 142 143
		mkdirSync(configParent);

		writeFileSync(workspace.configPath, JSON.stringify(storedWorkspace, null, '\t'));

		return workspace;
	}

	private createUntitledWorkspace(folders: string[]): { workspace: IWorkspaceIdentifier, configParent: string, storedWorkspace: IStoredWorkspace } {
144 145 146
		const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString();
		const untitledWorkspaceConfigFolder = join(this.workspacesHome, randomId);
		const untitledWorkspaceConfigPath = join(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME);
B
Benjamin Pasero 已提交
147

148 149 150 151 152
		const storedWorkspace: IStoredWorkspace = {
			folders: folders.map(folder => ({
				path: folder
			}))
		};
B
Benjamin Pasero 已提交
153

154 155
		return {
			workspace: {
156 157
				id: this.getWorkspaceId(untitledWorkspaceConfigPath),
				configPath: untitledWorkspaceConfigPath
158 159 160 161
			},
			configParent: untitledWorkspaceConfigFolder,
			storedWorkspace
		};
B
Benjamin Pasero 已提交
162 163
	}

164 165 166 167 168 169
	public getWorkspaceId(workspaceConfigPath: string): string {
		if (!isLinux) {
			workspaceConfigPath = workspaceConfigPath.toLowerCase(); // sanitize for platform file system
		}

		return createHash('md5').update(workspaceConfigPath).digest('hex');
B
Benjamin Pasero 已提交
170
	}
B
Benjamin Pasero 已提交
171 172 173 174

	public isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean {
		return this.isInsideWorkspacesHome(workspace.configPath);
	}
175

176
	public saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPath: string): TPromise<IWorkspaceIdentifier> {
B
Benjamin Pasero 已提交
177 178

		// Return early if target is same as source
179
		if (isEqual(workspace.configPath, targetConfigPath, !isLinux)) {
B
Benjamin Pasero 已提交
180 181 182
			return TPromise.as(workspace);
		}

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
		// Read the contents of the workspace file and resolve it
		return readFile(workspace.configPath).then(rawWorkspaceContents => {
			let storedWorkspace: IStoredWorkspace;
			try {
				storedWorkspace = this.doParseStoredWorkspace(workspace.configPath, rawWorkspaceContents.toString());
			} catch (error) {
				return TPromise.wrapError(error);
			}

			const sourceConfigFolder = dirname(workspace.configPath);
			const targetConfigFolder = dirname(targetConfigPath);

			// Rewrite absolute paths to relative paths if the target workspace folder
			// is a parent of the location of the workspace file itself. Otherwise keep
			// using absolute paths.
			storedWorkspace.folders.forEach(folder => {
199 200 201 202 203 204
				if (!isAbsolute(folder.path)) {
					folder.path = resolve(sourceConfigFolder, folder.path); // relative paths get resolved against the workspace location
				}

				if (isEqualOrParent(folder.path, targetConfigFolder, !isLinux)) {
					folder.path = relative(targetConfigFolder, folder.path) || '.'; // absolute paths get converted to relative ones to workspace location if possible
205 206
				}
			});
207

208 209
			return writeFile(targetConfigPath, JSON.stringify(storedWorkspace, null, '\t')).then(() => {
				const savedWorkspaceIdentifier = { id: this.getWorkspaceId(targetConfigPath), configPath: targetConfigPath };
210

211 212
				// Event
				this._onWorkspaceSaved.fire({ workspace: savedWorkspaceIdentifier, oldConfigPath: workspace.configPath });
213

214 215 216 217 218
				// Delete untitled workspace
				this.deleteUntitledWorkspaceSync(workspace);

				return savedWorkspaceIdentifier;
			});
219 220
		});
	}
221

222
	public deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
223
		if (!this.isUntitledWorkspace(workspace)) {
224
			return; // only supported for untitled workspaces
225 226
		}

227
		// Delete from disk
228
		this.doDeleteUntitledWorkspaceSync(workspace.configPath);
229 230

		// Event
231
		this._onUntitledWorkspaceDeleted.fire(workspace);
232
	}
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262

	private doDeleteUntitledWorkspaceSync(configPath: string): void {
		try {
			delSync(dirname(configPath));
		} catch (error) {
			this.logService.log(`Unable to delete untitled workspace ${configPath} (${error}).`);
		}
	}

	public getUntitledWorkspacesSync(): IWorkspaceIdentifier[] {
		let untitledWorkspacePaths: string[] = [];
		try {
			untitledWorkspacePaths = readdirSync(this.workspacesHome).map(folder => join(this.workspacesHome, folder, UNTITLED_WORKSPACE_NAME));
		} catch (error) {
			this.logService.log(`Unable to read folders in ${this.workspacesHome} (${error}).`);
		}

		const untitledWorkspaces: IWorkspaceIdentifier[] = coalesce(untitledWorkspacePaths.map(untitledWorkspacePath => {
			const workspace = this.resolveWorkspaceSync(untitledWorkspacePath);
			if (!workspace) {
				this.doDeleteUntitledWorkspaceSync(untitledWorkspacePath);

				return null; // invalid workspace
			}

			return { id: workspace.id, configPath: untitledWorkspacePath };
		}));

		return untitledWorkspaces;
	}
B
Benjamin Pasero 已提交
263
}