diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 5423e2ee35e..aed2162df64 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -244,6 +244,77 @@ export interface IFileSystemProvider { write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; } +export enum FileSystemProviderErrorCode { + FileExists = 'EntryExists', + FileNotFound = 'EntryNotFound', + FileNotADirectory = 'EntryNotADirectory', + FileIsADirectory = 'EntryIsADirectory', + NoPermissions = 'NoPermissions', + Unavailable = 'Unavailable' +} + +export class FileSystemProviderError extends Error { + + constructor(message: string, public readonly code?: FileSystemProviderErrorCode) { + super(message); + } +} + +export function createFileSystemProviderError(error: Error, code?: FileSystemProviderErrorCode): FileSystemProviderError { + const providerError = new FileSystemProviderError(error.toString(), code); + markAsFileSystemProviderError(providerError); + + return providerError; +} + +export function markAsFileSystemProviderError(error: Error, code?: FileSystemProviderErrorCode): Error { + error.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + + return error; +} + +export function toFileSystemProviderErrorCode(error: Error): FileSystemProviderErrorCode | undefined { + + // FileSystemProviderError comes with the code + if (error instanceof FileSystemProviderError) { + return error.code; + } + + // Any other error, check for name match by assuming that the error + // went through the markAsFileSystemProviderError() method + const match = /^(.+) \(FileSystemError\)$/.exec(error.name); + if (!match) { + return undefined; + } + + switch (match[1]) { + case FileSystemProviderErrorCode.FileExists: return FileSystemProviderErrorCode.FileExists; + case FileSystemProviderErrorCode.FileIsADirectory: return FileSystemProviderErrorCode.FileIsADirectory; + case FileSystemProviderErrorCode.FileNotADirectory: return FileSystemProviderErrorCode.FileNotADirectory; + case FileSystemProviderErrorCode.FileNotFound: return FileSystemProviderErrorCode.FileNotFound; + case FileSystemProviderErrorCode.NoPermissions: return FileSystemProviderErrorCode.NoPermissions; + case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable; + } + + return undefined; +} + +export function toFileOperationResult(error: Error): FileOperationResult { + switch (toFileSystemProviderErrorCode(error)) { + case FileSystemProviderErrorCode.FileNotFound: + return FileOperationResult.FILE_NOT_FOUND; + case FileSystemProviderErrorCode.FileIsADirectory: + return FileOperationResult.FILE_IS_DIRECTORY; + case FileSystemProviderErrorCode.NoPermissions: + return FileOperationResult.FILE_PERMISSION_DENIED; + case FileSystemProviderErrorCode.FileExists: + return FileOperationResult.FILE_MOVE_CONFLICT; + case FileSystemProviderErrorCode.FileNotADirectory: + default: + return FileOperationResult.FILE_OTHER_ERROR; + } +} + export interface IFileSystemProviderRegistrationEvent { added: boolean; scheme: string; @@ -670,7 +741,8 @@ export const enum FileOperationResult { FILE_PERMISSION_DENIED, FILE_TOO_LARGE, FILE_INVALID_PATH, - FILE_EXCEED_MEMORY_LIMIT + FILE_EXCEED_MEMORY_LIMIT, + FILE_OTHER_ERROR } export const AutoSaveConfiguration = { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 3e9ef712e6c..f875655a334 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -13,7 +13,7 @@ import { startsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as vscode from 'vscode'; - +import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; function es5ClassCompat(target: Function): any { ///@ts-ignore @@ -2188,27 +2188,30 @@ export enum FileChangeType { export class FileSystemError extends Error { static FileExists(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'EntryExists', FileSystemError.FileExists); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.FileExists, FileSystemError.FileExists); } static FileNotFound(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'EntryNotFound', FileSystemError.FileNotFound); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.FileNotFound, FileSystemError.FileNotFound); } static FileNotADirectory(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'EntryNotADirectory', FileSystemError.FileNotADirectory); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.FileNotADirectory, FileSystemError.FileNotADirectory); } static FileIsADirectory(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'EntryIsADirectory', FileSystemError.FileIsADirectory); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.FileIsADirectory, FileSystemError.FileIsADirectory); } static NoPermissions(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'NoPermissions', FileSystemError.NoPermissions); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.NoPermissions, FileSystemError.NoPermissions); } static Unavailable(messageOrUri?: string | URI): FileSystemError { - return new FileSystemError(messageOrUri, 'Unavailable', FileSystemError.Unavailable); + return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.Unavailable, FileSystemError.Unavailable); } constructor(uriOrMessage?: string | URI, code?: string, terminator?: Function) { super(URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); - this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + + // mark the error as file system provider error so that + // we can extract the error code on the receiving side + markAsFileSystemProviderError(this); // workaround when extending builtin objects and when compiling to ES5, see: // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work diff --git a/src/vs/workbench/services/files/node/remoteFileService.ts b/src/vs/workbench/services/files/node/remoteFileService.ts index 87c6a85d0d7..ab8b15ddc8c 100644 --- a/src/vs/workbench/services/files/node/remoteFileService.ts +++ b/src/vs/workbench/services/files/node/remoteFileService.ts @@ -14,7 +14,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/res import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileStat, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IResolveFileOptions, IResolveFileResult, IStat, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, IWatchOptions, FileType, ILegacyFileService, IFileService } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileStat, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IResolveFileOptions, IResolveFileResult, IStat, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, IWatchOptions, FileType, ILegacyFileService, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -198,30 +198,6 @@ export class RemoteFileService extends FileService { }; } - private _tryParseFileOperationResult(err: any): FileOperationResult | undefined { - if (!(err instanceof Error)) { - return undefined; - } - let match = /^(.+) \(FileSystemError\)$/.exec(err.name); - if (!match) { - return undefined; - } - switch (match[1]) { - case 'EntryNotFound': - return FileOperationResult.FILE_NOT_FOUND; - case 'EntryIsADirectory': - return FileOperationResult.FILE_IS_DIRECTORY; - case 'NoPermissions': - return FileOperationResult.FILE_PERMISSION_DENIED; - case 'EntryExists': - return FileOperationResult.FILE_MOVE_CONFLICT; - case 'EntryNotADirectory': - default: - // todo - return undefined; - } - } - // --- stat private _withProvider(resource: URI): Promise { @@ -433,8 +409,8 @@ export class RemoteFileService extends FileService { return fileStat; }, err => { const message = localize('err.create', "Failed to create file {0}", resource.toString(false)); - const result = this._tryParseFileOperationResult(err); - throw new FileOperationError(message, result || -1, options); + const result = toFileOperationResult(err); + throw new FileOperationError(message, result, options); }); } } @@ -548,7 +524,7 @@ export class RemoteFileService extends FileService { this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.MOVE, fileStat)); return fileStat; }, err => { - const result = this._tryParseFileOperationResult(err); + const result = toFileOperationResult(err); if (result === FileOperationResult.FILE_MOVE_CONFLICT) { throw new FileOperationError(localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), result); } @@ -584,7 +560,7 @@ export class RemoteFileService extends FileService { this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat)); return fileStat; }, err => { - const result = this._tryParseFileOperationResult(err); + const result = toFileOperationResult(err); if (result === FileOperationResult.FILE_MOVE_CONFLICT) { throw new FileOperationError(localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), result); } @@ -611,7 +587,7 @@ export class RemoteFileService extends FileService { return fileStat; }); }, err => { - const result = this._tryParseFileOperationResult(err); + const result = toFileOperationResult(err); if (result === FileOperationResult.FILE_MOVE_CONFLICT) { throw new FileOperationError(localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), result); } else if (err instanceof Error && err.name === 'ENOPRO') { diff --git a/src/vs/workbench/services/files2/common/fileService2.ts b/src/vs/workbench/services/files2/common/fileService2.ts index 97a80c03791..5ad2495d7f4 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType } from 'vs/platform/files/common/files'; +import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -200,12 +200,17 @@ export class FileService2 extends Disposable implements IFileService { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { - throw new Error(`${directory.toString()} is not a directory`); + throw new Error(`${directory.toString()} exists, but is not a directory`); } - break; // we have hit a directory -> good + break; // we have hit a directory that exists -> good } catch (e) { + // Bubble up any other error that is not file not found + if (toFileSystemProviderErrorCode(e) !== FileSystemProviderErrorCode.FileNotFound) { + throw e; + } + // Upon error, remember directories that need to be created directoriesToCreate.push(basename(directory)); diff --git a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts index 2fcbe9045f3..75019caa302 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -6,7 +6,7 @@ import { mkdir } from 'fs'; import { promisify } from 'util'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions } from 'vs/platform/files/common/files'; +import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux } from 'vs/base/common/platform'; @@ -40,14 +40,18 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro //#region File Metadata Resolving async stat(resource: URI): Promise { - const { stat, isSymbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly + try { + const { stat, isSymbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly - return { - type: isSymbolicLink ? FileType.SymbolicLink : stat.isFile() ? FileType.File : stat.isDirectory() ? FileType.Directory : FileType.Unknown, - ctime: stat.ctime.getTime(), - mtime: stat.mtime.getTime(), - size: stat.size - } as IStat; + return { + type: isSymbolicLink ? FileType.SymbolicLink : stat.isFile() ? FileType.File : stat.isDirectory() ? FileType.Directory : FileType.Unknown, + ctime: stat.ctime.getTime(), + mtime: stat.mtime.getTime(), + size: stat.size + } as IStat; + } catch (error) { + throw this.toFileSystemProviderError(error); + } } readdir(resource: URI): Promise<[string, FileType][]> { @@ -121,5 +125,26 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro return normalize(resource.fsPath); } + private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError { + let code: FileSystemProviderErrorCode | undefined = undefined; + switch (error.code) { + case 'ENOENT': + code = FileSystemProviderErrorCode.FileNotFound; + break; + case 'EISDIR': + code = FileSystemProviderErrorCode.FileIsADirectory; + break; + case 'EEXIST': + code = FileSystemProviderErrorCode.FileExists; + break; + case 'EPERM': + case 'EACCESS': + code = FileSystemProviderErrorCode.NoPermissions; + break; + } + + return createFileSystemProviderError(error, code); + } + //#endregion } \ No newline at end of file