diskFileSystemProvider.ts 17.1 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 { mkdir, open, close, read, write, fdatasync } from 'fs';
M
Martin Aeschlimann 已提交
7
import * as os from 'os';
8
import { promisify } from 'util';
9
import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
10
import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files';
11 12
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
13
import { isLinux, isWindows } from 'vs/base/common/platform';
14
import { statLink, readdir, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists } from 'vs/base/node/pfs';
B
Benjamin Pasero 已提交
15
import { normalize, basename, dirname } from 'vs/base/common/path';
B
Benjamin Pasero 已提交
16
import { joinPath } from 'vs/base/common/resources';
17
import { isEqual } from 'vs/base/common/extpath';
18 19
import { retry, ThrottledDelayer } from 'vs/base/common/async';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
B
Benjamin Pasero 已提交
20
import { localize } from 'vs/nls';
21
import { IDiskFileChange, toFileChanges, ILogMessage } from 'vs/workbench/services/files/node/watcher/watcher';
B
Benjamin Pasero 已提交
22 23 24 25
import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService';
import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService';
import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService';
import { FileWatcher as NodeJSWatcherService } from 'vs/workbench/services/files/node/watcher/nodejs/watcherService';
26 27 28

export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider {

B
Benjamin Pasero 已提交
29 30 31 32
	constructor(private logService: ILogService) {
		super();
	}

33 34 35 36
	//#region File Capabilities

	onDidChangeCapabilities: Event<void> = Event.None;

37
	protected _capabilities: FileSystemProviderCapabilities;
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
	get capabilities(): FileSystemProviderCapabilities {
		if (!this._capabilities) {
			this._capabilities =
				FileSystemProviderCapabilities.FileReadWrite |
				FileSystemProviderCapabilities.FileOpenReadWriteClose |
				FileSystemProviderCapabilities.FileFolderCopy;

			if (isLinux) {
				this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
			}
		}

		return this._capabilities;
	}

	//#endregion

	//#region File Metadata Resolving

	async stat(resource: URI): Promise<IStat> {
58 59 60
		try {
			const { stat, isSymbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly

B
Benjamin Pasero 已提交
61 62 63 64 65 66 67
			let type: number;
			if (isSymbolicLink) {
				type = FileType.SymbolicLink | (stat.isDirectory() ? FileType.Directory : FileType.File);
			} else {
				type = stat.isFile() ? FileType.File : stat.isDirectory() ? FileType.Directory : FileType.Unknown;
			}

68
			return {
B
Benjamin Pasero 已提交
69
				type,
70 71 72
				ctime: stat.ctime.getTime(),
				mtime: stat.mtime.getTime(),
				size: stat.size
73
			};
74 75 76
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
77 78
	}

B
Benjamin Pasero 已提交
79 80 81 82 83
	async readdir(resource: URI): Promise<[string, FileType][]> {
		try {
			const children = await readdir(this.toFilePath(resource));

			const result: [string, FileType][] = [];
B
Benjamin Pasero 已提交
84
			await Promise.all(children.map(async child => {
B
Benjamin Pasero 已提交
85 86 87 88 89 90
				try {
					const stat = await this.stat(joinPath(resource, child));
					result.push([child, stat.type]);
				} catch (error) {
					this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied
				}
B
Benjamin Pasero 已提交
91
			}));
B
Benjamin Pasero 已提交
92 93 94 95 96

			return result;
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
97 98 99 100 101 102
	}

	//#endregion

	//#region File Reading/Writing

103 104 105 106 107 108 109 110
	async readFile(resource: URI): Promise<Uint8Array> {
		try {
			const filePath = this.toFilePath(resource);

			return await readFile(filePath);
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
111 112
	}

113
	async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
114
		let handle: number | undefined = undefined;
115 116 117 118
		try {
			const filePath = this.toFilePath(resource);

			// Validate target
119 120
			const fileExists = await exists(filePath);
			if (fileExists && !opts.overwrite) {
B
Benjamin Pasero 已提交
121
				throw createFileSystemProviderError(new Error(localize('fileExists', "File already exists")), FileSystemProviderErrorCode.FileExists);
122
			} else if (!fileExists && !opts.create) {
B
Benjamin Pasero 已提交
123
				throw createFileSystemProviderError(new Error(localize('fileNotExists', "File does not exist")), FileSystemProviderErrorCode.FileNotFound);
124 125
			}

126 127
			// Open
			handle = await this.open(resource, { create: true });
B
Benjamin Pasero 已提交
128

129 130
			// Write content at once
			await this.write(handle, 0, content, 0, content.byteLength);
131 132
		} catch (error) {
			throw this.toFileSystemProviderError(error);
133 134 135 136
		} finally {
			if (typeof handle === 'number') {
				await this.close(handle);
			}
137
		}
138 139
	}

140 141 142
	private writeHandles: Set<number> = new Set();
	private canFlush: boolean = true;

143 144 145 146
	async open(resource: URI, opts: FileOpenOptions): Promise<number> {
		try {
			const filePath = this.toFilePath(resource);

147
			let flags: string | undefined = undefined;
148
			if (opts.create) {
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
				if (isWindows && await exists(filePath)) {
					try {
						// On Windows and if the file exists, we use a different strategy of saving the file
						// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
						// (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams
						// (see https://github.com/Microsoft/vscode/issues/6363)
						await truncate(filePath, 0);

						// After a successful truncate() the flag can be set to 'r+' which will not truncate.
						flags = 'r+';
					} catch (error) {
						this.logService.trace(error);
					}
				}

				// we take opts.create as a hint that the file is opened for writing
165 166
				// as such we use 'w' to truncate an existing or create the
				// file otherwise. we do not allow reading.
167 168 169
				if (!flags) {
					flags = 'w';
				}
170 171 172 173
			} else {
				// otherwise we assume the file is opened for reading
				// as such we use 'r' to neither truncate, nor create
				// the file.
B
Benjamin Pasero 已提交
174
				flags = 'r';
175 176
			}

177 178 179 180 181 182 183 184
			const handle = await promisify(open)(filePath, flags);

			// remember that this handle was used for writing
			if (opts.create) {
				this.writeHandles.add(handle);
			}

			return handle;
185 186 187
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
188 189
	}

190 191
	async close(fd: number): Promise<void> {
		try {
192 193 194 195 196 197 198 199 200 201 202 203 204
			// if a handle is closed that was used for writing, ensure
			// to flush the contents to disk if possible.
			if (this.writeHandles.delete(fd) && this.canFlush) {
				try {
					await promisify(fdatasync)(fd);
				} catch (error) {
					// In some exotic setups it is well possible that node fails to sync
					// In that case we disable flushing and log the error to our logger
					this.canFlush = false;
					this.logService.error(error);
				}
			}

205 206 207 208
			return await promisify(close)(fd);
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
209 210
	}

211 212 213 214 215 216 217 218 219 220 221
	async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
		try {
			const result = await promisify(read)(fd, data, offset, length, pos);
			if (typeof result === 'number') {
				return result; // node.d.ts fail
			}

			return result.bytesRead;
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
222 223
	}

224
	async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
225 226 227 228 229 230 231
		// we know at this point that the file to write to is truncated and thus empty
		// if the write now fails, the file remains empty. as such we really try hard
		// to ensure the write succeeds by retrying up to three times.
		return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);
	}

	private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
232 233 234 235 236 237 238 239 240 241
		try {
			const result = await promisify(write)(fd, data, offset, length, pos);
			if (typeof result === 'number') {
				return result; // node.d.ts fail
			}

			return result.bytesWritten;
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
242 243 244 245 246 247
	}

	//#endregion

	//#region Move/Copy/Delete/Create Folder

248 249 250 251 252 253
	async mkdir(resource: URI): Promise<void> {
		try {
			await promisify(mkdir)(this.toFilePath(resource));
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
254 255
	}

256 257 258 259
	async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
		try {
			const filePath = this.toFilePath(resource);

260
			await this.doDelete(filePath, opts);
261 262 263
		} catch (error) {
			throw this.toFileSystemProviderError(error);
		}
264 265
	}

266 267
	protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
		if (opts.recursive) {
B
Benjamin Pasero 已提交
268
			await rimraf(filePath, RimRafMode.MOVE);
269 270 271 272 273
		} else {
			await unlink(filePath);
		}
	}

B
Benjamin Pasero 已提交
274
	async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
B
Benjamin Pasero 已提交
275 276 277
		const fromFilePath = this.toFilePath(from);
		const toFilePath = this.toFilePath(to);

B
Benjamin Pasero 已提交
278 279
		try {

280 281 282 283
			// Ensure target does not exist
			await this.validateTargetDeleted(from, to, opts && opts.overwrite);

			// Move
B
Benjamin Pasero 已提交
284 285
			await move(fromFilePath, toFilePath);
		} catch (error) {
B
Benjamin Pasero 已提交
286 287 288 289 290 291 292

			// rewrite some typical errors that can happen especially around symlinks
			// to something the user can better understand
			if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
				error = new Error(localize('moveError', "Unable to move '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
			}

B
Benjamin Pasero 已提交
293 294
			throw this.toFileSystemProviderError(error);
		}
295 296
	}

297
	async copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
B
Benjamin Pasero 已提交
298 299 300
		const fromFilePath = this.toFilePath(from);
		const toFilePath = this.toFilePath(to);

301 302
		try {

303 304 305 306 307
			// Ensure target does not exist
			await this.validateTargetDeleted(from, to, opts && opts.overwrite);

			// Copy
			await copy(fromFilePath, toFilePath);
308
		} catch (error) {
B
Benjamin Pasero 已提交
309 310 311 312 313 314 315

			// rewrite some typical errors that can happen especially around symlinks
			// to something the user can better understand
			if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
				error = new Error(localize('copyError', "Unable to copy '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
			}

316 317
			throw this.toFileSystemProviderError(error);
		}
318 319
	}

320 321 322 323 324 325 326 327
	private async validateTargetDeleted(from: URI, to: URI, overwrite?: boolean): Promise<void> {
		const fromFilePath = this.toFilePath(from);
		const toFilePath = this.toFilePath(to);

		const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
		const isCaseChange = isPathCaseSensitive ? false : isEqual(fromFilePath, toFilePath, true /* ignore case */);

		// handle existing target (unless this is a case change)
328
		if (!isCaseChange && await exists(toFilePath)) {
329 330 331 332
			if (!overwrite) {
				throw createFileSystemProviderError(new Error('File at target already exists'), FileSystemProviderErrorCode.FileExists);
			}

333
			await this.delete(to, { recursive: true, useTrash: false });
334 335 336
		}
	}

337 338 339 340
	//#endregion

	//#region File Watching

341 342
	private _onDidWatchErrorOccur: Emitter<Error> = this._register(new Emitter<Error>());
	get onDidErrorOccur(): Event<Error> { return this._onDidWatchErrorOccur.event; }
343

344 345 346
	private _onDidChangeFile: Emitter<IFileChange[]> = this._register(new Emitter<IFileChange[]>());
	get onDidChangeFile(): Event<IFileChange[]> { return this._onDidChangeFile.event; }

B
Benjamin Pasero 已提交
347
	private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
348 349
	private recursiveFoldersToWatch: { path: string, excludes: string[] }[] = [];
	private recursiveWatchRequestDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(0));
350

351 352
	private recursiveWatcherLogLevelListener: IDisposable | undefined;

353
	watch(resource: URI, opts: IWatchOptions): IDisposable {
354
		if (opts.recursive) {
355
			return this.watchRecursive(resource, opts.excludes);
356 357 358 359 360
		}

		return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases
	}

361
	private watchRecursive(resource: URI, excludes: string[]): IDisposable {
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380

		// Add to list of folders to watch recursively
		const folderToWatch = { path: this.toFilePath(resource), excludes };
		this.recursiveFoldersToWatch.push(folderToWatch);

		// Trigger update
		this.refreshRecursiveWatchers();

		return toDisposable(() => {

			// Remove from list of folders to watch recursively
			this.recursiveFoldersToWatch.splice(this.recursiveFoldersToWatch.indexOf(folderToWatch), 1);

			// Trigger update
			this.refreshRecursiveWatchers();
		});
	}

	private refreshRecursiveWatchers(): void {
381 382 383 384

		// Buffer requests for recursive watching to decide on right watcher
		// that supports potentially watching more than one folder at once
		this.recursiveWatchRequestDelayer.trigger(() => {
385
			this.doRefreshRecursiveWatchers();
386

387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
			return Promise.resolve();
		});
	}

	private doRefreshRecursiveWatchers(): void {

		// Reuse existing
		if (this.recursiveWatcher instanceof NsfwWatcherService) {
			this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch);
		}

		// Create new
		else {

			// Dispose old
B
Benjamin Pasero 已提交
402
			dispose(this.recursiveWatcher);
403
			this.recursiveWatcher = undefined;
404

405 406 407 408 409 410
			// Create new if we actually have folders to watch
			if (this.recursiveFoldersToWatch.length > 0) {
				let watcherImpl: {
					new(
						folders: { path: string, excludes: string[] }[],
						onChange: (changes: IDiskFileChange[]) => void,
411
						onLogMessage: (msg: ILogMessage) => void,
M
Martin Aeschlimann 已提交
412 413
						verboseLogging: boolean,
						watcherOptions?: { [key: string]: boolean | number | string }
414 415
					): WindowsWatcherService | UnixWatcherService | NsfwWatcherService
				};
M
Martin Aeschlimann 已提交
416 417 418 419 420 421 422 423 424 425 426 427 428 429
				let watcherOptions = undefined;

				if (this.forcePolling()) {
					// WSL needs a polling watcher
					watcherImpl = UnixWatcherService;
					watcherOptions = { usePolling: true };
				} else {
					// Single Folder Watcher
					if (this.recursiveFoldersToWatch.length === 1) {
						if (isWindows) {
							watcherImpl = WindowsWatcherService;
						} else {
							watcherImpl = UnixWatcherService;
						}
430
					}
431

M
Martin Aeschlimann 已提交
432 433 434 435
					// Multi Folder Watcher
					else {
						watcherImpl = NsfwWatcherService;
					}
436 437
				}

438 439 440 441
				// Create and start watching
				this.recursiveWatcher = new watcherImpl(
					this.recursiveFoldersToWatch,
					event => this._onDidChangeFile.fire(toFileChanges(event)),
442 443 444 445 446 447
					msg => {
						if (msg.type === 'error') {
							this._onDidWatchErrorOccur.fire(new Error(msg.message));
						}
						this.logService[msg.type](msg.message);
					},
M
Martin Aeschlimann 已提交
448 449
					this.logService.getLevel() === LogLevel.Trace,
					watcherOptions
450
				);
451 452 453 454 455 456 457 458

				if (!this.recursiveWatcherLogLevelListener) {
					this.recursiveWatcherLogLevelListener = this.logService.onDidChangeLogLevel(_ => {
						if (this.recursiveWatcher) {
							this.recursiveWatcher.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
						}
					});
				}
459
			}
460
		}
461 462
	}

463
	private watchNonRecursive(resource: URI): IDisposable {
464
		const watcherService = new NodeJSWatcherService(
465 466
			this.toFilePath(resource),
			changes => this._onDidChangeFile.fire(toFileChanges(changes)),
467 468 469 470 471 472
			msg => {
				if (msg.type === 'error') {
					this._onDidWatchErrorOccur.fire(new Error(msg.message));
				}
				this.logService[msg.type](msg.message);
			},
473 474
			this.logService.getLevel() === LogLevel.Trace
		);
475 476 477 478 479
		const logLevelListener = this.logService.onDidChangeLogLevel(_ => {
			watcherService.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
		});

		return combinedDisposable(watcherService, logLevelListener);
480 481
	}

482 483 484 485
	//#endregion

	//#region Helpers

486
	protected toFilePath(resource: URI): string {
487 488 489
		return normalize(resource.fsPath);
	}

490
	private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError {
B
Benjamin Pasero 已提交
491 492 493 494
		if (error instanceof FileSystemProviderError) {
			return error; // avoid double conversion
		}

B
Benjamin Pasero 已提交
495
		let code: FileSystemProviderErrorCode;
496 497 498 499 500 501 502 503 504 505 506
		switch (error.code) {
			case 'ENOENT':
				code = FileSystemProviderErrorCode.FileNotFound;
				break;
			case 'EISDIR':
				code = FileSystemProviderErrorCode.FileIsADirectory;
				break;
			case 'EEXIST':
				code = FileSystemProviderErrorCode.FileExists;
				break;
			case 'EPERM':
507
			case 'EACCES':
508 509
				code = FileSystemProviderErrorCode.NoPermissions;
				break;
B
Benjamin Pasero 已提交
510 511
			default:
				code = FileSystemProviderErrorCode.Unknown;
512 513 514 515 516
		}

		return createFileSystemProviderError(error, code);
	}

M
Martin Aeschlimann 已提交
517 518 519 520 521 522

	forcePolling(): boolean {
		// wsl1 needs polling
		return isLinux && /^[\.\-0-9]+-Microsoft/.test(os.release());
	}

523
	//#endregion
524 525 526 527

	dispose(): void {
		super.dispose();

B
Benjamin Pasero 已提交
528 529
		dispose(this.recursiveWatcher);
		this.recursiveWatcher = undefined;
530 531 532

		dispose(this.recursiveWatcherLogLevelListener);
		this.recursiveWatcherLogLevelListener = undefined;
533
	}
534
}