Path: blob/main/src/vs/platform/files/node/diskFileSystemProvider.ts
3296 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, 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 { ILogService } from '../../log/common/log.js';24import { AbstractDiskFileSystemProvider, IDiskFileSystemProviderOptions } from '../common/diskFileSystemProvider.js';25import { UniversalWatcherClient } from './watcher/watcherClient.js';26import { NodeJSWatcherClient } from './watcher/nodejs/nodejsClient.js';2728export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider implements29IFileSystemProviderWithFileReadWriteCapability,30IFileSystemProviderWithOpenReadWriteCloseCapability,31IFileSystemProviderWithFileReadStreamCapability,32IFileSystemProviderWithFileFolderCopyCapability,33IFileSystemProviderWithFileAtomicReadCapability,34IFileSystemProviderWithFileAtomicWriteCapability,35IFileSystemProviderWithFileAtomicDeleteCapability,36IFileSystemProviderWithFileCloneCapability,37IFileSystemProviderWithFileRealpathCapability {3839private static TRACE_LOG_RESOURCE_LOCKS = false; // not enabled by default because very spammy4041constructor(42logService: ILogService,43options?: IDiskFileSystemProviderOptions44) {45super(logService, options);46}4748//#region File Capabilities4950readonly onDidChangeCapabilities = Event.None;5152private _capabilities: FileSystemProviderCapabilities | undefined;53get capabilities(): FileSystemProviderCapabilities {54if (!this._capabilities) {55this._capabilities =56FileSystemProviderCapabilities.FileReadWrite |57FileSystemProviderCapabilities.FileOpenReadWriteClose |58FileSystemProviderCapabilities.FileReadStream |59FileSystemProviderCapabilities.FileFolderCopy |60FileSystemProviderCapabilities.FileWriteUnlock |61FileSystemProviderCapabilities.FileAtomicRead |62FileSystemProviderCapabilities.FileAtomicWrite |63FileSystemProviderCapabilities.FileAtomicDelete |64FileSystemProviderCapabilities.FileClone |65FileSystemProviderCapabilities.FileRealpath;6667if (isLinux) {68this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;69}70}7172return this._capabilities;73}7475//#endregion7677//#region File Metadata Resolving7879async stat(resource: URI): Promise<IStat> {80try {81const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly8283return {84type: this.toType(stat, symbolicLink),85ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time86mtime: stat.mtime.getTime(),87size: stat.size,88permissions: (stat.mode & 0o200) === 0 ? FilePermission.Locked : undefined89};90} catch (error) {91throw this.toFileSystemProviderError(error);92}93}9495private async statIgnoreError(resource: URI): Promise<IStat | undefined> {96try {97return await this.stat(resource);98} catch (error) {99return undefined;100}101}102103async realpath(resource: URI): Promise<string> {104const filePath = this.toFilePath(resource);105106return Promises.realpath(filePath);107}108109async readdir(resource: URI): Promise<[string, FileType][]> {110try {111const children = await Promises.readdir(this.toFilePath(resource), { withFileTypes: true });112113const result: [string, FileType][] = [];114await Promise.all(children.map(async child => {115try {116let type: FileType;117if (child.isSymbolicLink()) {118type = (await this.stat(joinPath(resource, child.name))).type; // always resolve target the link points to if any119} else {120type = this.toType(child);121}122123result.push([child.name, type]);124} catch (error) {125this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied126}127}));128129return result;130} catch (error) {131throw this.toFileSystemProviderError(error);132}133}134135private toType(entry: Stats | IDirent, symbolicLink?: { dangling: boolean }): FileType {136137// Signal file type by checking for file / directory, except:138// - symbolic links pointing to nonexistent files are FileType.Unknown139// - files that are neither file nor directory are FileType.Unknown140let type: FileType;141if (symbolicLink?.dangling) {142type = FileType.Unknown;143} else if (entry.isFile()) {144type = FileType.File;145} else if (entry.isDirectory()) {146type = FileType.Directory;147} else {148type = FileType.Unknown;149}150151// Always signal symbolic link as file type additionally152if (symbolicLink) {153type |= FileType.SymbolicLink;154}155156return type;157}158159//#endregion160161//#region File Reading/Writing162163private readonly resourceLocks = new ResourceMap<Barrier>(resource => extUriBiasedIgnorePathCase.getComparisonKey(resource));164165private async createResourceLock(resource: URI): Promise<IDisposable> {166const filePath = this.toFilePath(resource);167this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - request to acquire resource lock (${filePath})`);168169// Await pending locks for resource. It is possible for a new lock being170// added right after opening, so we have to loop over locks until no lock171// remains.172let existingLock: Barrier | undefined = undefined;173while (existingLock = this.resourceLocks.get(resource)) {174this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - waiting for resource lock to be released (${filePath})`);175await existingLock.wait();176}177178// Store new179const newLock = new Barrier();180this.resourceLocks.set(resource, newLock);181182this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - new resource lock created (${filePath})`);183184return toDisposable(() => {185this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock dispose() (${filePath})`);186187// Delete lock if it is still ours188if (this.resourceLocks.get(resource) === newLock) {189this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock removed from resource-lock map (${filePath})`);190this.resourceLocks.delete(resource);191}192193// Open lock194this.traceLock(`[Disk FileSystemProvider]: createResourceLock() - resource lock barrier open() (${filePath})`);195newLock.open();196});197}198199async readFile(resource: URI, options?: IFileAtomicReadOptions): Promise<Uint8Array> {200let lock: IDisposable | undefined = undefined;201try {202if (options?.atomic) {203this.traceLock(`[Disk FileSystemProvider]: atomic read operation started (${this.toFilePath(resource)})`);204205// When the read should be atomic, make sure206// to await any pending locks for the resource207// and lock for the duration of the read.208lock = await this.createResourceLock(resource);209}210211const filePath = this.toFilePath(resource);212213return await promises.readFile(filePath);214} catch (error) {215throw this.toFileSystemProviderError(error);216} finally {217lock?.dispose();218}219}220221private traceLock(msg: string): void {222if (DiskFileSystemProvider.TRACE_LOG_RESOURCE_LOCKS) {223this.logService.trace(msg);224}225}226227readFileStream(resource: URI, opts: IFileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {228const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);229230readFileIntoStream(this, resource, stream, data => data.buffer, {231...opts,232bufferSize: 256 * 1024 // read into chunks of 256kb each to reduce IPC overhead233}, token);234235return stream;236}237238async writeFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {239if (opts?.atomic !== false && opts?.atomic?.postfix && await this.canWriteFileAtomic(resource)) {240return this.doWriteFileAtomic(resource, joinPath(resourcesDirname(resource), `${resourcesBasename(resource)}${opts.atomic.postfix}`), content, opts);241} else {242return this.doWriteFile(resource, content, opts);243}244}245246private async canWriteFileAtomic(resource: URI): Promise<boolean> {247try {248const filePath = this.toFilePath(resource);249const { symbolicLink } = await SymlinkSupport.stat(filePath);250if (symbolicLink) {251// atomic writes are unsupported for symbolic links because252// we need to ensure that the `rename` operation is atomic253// and that only works if the link is on the same disk.254// Since we do not know where the symbolic link points to255// we refuse to write atomically.256return false;257}258} catch (error) {259// ignore stat errors here and just proceed trying to write260}261262return true; // atomic writing supported263}264265private async doWriteFileAtomic(resource: URI, tempResource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {266267// Ensure to create locks for all resources involved268// since atomic write involves mutiple disk operations269// and resources.270271const locks = new DisposableStore();272273try {274locks.add(await this.createResourceLock(resource));275locks.add(await this.createResourceLock(tempResource));276277// Write to temp resource first278await this.doWriteFile(tempResource, content, opts, true /* disable write lock */);279280try {281282// Rename over existing to ensure atomic replace283await this.rename(tempResource, resource, { overwrite: true });284285} catch (error) {286287// Cleanup in case of rename error288try {289await this.delete(tempResource, { recursive: false, useTrash: false, atomic: false });290} catch (error) {291// ignore - we want the outer error to bubble up292}293294throw error;295}296} finally {297locks.dispose();298}299}300301private async doWriteFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions, disableWriteLock?: boolean): Promise<void> {302let handle: number | undefined = undefined;303try {304const filePath = this.toFilePath(resource);305306// Validate target unless { create: true, overwrite: true }307if (!opts.create || !opts.overwrite) {308const fileExists = await Promises.exists(filePath);309if (fileExists) {310if (!opts.overwrite) {311throw createFileSystemProviderError(localize('fileExists', "File already exists"), FileSystemProviderErrorCode.FileExists);312}313} else {314if (!opts.create) {315throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);316}317}318}319320// Open321handle = await this.open(resource, { create: true, unlock: opts.unlock }, disableWriteLock);322323// Write content at once324await this.write(handle, 0, content, 0, content.byteLength);325} catch (error) {326throw await this.toFileSystemProviderWriteError(resource, error);327} finally {328if (typeof handle === 'number') {329await this.close(handle);330}331}332}333334private readonly mapHandleToPos = new Map<number, number>();335private readonly mapHandleToLock = new Map<number, IDisposable>();336337private readonly writeHandles = new Map<number, URI>();338339private static canFlush: boolean = true;340341static configureFlushOnWrite(enabled: boolean): void {342DiskFileSystemProvider.canFlush = enabled;343}344345async open(resource: URI, opts: IFileOpenOptions, disableWriteLock?: boolean): Promise<number> {346const filePath = this.toFilePath(resource);347348// Writes: guard multiple writes to the same resource349// behind a single lock to prevent races when writing350// from multiple places at the same time to the same file351let lock: IDisposable | undefined = undefined;352if (isFileOpenForWriteOptions(opts) && !disableWriteLock) {353lock = await this.createResourceLock(resource);354}355356let fd: number | undefined = undefined;357try {358359// Determine whether to unlock the file (write only)360if (isFileOpenForWriteOptions(opts) && opts.unlock) {361try {362const { stat } = await SymlinkSupport.stat(filePath);363if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {364await promises.chmod(filePath, stat.mode | 0o200);365}366} catch (error) {367if (error.code !== 'ENOENT') {368this.logService.trace(error); // log errors but do not give up writing369}370}371}372373// Windows gets special treatment (write only)374if (isWindows && isFileOpenForWriteOptions(opts)) {375try {376377// We try to use 'r+' for opening (which will fail if the file does not exist)378// to prevent issues when saving hidden files or preserving alternate data379// streams.380// Related issues:381// - https://github.com/microsoft/vscode/issues/931382// - https://github.com/microsoft/vscode/issues/6363383fd = await Promises.open(filePath, 'r+');384385// The flag 'r+' will not truncate the file, so we have to do this manually386await Promises.ftruncate(fd, 0);387} catch (error) {388if (error.code !== 'ENOENT') {389this.logService.trace(error); // log errors but do not give up writing390}391392// Make sure to close the file handle if we have one393if (typeof fd === 'number') {394try {395await Promises.close(fd);396} catch (error) {397this.logService.trace(error); // log errors but do not give up writing398}399400// Reset `fd` to be able to try again with 'w'401fd = undefined;402}403}404}405406if (typeof fd !== 'number') {407fd = await Promises.open(filePath, isFileOpenForWriteOptions(opts) ?408// We take `opts.create` as a hint that the file is opened for writing409// as such we use 'w' to truncate an existing or create the410// file otherwise. we do not allow reading.411'w' :412// Otherwise we assume the file is opened for reading413// as such we use 'r' to neither truncate, nor create414// the file.415'r'416);417}418419} catch (error) {420421// Release lock because we have no valid handle422// if we did open a lock during this operation423lock?.dispose();424425// Rethrow as file system provider error426if (isFileOpenForWriteOptions(opts)) {427throw await this.toFileSystemProviderWriteError(resource, error);428} else {429throw this.toFileSystemProviderError(error);430}431}432433// Remember this handle to track file position of the handle434// we init the position to 0 since the file descriptor was435// just created and the position was not moved so far (see436// also http://man7.org/linux/man-pages/man2/open.2.html -437// "The file offset is set to the beginning of the file.")438this.mapHandleToPos.set(fd, 0);439440// remember that this handle was used for writing441if (isFileOpenForWriteOptions(opts)) {442this.writeHandles.set(fd, resource);443}444445if (lock) {446const previousLock = this.mapHandleToLock.get(fd);447448// Remember that this handle has an associated lock449this.traceLock(`[Disk FileSystemProvider]: open() - storing lock for handle ${fd} (${filePath})`);450this.mapHandleToLock.set(fd, lock);451452// There is a slight chance that a resource lock for a453// handle was not yet disposed when we acquire a new454// lock, so we must ensure to dispose the previous lock455// before storing a new one for the same handle, other456// wise we end up in a deadlock situation457// https://github.com/microsoft/vscode/issues/142462458if (previousLock) {459this.traceLock(`[Disk FileSystemProvider]: open() - disposing a previous lock that was still stored on same handle ${fd} (${filePath})`);460previousLock.dispose();461}462}463464return fd;465}466467async close(fd: number): Promise<void> {468469// It is very important that we keep any associated lock470// for the file handle before attempting to call `fs.close(fd)`471// because of a possible race condition: as soon as a file472// handle is released, the OS may assign the same handle to473// the next `fs.open` call and as such it is possible that our474// lock is getting overwritten475const lockForHandle = this.mapHandleToLock.get(fd);476477try {478479// Remove this handle from map of positions480this.mapHandleToPos.delete(fd);481482// If a handle is closed that was used for writing, ensure483// to flush the contents to disk if possible.484if (this.writeHandles.delete(fd) && DiskFileSystemProvider.canFlush) {485try {486await Promises.fdatasync(fd); // https://github.com/microsoft/vscode/issues/9589487} catch (error) {488// In some exotic setups it is well possible that node fails to sync489// In that case we disable flushing and log the error to our logger490DiskFileSystemProvider.configureFlushOnWrite(false);491this.logService.error(error);492}493}494495return await Promises.close(fd);496} catch (error) {497throw this.toFileSystemProviderError(error);498} finally {499if (lockForHandle) {500if (this.mapHandleToLock.get(fd) === lockForHandle) {501this.traceLock(`[Disk FileSystemProvider]: close() - resource lock removed from handle-lock map ${fd}`);502this.mapHandleToLock.delete(fd); // only delete from map if this is still our lock!503}504505this.traceLock(`[Disk FileSystemProvider]: close() - disposing lock for handle ${fd}`);506lockForHandle.dispose();507}508}509}510511async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {512const normalizedPos = this.normalizePos(fd, pos);513514let bytesRead: number | null = null;515try {516bytesRead = (await Promises.read(fd, data, offset, length, normalizedPos)).bytesRead;517} catch (error) {518throw this.toFileSystemProviderError(error);519} finally {520this.updatePos(fd, normalizedPos, bytesRead);521}522523return bytesRead;524}525526private normalizePos(fd: number, pos: number): number | null {527528// When calling fs.read/write we try to avoid passing in the "pos" argument and529// rather prefer to pass in "null" because this avoids an extra seek(pos)530// call that in some cases can even fail (e.g. when opening a file over FTP -531// see https://github.com/microsoft/vscode/issues/73884).532//533// as such, we compare the passed in position argument with our last known534// position for the file descriptor and use "null" if they match.535if (pos === this.mapHandleToPos.get(fd)) {536return null;537}538539return pos;540}541542private updatePos(fd: number, pos: number | null, bytesLength: number | null): void {543const lastKnownPos = this.mapHandleToPos.get(fd);544if (typeof lastKnownPos === 'number') {545546// pos !== null signals that previously a position was used that is547// not null. node.js documentation explains, that in this case548// the internal file pointer is not moving and as such we do not move549// our position pointer.550//551// Docs: "If position is null, data will be read from the current file position,552// and the file position will be updated. If position is an integer, the file position553// will remain unchanged."554if (typeof pos === 'number') {555// do not modify the position556}557558// bytesLength = number is a signal that the read/write operation was559// successful and as such we need to advance the position in the Map560//561// Docs (http://man7.org/linux/man-pages/man2/read.2.html):562// "On files that support seeking, the read operation commences at the563// file offset, and the file offset is incremented by the number of564// bytes read."565//566// Docs (http://man7.org/linux/man-pages/man2/write.2.html):567// "For a seekable file (i.e., one to which lseek(2) may be applied, for568// example, a regular file) writing takes place at the file offset, and569// the file offset is incremented by the number of bytes actually570// written."571else if (typeof bytesLength === 'number') {572this.mapHandleToPos.set(fd, lastKnownPos + bytesLength);573}574575// bytesLength = null signals an error in the read/write operation576// and as such we drop the handle from the Map because the position577// is unspecificed at this point.578else {579this.mapHandleToPos.delete(fd);580}581}582}583584async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {585586// We know at this point that the file to write to is truncated and thus empty587// if the write now fails, the file remains empty. as such we really try hard588// to ensure the write succeeds by retrying up to three times.589return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);590}591592private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {593const normalizedPos = this.normalizePos(fd, pos);594595let bytesWritten: number | null = null;596try {597bytesWritten = (await Promises.write(fd, data, offset, length, normalizedPos)).bytesWritten;598} catch (error) {599throw await this.toFileSystemProviderWriteError(this.writeHandles.get(fd), error);600} finally {601this.updatePos(fd, normalizedPos, bytesWritten);602}603604return bytesWritten;605}606607//#endregion608609//#region Move/Copy/Delete/Create Folder610611async mkdir(resource: URI): Promise<void> {612try {613await promises.mkdir(this.toFilePath(resource));614} catch (error) {615throw this.toFileSystemProviderError(error);616}617}618619async delete(resource: URI, opts: IFileDeleteOptions): Promise<void> {620try {621const filePath = this.toFilePath(resource);622if (opts.recursive) {623let rmMoveToPath: string | undefined = undefined;624if (opts?.atomic !== false && opts.atomic.postfix) {625rmMoveToPath = join(dirname(filePath), `${basename(filePath)}${opts.atomic.postfix}`);626}627628await Promises.rm(filePath, RimRafMode.MOVE, rmMoveToPath);629} else {630try {631await promises.unlink(filePath);632} catch (unlinkError) {633634// `fs.unlink` will throw when used on directories635// we try to detect this error and then see if the636// provided resource is actually a directory. in that637// case we use `fs.rmdir` to delete the directory.638639if (unlinkError.code === 'EPERM' || unlinkError.code === 'EISDIR') {640let isDirectory = false;641try {642const { stat, symbolicLink } = await SymlinkSupport.stat(filePath);643isDirectory = stat.isDirectory() && !symbolicLink;644} catch (statError) {645// ignore646}647648if (isDirectory) {649await promises.rmdir(filePath);650} else {651throw unlinkError;652}653} else {654throw unlinkError;655}656}657}658} catch (error) {659throw this.toFileSystemProviderError(error);660}661}662663async rename(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {664const fromFilePath = this.toFilePath(from);665const toFilePath = this.toFilePath(to);666667if (fromFilePath === toFilePath) {668return; // simulate node.js behaviour here and do a no-op if paths match669}670671try {672673// Validate the move operation can perform674await this.validateMoveCopy(from, to, 'move', opts.overwrite);675676// Rename677await Promises.rename(fromFilePath, toFilePath);678} catch (error) {679680// Rewrite some typical errors that can happen especially around symlinks681// to something the user can better understand682if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {683error = new Error(localize('moveError', "Unable to move '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));684}685686throw this.toFileSystemProviderError(error);687}688}689690async copy(from: URI, to: URI, opts: IFileOverwriteOptions): Promise<void> {691const fromFilePath = this.toFilePath(from);692const toFilePath = this.toFilePath(to);693694if (fromFilePath === toFilePath) {695return; // simulate node.js behaviour here and do a no-op if paths match696}697698try {699700// Validate the copy operation can perform701await this.validateMoveCopy(from, to, 'copy', opts.overwrite);702703// Copy704await Promises.copy(fromFilePath, toFilePath, { preserveSymlinks: true });705} catch (error) {706707// Rewrite some typical errors that can happen especially around symlinks708// to something the user can better understand709if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {710error = new Error(localize('copyError', "Unable to copy '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));711}712713throw this.toFileSystemProviderError(error);714}715}716717private async validateMoveCopy(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<void> {718const fromFilePath = this.toFilePath(from);719const toFilePath = this.toFilePath(to);720721let isSameResourceWithDifferentPathCase = false;722const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);723if (!isPathCaseSensitive) {724isSameResourceWithDifferentPathCase = isEqual(fromFilePath, toFilePath, true /* ignore case */);725}726727if (isSameResourceWithDifferentPathCase) {728729// You cannot copy the same file to the same location with different730// path case unless you are on a case sensitive file system731if (mode === 'copy') {732throw createFileSystemProviderError(localize('fileCopyErrorPathCase', "File cannot be copied to same path with different path case"), FileSystemProviderErrorCode.FileExists);733}734735// You can move the same file to the same location with different736// path case on case insensitive file systems737else if (mode === 'move') {738return;739}740}741742// Here we have to see if the target to move/copy to exists or not.743// We need to respect the `overwrite` option to throw in case the744// target exists.745746const fromStat = await this.statIgnoreError(from);747if (!fromStat) {748throw createFileSystemProviderError(localize('fileMoveCopyErrorNotFound', "File to move/copy does not exist"), FileSystemProviderErrorCode.FileNotFound);749}750751const toStat = await this.statIgnoreError(to);752if (!toStat) {753return; // target does not exist so we are good754}755756if (!overwrite) {757throw createFileSystemProviderError(localize('fileMoveCopyErrorExists', "File at target already exists and thus will not be moved/copied to unless overwrite is specified"), FileSystemProviderErrorCode.FileExists);758}759760// Handle existing target for move/copy761if ((fromStat.type & FileType.File) !== 0 && (toStat.type & FileType.File) !== 0) {762return; // node.js can move/copy a file over an existing file without having to delete it first763} else {764await this.delete(to, { recursive: true, useTrash: false, atomic: false });765}766}767768//#endregion769770//#region Clone File771772async cloneFile(from: URI, to: URI): Promise<void> {773return this.doCloneFile(from, to, false /* optimistically assume parent folders exist */);774}775776private async doCloneFile(from: URI, to: URI, mkdir: boolean): Promise<void> {777const fromFilePath = this.toFilePath(from);778const toFilePath = this.toFilePath(to);779780const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);781if (isEqual(fromFilePath, toFilePath, !isPathCaseSensitive)) {782return; // cloning is only supported `from` and `to` are different files783}784785// Implement clone by using `fs.copyFile`, however setup locks786// for both `from` and `to` because node.js does not ensure787// this to be an atomic operation788789const locks = new DisposableStore();790791try {792locks.add(await this.createResourceLock(from));793locks.add(await this.createResourceLock(to));794795if (mkdir) {796await promises.mkdir(dirname(toFilePath), { recursive: true });797}798799await promises.copyFile(fromFilePath, toFilePath);800} catch (error) {801if (error.code === 'ENOENT' && !mkdir) {802return this.doCloneFile(from, to, true);803}804805throw this.toFileSystemProviderError(error);806} finally {807locks.dispose();808}809}810811//#endregion812813//#region File Watching814815protected createUniversalWatcher(816onChange: (changes: IFileChange[]) => void,817onLogMessage: (msg: ILogMessage) => void,818verboseLogging: boolean819): AbstractUniversalWatcherClient {820return new UniversalWatcherClient(changes => onChange(changes), msg => onLogMessage(msg), verboseLogging);821}822823protected createNonRecursiveWatcher(824onChange: (changes: IFileChange[]) => void,825onLogMessage: (msg: ILogMessage) => void,826verboseLogging: boolean827): AbstractNonRecursiveWatcherClient {828return new NodeJSWatcherClient(changes => onChange(changes), msg => onLogMessage(msg), verboseLogging);829}830831//#endregion832833//#region Helpers834835private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError {836if (error instanceof FileSystemProviderError) {837return error; // avoid double conversion838}839840let resultError: Error | string = error;841let code: FileSystemProviderErrorCode;842switch (error.code) {843case 'ENOENT':844code = FileSystemProviderErrorCode.FileNotFound;845break;846case 'EISDIR':847code = FileSystemProviderErrorCode.FileIsADirectory;848break;849case 'ENOTDIR':850code = FileSystemProviderErrorCode.FileNotADirectory;851break;852case 'EEXIST':853code = FileSystemProviderErrorCode.FileExists;854break;855case 'EPERM':856case 'EACCES':857code = FileSystemProviderErrorCode.NoPermissions;858break;859case 'ERR_UNC_HOST_NOT_ALLOWED':860resultError = `${error.message}. Please update the 'security.allowedUNCHosts' setting if you want to allow this host.`;861code = FileSystemProviderErrorCode.Unknown;862break;863default:864code = FileSystemProviderErrorCode.Unknown;865}866867return createFileSystemProviderError(resultError, code);868}869870private async toFileSystemProviderWriteError(resource: URI | undefined, error: NodeJS.ErrnoException): Promise<FileSystemProviderError> {871let fileSystemProviderWriteError = this.toFileSystemProviderError(error);872873// If the write error signals permission issues, we try874// to read the file's mode to see if the file is write875// locked.876if (resource && fileSystemProviderWriteError.code === FileSystemProviderErrorCode.NoPermissions) {877try {878const { stat } = await SymlinkSupport.stat(this.toFilePath(resource));879if (!(stat.mode & 0o200 /* File mode indicating writable by owner */)) {880fileSystemProviderWriteError = createFileSystemProviderError(error, FileSystemProviderErrorCode.FileWriteLocked);881}882} catch (error) {883this.logService.trace(error); // ignore - return original error884}885}886887return fileSystemProviderWriteError;888}889890//#endregion891}892893894