Path: blob/main/src/vs/platform/files/node/diskFileSystemProvider.ts
5222 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Stats, constants, promises } from 'fs';6import { Barrier, retry } from '../../../base/common/async.js';7import { ResourceMap } from '../../../base/common/map.js';8import { VSBuffer } from '../../../base/common/buffer.js';9import { CancellationToken } from '../../../base/common/cancellation.js';10import { Event } from '../../../base/common/event.js';11import { isEqual } from '../../../base/common/extpath.js';12import { DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';13import { basename, dirname, join } from '../../../base/common/path.js';14import { isLinux, isWindows } from '../../../base/common/platform.js';15import { extUriBiasedIgnorePathCase, joinPath, basename as resourcesBasename, dirname as resourcesDirname } from '../../../base/common/resources.js';16import { newWriteableStream, ReadableStreamEvents } from '../../../base/common/stream.js';17import { URI } from '../../../base/common/uri.js';18import { IDirent, Promises, RimRafMode, SymlinkSupport } from '../../../base/node/pfs.js';19import { localize } from '../../../nls.js';20import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileChange, IFileSystemProviderWithFileRealpathCapability } from '../common/files.js';21import { readFileIntoStream } from '../common/io.js';22import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage } from '../common/watcher.js';23import { AbstractDiskFileSystemProvider } from '../common/diskFileSystemProvider.js';24import { UniversalWatcherClient } from './watcher/watcherClient.js';25import { NodeJSWatcherClient } from './watcher/nodejs/nodejsClient.js';2627export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements28IFileSystemProviderWithFileReadWriteCapability,29IFileSystemProviderWithOpenReadWriteCloseCapability,30IFileSystemProviderWithFileReadStreamCapability,31IFileSystemProviderWithFileFolderCopyCapability,32IFileSystemProviderWithFileAtomicReadCapability,33IFileSystemProviderWithFileAtomicWriteCapability,34IFileSystemProviderWithFileAtomicDeleteCapability,35IFileSystemProviderWithFileCloneCapability,36IFileSystemProviderWithFileRealpathCapability {3738private static TRACE_LOG_RESOURCE_LOCKS = false; // not enabled by default because very spammy3940//#region File Capabilities4142readonly onDidChangeCapabilities = Event.None;4344private _capabilities: FileSystemProviderCapabilities | undefined;45get capabilities(): FileSystemProviderCapabilities {46if (!this._capabilities) {47this._capabilities =48FileSystemProviderCapabilities.FileReadWrite |49FileSystemProviderCapabilities.FileOpenReadWriteClose |50FileSystemProviderCapabilities.FileReadStream |51FileSystemProviderCapabilities.FileFolderCopy |52FileSystemProviderCapabilities.FileWriteUnlock |53FileSystemProviderCapabilities.FileAppend |54FileSystemProviderCapabilities.FileAtomicRead |55FileSystemProviderCapabilities.FileAtomicWrite |56FileSystemProviderCapabilities.FileAtomicDelete |57FileSystemProviderCapabilities.FileClone |58FileSystemProviderCapabilities.FileRealpath;5960if (isLinux) {61this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;62}63}6465return this._capabilities;66}6768//#endregion6970//#region File Metadata Resolving7172async stat(resource: URI): Promise<IStat> {73try {74const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly7576let permissions: FilePermission | undefined = undefined;77if ((stat.mode & 0o200) === 0) {78permissions = FilePermission.Locked;79}80if (81stat.mode & constants.S_IXUSR ||82stat.mode & constants.S_IXGRP ||83stat.mode & constants.S_IXOTH84) {85permissions = (permissions ?? 0) | FilePermission.Executable;86}8788return {89type: this.toType(stat, symbolicLink),90ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time91mtime: stat.mtime.getTime(),92size: stat.size,93permissions94};95} catch (error) {96throw this.toFileSystemProviderError(error);97}98}99100private async statIgnoreError(resource: URI): Promise<IStat | undefined> {101try {102return await this.stat(resource);103} catch (error) {104return undefined;105}106}107108async realpath(resource: URI): Promise<string> {109const filePath = this.toFilePath(resource);110111return Promises.realpath(filePath);112}113114async readdir(resource: URI): Promise<[string, FileType][]> {115try {116const children = await Promises.readdir(this.toFilePath(resource), { withFileTypes: true });117118const result: [string, FileType][] = [];119await Promise.all(children.map(async child => {120try {121let type: FileType;122if (child.isSymbolicLink()) {123type = (await this.stat(joinPath(resource, child.name))).type; // always resolve target the link points to if any124} else {125type = this.toType(child);126}127128result.push([child.name, type]);129} catch (error) {130this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied131}132}));133134return result;135} catch (error) {136throw this.toFileSystemProviderError(error);137}138}139140private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType {141142// Signal file type by checking for file / directory, except:143// - symbolic links pointing to nonexistent files are FileType.Unknown144// - files that are neither file nor directory are FileType.Unknown145let type: FileType;146if (symbolicLink?.dangling) {147type = FileType.Unknown;148} else if (entry.isFile()) {149type = FileType.File;150} else if (entry.isDirectory()) {151type = FileType.Directory;152} else {153type = FileType.Unknown;154}155156// Always signal symbolic link as file type additionally157if (symbolicLink) {158type |= FileType.SymbolicLink;159}160161return type;162}163164//#endregion165166//#region File Reading/Writing167168private readonly resourceLocks = new ResourceMap<Barrier>(resource => extUriBiasedIgnorePathCase.getComparisonKey(resource));169170private async createResourceLock(resource: URI): Promise<IDisposable> {171const filePath = this.toFilePath(resource);172this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - request to acquire resource lock (${filePath})`);173174// Await pending locks for resource. It is possible for a new lock being175// added right after opening, so we have to loop over locks until no lock176// remains.177let existingLock: Barrier | undefined = undefined;178while (existingLock = this.resourceLocks.get(resource)) {179this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - waiting for resource lock to be released (${filePath})`);180await existingLock.wait();181}182183// Store new184const newLock = new Barrier();185this.resourceLocks.set(resource, newLock);186187this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - new resource lock created (${filePath})`);188189return toDisposable(() => {190this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock dispose() (${filePath})`);191192// Delete lock if it is still ours193if (this.resourceLocks.get(resource) === newLock) {194this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock removed from resource-lock map (${filePath})`);195this.resourceLocks.delete(resource);196}197198// Open lock199this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock barrier open() (${filePath})`);200newLock.open();201});202}203204async readFile(resource: URI, options?: IFileAtomicReadOptions): Promise<Uint8Array> {205let lock: IDisposable | undefined = undefined;206try {207if (options?.atomic) {208this.traceLock(`[Disk FileSystemProvider]: atomic read operation started (${this.toFilePath(resource)})`);209210// When the read should be atomic, make sure211// to await any pending locks for the resource212// and lock for the duration of the read.213lock = await this.createResourceLock(resource);214}215216const filePath = this.toFilePath(resource);217218return await promises.readFile(filePath);219} catch (error) {220throw this.toFileSystemProviderError(error);221} finally {222lock?.dispose();223}224}225226private traceLock(msg: string): void {227if (DiskFileSystemProvider.TRACE_LOG_RESOURCE_LOCKS) {228this.logService.trace(msg);229}230}231232readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {233const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);234235readFileIntoStream(this, resource, stream, data => data.buffer, {236...opts,237bufferSize: 256 * 1024 // read into chunks of 256kb each to reduce IPC overhead238}, token);239240return stream;241}242243async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {244if (opts?.atomic !== false && opts?.atomic?.postfix && await this.canWriteFileAtomic(resource)) {245return this.doWriteFileAtomic(resource, joinPath(resourcesDirname(resource), `${resourcesBasename(resource)}${opts.atomic.postfix}`), content, opts);246} else {247return this.doWriteFile(resource, content, opts);248}249}250251private async canWriteFileAtomic(resource: URI): Promise<boolean> {252try {253const filePath = this.toFilePath(resource);254const { symbolicLink } = await SymlinkSupport.stat(filePath);255if (symbolicLink) {256// atomic writes are unsupported for symbolic links because257// we need to ensure that the `rename` operation is atomic258// and that only works if the link is on the same disk.259// Since we do not know where the symbolic link points to260// we refuse to write atomically.261return false;262}263} catch (error) {264// ignore stat errors here and just proceed trying to write265}266267return true; // atomic writing supported268}269270private async doWriteFileAtomic(resource: URI, tempResource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {271272// Ensure to create locks for all resources involved273// since atomic write involves mutiple disk operations274// and resources.275276const locks = new DisposableStore();277278try {279locks.add(await this.createResourceLock(resource));280locks.add(await this.createResourceLock(tempResource));281282// Write to temp resource first283await this.doWriteFile(tempResource, content, { ...opts, create: true, overwrite: true }, true /* disable write lock */);284285try {286287// Rename over existing to ensure atomic replace288await this.rename(tempResource, resource, { overwrite: true });289290} catch (error) {291292// Cleanup in case of rename error293try {294await this.delete(tempResource, { recursive: false, useTrash: false, atomic: false });295} catch (error) {296// ignore - we want the outer error to bubble up297}298299throw error;300}301} finally {302locks.dispose();303}304}305306private async doWriteFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions, disableWriteLock?: boolean): Promise<void> {307let handle: number | undefined = undefined;308try {309const filePath = this.toFilePath(resource);310311// Validate target unless { create: true, overwrite: true }312if (!opts.create || !opts.overwrite) {313const fileExists = await Promises.exists(filePath);314if (fileExists) {315if (!opts.overwrite) {316throw createFileSystemProviderError(localize('fileExists', "File already exists"), FileSystemProviderErrorCode.FileExists);317}318} else {319if (!opts.create) {320throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);321}322}323}324325// Open326handle = await this.open(resource, { create: true, append: opts.append, unlock: opts.unlock }, disableWriteLock);327328// Write content at once329await this.write(handle, 0, content, 0, content.byteLength);330} catch (error) {331throw await this.toFileSystemProviderWriteError(resource, error);332} finally {333if (typeof handle === 'number') {334await this.close(handle);335}336}337}338339private readonly mapHandleToPos = new Map<number, number>();340private readonly mapHandleToLock = new Map<number, IDisposable>();341342private readonly writeHandles = new Map<number, URI>();343344private static canFlush = true;345346static configureFlushOnWrite(enabled: boolean): void {347DiskFileSystemProvider.canFlush = enabled;348}349350async open(resource: URI, opts: IFileOpenOptions, disableWriteLock?: boolean): Promise<number> {351const filePath = this.toFilePath(resource);352353// Writes: guard multiple writes to the same resource354// behind a single lock to prevent races when writing355// from multiple places at the same time to the same file356let lock: IDisposable | undefined = undefined;357if (isFileOpenForWriteOptions(opts) && !disableWriteLock) {358lock = await this.createResourceLock(resource);359}360361let fd: number | undefined = undefined;362try {363364// Determine whether to unlock the file (write only)365if (isFileOpenForWriteOptions(opts) && opts.unlock) {366try {367const { stat } = await SymlinkSupport.stat(filePath);368if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {369await promises.chmod(filePath, stat.mode | 0o200);370}371} catch (error) {372if (error.code !== 'ENOENT') {373this.logService.trace(error); // log errors but do not give up writing374}375}376}377378// Windows gets special treatment (write only, but not for append)379if (isWindows && isFileOpenForWriteOptions(opts) && !opts.append) {380try {381382// We try to use 'r+' for opening (which will fail if the file does not exist)383// to prevent issues when saving hidden files or preserving alternate data384// streams.385// Related issues:386// - https://github.com/microsoft/vscode/issues/931387// - https://github.com/microsoft/vscode/issues/6363388fd = await Promises.open(filePath, 'r+');389390// The flag 'r+' will not truncate the file, so we have to do this manually391await Promises.ftruncate(fd, 0);392} catch (error) {393if (error.code !== 'ENOENT') {394this.logService.trace(error); // log errors but do not give up writing395}396397// Make sure to close the file handle if we have one398if (typeof fd === 'number') {399try {400await Promises.close(fd);401} catch (error) {402this.logService.trace(error); // log errors but do not give up writing403}404405// Reset `fd` to be able to try again with 'w'406fd = undefined;407}408}409}410411if (typeof fd !== 'number') {412fd = await Promises.open(filePath, isFileOpenForWriteOptions(opts) ?413// We take `opts.create` as a hint that the file is opened for writing414// as such we use 'w' to truncate an existing or create the415// file otherwise. we do not allow reading.416// If `opts.append` is true, use 'a' to append to the file.417(opts.append ? 'a' : 'w') :418// Otherwise we assume the file is opened for reading419// as such we use 'r' to neither truncate, nor create420// the file.421'r'422);423}424425} catch (error) {426427// Release lock because we have no valid handle428// if we did open a lock during this operation429lock?.dispose();430431// Rethrow as file system provider error432if (isFileOpenForWriteOptions(opts)) {433throw await this.toFileSystemProviderWriteError(resource, error);434} else {435throw this.toFileSystemProviderError(error);436}437}438439// Remember this handle to track file position of the handle440// we init the position to 0 since the file descriptor was441// just created and the position was not moved so far (see442// also http://man7.org/linux/man-pages/man2/open.2.html -443// "The file offset is set to the beginning of the file.")444this.mapHandleToPos.set(fd, 0);445446// remember that this handle was used for writing447if (isFileOpenForWriteOptions(opts)) {448this.writeHandles.set(fd, resource);449}450451if (lock) {452const previousLock = this.mapHandleToLock.get(fd);453454// Remember that this handle has an associated lock455this.traceLock(`[Disk FileSystemProvider]: open() - storing lock for handle ${fd} (${filePath})`);456this.mapHandleToLock.set(fd, lock);457458// There is a slight chance that a resource lock for a459// handle was not yet disposed when we acquire a new460// lock, so we must ensure to dispose the previous lock461// before storing a new one for the same handle, other462// wise we end up in a deadlock situation463// https://github.com/microsoft/vscode/issues/142462464if (previousLock) {465this.traceLock(`[Disk FileSystemProvider]: open() - disposing a previous lock that was still stored on same handle ${fd} (${filePath})`);466previousLock.dispose();467}468}469470return fd;471}472473async close(fd: number): Promise<void> {474475// It is very important that we keep any associated lock476// for the file handle before attempting to call `fs.close(fd)`477// because of a possible race condition: as soon as a file478// handle is released, the OS may assign the same handle to479// the next `fs.open` call and as such it is possible that our480// lock is getting overwritten481const lockForHandle = this.mapHandleToLock.get(fd);482483try {484485// Remove this handle from map of positions486this.mapHandleToPos.delete(fd);487488// If a handle is closed that was used for writing, ensure489// to flush the contents to disk if possible.490if (this.writeHandles.delete(fd) && DiskFileSystemProvider.canFlush) {491try {492await Promises.fdatasync(fd); // https://github.com/microsoft/vscode/issues/9589493} catch (error) {494// In some exotic setups it is well possible that node fails to sync495// In that case we disable flushing and log the error to our logger496DiskFileSystemProvider.configureFlushOnWrite(false);497this.logService.error(error);498}499}500501return await Promises.close(fd);502} catch (error) {503throw this.toFileSystemProviderError(error);504} finally {505if (lockForHandle) {506if (this.mapHandleToLock.get(fd) === lockForHandle) {507this.traceLock(`[Disk FileSystemProvider]: close() - resource lock removed from handle-lock map ${fd}`);508this.mapHandleToLock.delete(fd); // only delete from map if this is still our lock!509}510511this.traceLock(`[Disk FileSystemProvider]: close() - disposing lock for handle ${fd}`);512lockForHandle.dispose();513}514}515}516517async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {518const normalizedPos = this.normalizePos(fd, pos);519520let bytesRead: number | null = null;521try {522bytesRead = (await Promises.read(fd, data, offset, length, normalizedPos)).bytesRead;523} catch (error) {524throw this.toFileSystemProviderError(error);525} finally {526this.updatePos(fd, normalizedPos, bytesRead);527}528529return bytesRead;530}531532private normalizePos(fd: number, pos: number): number | null {533534// When calling fs.read/write we try to avoid passing in the "pos" argument and535// rather prefer to pass in "null" because this avoids an extra seek(pos)536// call that in some cases can even fail (e.g. when opening a file over FTP -537// see https://github.com/microsoft/vscode/issues/73884).538//539// as such, we compare the passed in position argument with our last known540// position for the file descriptor and use "null" if they match.541if (pos === this.mapHandleToPos.get(fd)) {542return null;543}544545return pos;546}547548private updatePos(fd: number, pos: number | null, bytesLength: number | null): void {549const lastKnownPos = this.mapHandleToPos.get(fd);550if (typeof lastKnownPos === 'number') {551552// pos !== null signals that previously a position was used that is553// not null. node.js documentation explains, that in this case554// the internal file pointer is not moving and as such we do not move555// our position pointer.556//557// Docs: "If position is null, data will be read from the current file position,558// and the file position will be updated. If position is an integer, the file position559// will remain unchanged."560if (typeof pos === 'number') {561// do not modify the position562}563564// bytesLength = number is a signal that the read/write operation was565// successful and as such we need to advance the position in the Map566//567// Docs (http://man7.org/linux/man-pages/man2/read.2.html):568// "On files that support seeking, the read operation commences at the569// file offset, and the file offset is incremented by the number of570// bytes read."571//572// Docs (http://man7.org/linux/man-pages/man2/write.2.html):573// "For a seekable file (i.e., one to which lseek(2) may be applied, for574// example, a regular file) writing takes place at the file offset, and575// the file offset is incremented by the number of bytes actually576// written."577else if (typeof bytesLength === 'number') {578this.mapHandleToPos.set(fd, lastKnownPos + bytesLength);579}580581// bytesLength = null signals an error in the read/write operation582// and as such we drop the handle from the Map because the position583// is unspecificed at this point.584else {585this.mapHandleToPos.delete(fd);586}587}588}589590async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {591592// We know at this point that the file to write to is truncated and thus empty593// if the write now fails, the file remains empty. as such we really try hard594// to ensure the write succeeds by retrying up to three times.595return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);596}597598private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {599const normalizedPos = this.normalizePos(fd, pos);600601let bytesWritten: number | null = null;602try {603bytesWritten = (await Promises.write(fd, data, offset, length, normalizedPos)).bytesWritten;604} catch (error) {605throw await this.toFileSystemProviderWriteError(this.writeHandles.get(fd), error);606} finally {607this.updatePos(fd, normalizedPos, bytesWritten);608}609610return bytesWritten;611}612613//#endregion614615//#region Move/Copy/Delete/Create Folder616617async mkdir(resource: URI): Promise<void> {618try {619await promises.mkdir(this.toFilePath(resource));620} catch (error) {621throw this.toFileSystemProviderError(error);622}623}624625async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {626try {627const filePath = this.toFilePath(resource);628if (opts.recursive) {629let rmMoveToPath: string | undefined = undefined;630if (opts?.atomic !== false && opts.atomic.postfix) {631rmMoveToPath = join(dirname(filePath), `${basename(filePath)}${opts.atomic.postfix}`);632}633634await Promises.rm(filePath, RimRafMode.MOVE, rmMoveToPath);635} else {636try {637await promises.unlink(filePath);638} catch (unlinkError) {639640// `fs.unlink` will throw when used on directories641// we try to detect this error and then see if the642// provided resource is actually a directory. in that643// case we use `fs.rmdir` to delete the directory.644645if (unlinkError.code === 'EPERM' || unlinkError.code === 'EISDIR') {646let isDirectory = false;647try {648const { stat, symbolicLink } = await SymlinkSupport.stat(filePath);649isDirectory = stat.isDirectory() && !symbolicLink;650} catch (statError) {651// ignore652}653654if (isDirectory) {655await promises.rmdir(filePath);656} else {657throw unlinkError;658}659} else {660throw unlinkError;661}662}663}664} catch (error) {665throw this.toFileSystemProviderError(error);666}667}668669async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {670const fromFilePath = this.toFilePath(from);671const toFilePath = this.toFilePath(to);672673if (fromFilePath === toFilePath) {674return; // simulate node.js behaviour here and do a no-op if paths match675}676677try {678679// Validate the move operation can perform680await this.validateMoveCopy(from, to, 'move', opts.overwrite);681682// Rename683await Promises.rename(fromFilePath, toFilePath);684} catch (error) {685686// Rewrite some typical errors that can happen especially around symlinks687// to something the user can better understand688if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {689error = new Error(localize('moveError', "Unable to move '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));690}691692throw this.toFileSystemProviderError(error);693}694}695696async copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {697const fromFilePath = this.toFilePath(from);698const toFilePath = this.toFilePath(to);699700if (fromFilePath === toFilePath) {701return; // simulate node.js behaviour here and do a no-op if paths match702}703704try {705706// Validate the copy operation can perform707await this.validateMoveCopy(from, to, 'copy', opts.overwrite);708709// Copy710await Promises.copy(fromFilePath, toFilePath, { preserveSymlinks: true });711} catch (error) {712713// Rewrite some typical errors that can happen especially around symlinks714// to something the user can better understand715if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {716error = new Error(localize('copyError', "Unable to copy '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));717}718719throw this.toFileSystemProviderError(error);720}721}722723private async validateMoveCopy(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<void> {724const fromFilePath = this.toFilePath(from);725const toFilePath = this.toFilePath(to);726727let isSameResourceWithDifferentPathCase = false;728const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);729if (!isPathCaseSensitive) {730isSameResourceWithDifferentPathCase = isEqual(fromFilePath, toFilePath, true /* ignore case */);731}732733if (isSameResourceWithDifferentPathCase) {734735// You cannot copy the same file to the same location with different736// path case unless you are on a case sensitive file system737if (mode === 'copy') {738throw createFileSystemProviderError(localize('fileCopyErrorPathCase', "File cannot be copied to same path with different path case"), FileSystemProviderErrorCode.FileExists);739}740741// You can move the same file to the same location with different742// path case on case insensitive file systems743else if (mode === 'move') {744return;745}746}747748// Here we have to see if the target to move/copy to exists or not.749// We need to respect the `overwrite` option to throw in case the750// target exists.751752const fromStat = await this.statIgnoreError(from);753if (!fromStat) {754throw createFileSystemProviderError(localize('fileMoveCopyErrorNotFound', "File to move/copy does not exist"), FileSystemProviderErrorCode.FileNotFound);755}756757const toStat = await this.statIgnoreError(to);758if (!toStat) {759return; // target does not exist so we are good760}761762if (!overwrite) {763throw createFileSystemProviderError(localize('fileMoveCopyErrorExists', "File at target already exists and thus will not be moved/copied to unless overwrite is specified"), FileSystemProviderErrorCode.FileExists);764}765766// Handle existing target for move/copy767if ((fromStat.type & FileType.File) !== 0 && (toStat.type & FileType.File) !== 0) {768return; // node.js can move/copy a file over an existing file without having to delete it first769} else {770await this.delete(to, { recursive: true, useTrash: false, atomic: false });771}772}773774//#endregion775776//#region Clone File777778async cloneFile(from: URI, to: URI): Promise<void> {779return this.doCloneFile(from, to, false /* optimistically assume parent folders exist */);780}781782private async doCloneFile(from: URI, to: URI, mkdir: boolean): Promise<void> {783const fromFilePath = this.toFilePath(from);784const toFilePath = this.toFilePath(to);785786const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);787if (isEqual(fromFilePath, toFilePath, !isPathCaseSensitive)) {788return; // cloning is only supported `from` and `to` are different files789}790791// Implement clone by using `fs.copyFile`, however setup locks792// for both `from` and `to` because node.js does not ensure793// this to be an atomic operation794795const locks = new DisposableStore();796797try {798locks.add(await this.createResourceLock(from));799locks.add(await this.createResourceLock(to));800801if (mkdir) {802await promises.mkdir(dirname(toFilePath), { recursive: true });803}804805await promises.copyFile(fromFilePath, toFilePath);806} catch (error) {807if (error.code === 'ENOENT' && !mkdir) {808return this.doCloneFile(from, to, true);809}810811throw this.toFileSystemProviderError(error);812} finally {813locks.dispose();814}815}816817//#endregion818819//#region File Watching820821protected createUniversalWatcher(822onChange: (changes: IFileChange[]) => void,823onLogMessage: (msg: ILogMessage) => void,824verboseLogging: boolean825): AbstractUniversalWatcherClient {826return new UniversalWatcherClient(changes => onChange(changes), msg => onLogMessage(msg), verboseLogging);827}828829protected createNonRecursiveWatcher(830onChange: (changes: IFileChange[]) => void,831onLogMessage: (msg: ILogMessage) => void,832verboseLogging: boolean833): AbstractNonRecursiveWatcherClient {834return new NodeJSWatcherClient(changes => onChange(changes), msg => onLogMessage(msg), verboseLogging);835}836837//#endregion838839//#region Helpers840841private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError {842if (error instanceof FileSystemProviderError) {843return error; // avoid double conversion844}845846let resultError: Error | string = error;847let code: FileSystemProviderErrorCode;848switch (error.code) {849case 'ENOENT':850code = FileSystemProviderErrorCode.FileNotFound;851break;852case 'EISDIR':853code = FileSystemProviderErrorCode.FileIsADirectory;854break;855case 'ENOTDIR':856code = FileSystemProviderErrorCode.FileNotADirectory;857break;858case 'EEXIST':859code = FileSystemProviderErrorCode.FileExists;860break;861case 'EPERM':862case 'EACCES':863code = FileSystemProviderErrorCode.NoPermissions;864break;865case 'ERR_UNC_HOST_NOT_ALLOWED':866resultError = `${error.message}. Please update the 'security.allowedUNCHosts' setting if you want to allow this host.`;867code = FileSystemProviderErrorCode.Unknown;868break;869default:870code = FileSystemProviderErrorCode.Unknown;871}872873return createFileSystemProviderError(resultError, code);874}875876private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {877let fileSystemProviderWriteError = this.toFileSystemProviderError(error);878879// If the write error signals permission issues, we try880// to read the file's mode to see if the file is write881// locked.882if (resource && fileSystemProviderWriteError.code === FileSystemProviderErrorCode.NoPermissions) {883try {884const { stat } = await SymlinkSupport.stat(this.toFilePath(resource));885if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {886fileSystemProviderWriteError = createFileSystemProviderError(error, FileSystemProviderErrorCode.FileWriteLocked);887}888} catch (error) {889this.logService.trace(error); // ignore - return original error890}891}892893return fileSystemProviderWriteError;894}895896//#endregion897}898899900