workspacesMainService.ts 9.6 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 22 23 24 25 26 27 28
import { createHash } from 'crypto';
import URI from 'vs/base/common/uri';

// TODO@Ben migration
export interface ILegacyStoredWorkspace {
	id: string;
	folders: string[];
}
B
Benjamin Pasero 已提交
29 30 31 32 33

export class WorkspacesMainService implements IWorkspacesMainService {

	public _serviceBrand: any;

B
Benjamin Pasero 已提交
34
	protected workspacesHome: string;
B
Benjamin Pasero 已提交
35

36
	private _onWorkspaceSaved: Emitter<IWorkspaceSavedEvent>;
37
	private _onUntitledWorkspaceDeleted: Emitter<IWorkspaceIdentifier>;
38

39 40
	constructor(
		@IEnvironmentService private environmentService: IEnvironmentService,
41
		@ILogService private logService: ILogService
42
	) {
B
Benjamin Pasero 已提交
43
		this.workspacesHome = environmentService.workspacesHome;
44

45
		this._onWorkspaceSaved = new Emitter<IWorkspaceSavedEvent>();
46
		this._onUntitledWorkspaceDeleted = new Emitter<IWorkspaceIdentifier>();
47 48 49 50
	}

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

53 54
	public get onUntitledWorkspaceDeleted(): Event<IWorkspaceIdentifier> {
		return this._onUntitledWorkspaceDeleted.event;
55 56
	}

B
Benjamin Pasero 已提交
57 58 59 60 61 62 63 64
	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()));
	}

65
	public resolveWorkspaceSync(path: string): IResolvedWorkspace {
B
Benjamin Pasero 已提交
66
		if (!this.isWorkspacePath(path)) {
67 68 69
			return null; // does not look like a valid workspace config file
		}

B
Benjamin Pasero 已提交
70 71 72 73 74 75 76 77
		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 {
78
		try {
79
			const workspace = this.doParseStoredWorkspace(path, contents);
80

81
			// TODO@Ben migration
82
			const legacyStoredWorkspace = (<any>workspace) as ILegacyStoredWorkspace;
83
			if (legacyStoredWorkspace.folders.some(folder => typeof folder === 'string')) {
84 85
				workspace.folders = legacyStoredWorkspace.folders.map(folder => ({ path: URI.parse(folder).fsPath }));
				writeFileSync(path, JSON.stringify(workspace, null, '\t'));
86 87
			}

88
			// relative paths get resolved against the workspace location
89
			workspace.folders.forEach(folder => {
90
				if (!isAbsolute(folder.path)) {
91
					folder.path = resolve(dirname(path), folder.path);
92 93 94 95 96 97
				}
			});

			return {
				id: this.getWorkspaceId(path),
				configPath: path,
98
				folders: workspace.folders
99
			};
100
		} catch (error) {
101 102 103 104 105 106 107
			this.logService.log(error.toString());
		}

		return null;
	}

	private doParseStoredWorkspace(path: string, contents: string): IStoredWorkspace {
108 109

		// Parse workspace file
110 111 112 113 114 115
		let storedWorkspace: IStoredWorkspace;
		try {
			storedWorkspace = JSON.parse(contents);
		} catch (error) {
			throw new Error(`${path} cannot be parsed as JSON file (${error}).`);
		}
116

117 118 119 120 121 122
		// Filter out folders which do not have a path set
		if (Array.isArray(storedWorkspace.folders)) {
			storedWorkspace.folders = storedWorkspace.folders.filter(folder => !!folder.path);
		}

		// Validate
123 124
		if (!Array.isArray(storedWorkspace.folders) || storedWorkspace.folders.length === 0) {
			throw new Error(`${path} looks like an invalid workspace file.`);
125
		}
126 127

		return storedWorkspace;
B
Benjamin Pasero 已提交
128 129
	}

130 131 132 133
	private isInsideWorkspacesHome(path: string): boolean {
		return isParent(path, this.environmentService.workspacesHome, !isLinux /* ignore case */);
	}

B
Benjamin Pasero 已提交
134
	public createWorkspace(folders: string[]): TPromise<IWorkspaceIdentifier> {
135 136 137 138 139 140 141 142 143 144 145 146
		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 已提交
147 148
		}

149 150 151 152 153 154 155 156
		mkdirSync(configParent);

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

		return workspace;
	}

	private createUntitledWorkspace(folders: string[]): { workspace: IWorkspaceIdentifier, configParent: string, storedWorkspace: IStoredWorkspace } {
157 158 159
		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 已提交
160

161 162 163 164 165
		const storedWorkspace: IStoredWorkspace = {
			folders: folders.map(folder => ({
				path: folder
			}))
		};
B
Benjamin Pasero 已提交
166

167 168
		return {
			workspace: {
169 170
				id: this.getWorkspaceId(untitledWorkspaceConfigPath),
				configPath: untitledWorkspaceConfigPath
171 172 173 174
			},
			configParent: untitledWorkspaceConfigFolder,
			storedWorkspace
		};
B
Benjamin Pasero 已提交
175 176
	}

177 178 179 180 181 182
	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 已提交
183
	}
B
Benjamin Pasero 已提交
184 185 186 187

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

189
	public saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPath: string): TPromise<IWorkspaceIdentifier> {
B
Benjamin Pasero 已提交
190 191

		// Return early if target is same as source
192
		if (isEqual(workspace.configPath, targetConfigPath, !isLinux)) {
B
Benjamin Pasero 已提交
193 194 195
			return TPromise.as(workspace);
		}

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
		// 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 => {
212 213 214 215 216 217
				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
218 219
				}
			});
220

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

224 225
				// Event
				this._onWorkspaceSaved.fire({ workspace: savedWorkspaceIdentifier, oldConfigPath: workspace.configPath });
226

227 228 229 230 231
				// Delete untitled workspace
				this.deleteUntitledWorkspaceSync(workspace);

				return savedWorkspaceIdentifier;
			});
232 233
		});
	}
234

235
	public deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
236
		if (!this.isUntitledWorkspace(workspace)) {
237
			return; // only supported for untitled workspaces
238 239
		}

240
		// Delete from disk
241
		this.doDeleteUntitledWorkspaceSync(workspace.configPath);
242 243

		// Event
244
		this._onUntitledWorkspaceDeleted.fire(workspace);
245
	}
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275

	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 已提交
276
}