UNPKG

@pkerschbaum/code-oss-file-service

Version:

VS Code ([microsoft/vscode](https://github.com/microsoft/vscode)) includes a rich "`FileService`" and "`DiskFileSystemProvider`" abstraction built on top of Node.js core modules (`fs`, `path`) and Electron's `shell` module. This package allows to use that

664 lines (663 loc) 29.9 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Promises = exports.writeFileSync = exports.SymlinkSupport = exports.whenDeleted = exports.readdirSync = exports.rimrafSync = exports.RimRafMode = void 0; const fs = require("fs"); const stream = require("stream"); const os_1 = require("os"); const util_1 = require("util"); const async_1 = require("../../base/common/async"); const extpath_1 = require("../../base/common/extpath"); const normalization_1 = require("../../base/common/normalization"); const path_1 = require("../../base/common/path"); const platform_1 = require("../../base/common/platform"); const resources_1 = require("../../base/common/resources"); const uri_1 = require("../../base/common/uri"); //#region rimraf var RimRafMode; (function (RimRafMode) { /** * Slow version that unlinks each file and folder. */ RimRafMode[RimRafMode["UNLINK"] = 0] = "UNLINK"; /** * Fast version that first moves the file/folder * into a temp directory and then deletes that * without waiting for it. */ RimRafMode[RimRafMode["MOVE"] = 1] = "MOVE"; })(RimRafMode = exports.RimRafMode || (exports.RimRafMode = {})); /** * Allows to delete the provided path (either file or folder) recursively * with the options: * - `UNLINK`: direct removal from disk * - `MOVE`: faster variant that first moves the target to temp dir and then * deletes it in the background without waiting for that to finish. */ function rimraf(path, mode = RimRafMode.UNLINK) { return __awaiter(this, void 0, void 0, function* () { if ((0, extpath_1.isRootOrDriveLetter)(path)) { throw new Error('rimraf - will refuse to recursively delete root'); } /* * (modification for file-explorer https://github.com/pkerschbaum/file-explorer): * always use rimrafUnlink (since rimrafMove causes problems with some devices on windows, e.g. external drives, network drives) * * original code is here: // delete: via rmDir if (mode === RimRafMode.UNLINK) { return rimrafUnlink(path); } // delete: via move return rimrafMove(path); */ return rimrafUnlink(path); }); } function rimrafMove(path) { return __awaiter(this, void 0, void 0, function* () { try { const pathInTemp = (0, extpath_1.randomPath)((0, os_1.tmpdir)()); try { yield exports.Promises.rename(path, pathInTemp); } catch (error) { return rimrafUnlink(path); // if rename fails, delete without tmp dir } // Delete but do not return as promise rimrafUnlink(pathInTemp).catch(error => { }); } catch (error) { if (error.code !== 'ENOENT') { throw error; } } }); } function rimrafUnlink(path) { return __awaiter(this, void 0, void 0, function* () { return exports.Promises.rm(path, { recursive: true, maxRetries: 3 }); }); } function rimrafSync(path) { if ((0, extpath_1.isRootOrDriveLetter)(path)) { throw new Error('rimraf - will refuse to recursively delete root'); } fs.rmdirSync(path, { recursive: true }); } exports.rimrafSync = rimrafSync; function readdir(path, options) { return __awaiter(this, void 0, void 0, function* () { return handleDirectoryChildren(yield (options ? safeReaddirWithFileTypes(path) : (0, util_1.promisify)(fs.readdir)(path))); }); } function safeReaddirWithFileTypes(path) { return __awaiter(this, void 0, void 0, function* () { try { return yield (0, util_1.promisify)(fs.readdir)(path, { withFileTypes: true }); } catch (error) { console.warn('[node.js fs] readdir with filetypes failed with error: ', error); } // Fallback to manually reading and resolving each // children of the folder in case we hit an error // previously. // This can only really happen on exotic file systems // such as explained in #115645 where we get entries // from `readdir` that we can later not `lstat`. const result = []; const children = yield readdir(path); for (const child of children) { let isFile = false; let isDirectory = false; let isSymbolicLink = false; try { const lstat = yield exports.Promises.lstat((0, path_1.join)(path, child)); isFile = lstat.isFile(); isDirectory = lstat.isDirectory(); isSymbolicLink = lstat.isSymbolicLink(); } catch (error) { console.warn('[node.js fs] unexpected error from lstat after readdir: ', error); } result.push({ name: child, isFile: () => isFile, isDirectory: () => isDirectory, isSymbolicLink: () => isSymbolicLink }); } return result; }); } /** * Drop-in replacement of `fs.readdirSync` with support * for converting from macOS NFD unicon form to NFC * (https://github.com/nodejs/node/issues/2165) */ function readdirSync(path) { return handleDirectoryChildren(fs.readdirSync(path)); } exports.readdirSync = readdirSync; function handleDirectoryChildren(children) { return children.map(child => { // Mac: uses NFD unicode form on disk, but we want NFC // See also https://github.com/nodejs/node/issues/2165 if (typeof child === 'string') { return platform_1.isMacintosh ? (0, normalization_1.normalizeNFC)(child) : child; } child.name = platform_1.isMacintosh ? (0, normalization_1.normalizeNFC)(child.name) : child.name; return child; }); } /** * A convenience method to read all children of a path that * are directories. */ function readDirsInDir(dirPath) { return __awaiter(this, void 0, void 0, function* () { const children = yield readdir(dirPath); const directories = []; for (const child of children) { if (yield SymlinkSupport.existsDirectory((0, path_1.join)(dirPath, child))) { directories.push(child); } } return directories; }); } //#endregion //#region whenDeleted() /** * A `Promise` that resolves when the provided `path` * is deleted from disk. */ function whenDeleted(path, intervalMs = 1000) { return new Promise(resolve => { let running = false; const interval = setInterval(() => { if (!running) { running = true; fs.access(path, err => { running = false; if (err) { clearInterval(interval); resolve(undefined); } }); } }, intervalMs); }); } exports.whenDeleted = whenDeleted; //#endregion //#region Methods with symbolic links support var SymlinkSupport; (function (SymlinkSupport) { /** * Resolves the `fs.Stats` of the provided path. If the path is a * symbolic link, the `fs.Stats` will be from the target it points * to. If the target does not exist, `dangling: true` will be returned * as `symbolicLink` value. */ function stat(path) { return __awaiter(this, void 0, void 0, function* () { // First stat the link let lstats; try { lstats = yield exports.Promises.lstat(path); // Return early if the stat is not a symbolic link at all if (!lstats.isSymbolicLink()) { return { stat: lstats }; } } catch (error) { /* ignore - use stat() instead */ } // If the stat is a symbolic link or failed to stat, use fs.stat() // which for symbolic links will stat the target they point to try { const stats = yield exports.Promises.stat(path); return { stat: stats, symbolicLink: (lstats === null || lstats === void 0 ? void 0 : lstats.isSymbolicLink()) ? { dangling: false } : undefined }; } catch (error) { // If the link points to a nonexistent file we still want // to return it as result while setting dangling: true flag if (error.code === 'ENOENT' && lstats) { return { stat: lstats, symbolicLink: { dangling: true } }; } // Windows: workaround a node.js bug where reparse points // are not supported (https://github.com/nodejs/node/issues/36790) if (platform_1.isWindows && error.code === 'EACCES') { try { const stats = yield exports.Promises.stat(yield exports.Promises.readlink(path)); return { stat: stats, symbolicLink: { dangling: false } }; } catch (error) { // If the link points to a nonexistent file we still want // to return it as result while setting dangling: true flag if (error.code === 'ENOENT' && lstats) { return { stat: lstats, symbolicLink: { dangling: true } }; } throw error; } } throw error; } }); } SymlinkSupport.stat = stat; /** * Figures out if the `path` exists and is a file with support * for symlinks. * * Note: this will return `false` for a symlink that exists on * disk but is dangling (pointing to a nonexistent path). * * Use `exists` if you only care about the path existing on disk * or not without support for symbolic links. */ function existsFile(path) { return __awaiter(this, void 0, void 0, function* () { try { const { stat, symbolicLink } = yield SymlinkSupport.stat(path); return stat.isFile() && (symbolicLink === null || symbolicLink === void 0 ? void 0 : symbolicLink.dangling) !== true; } catch (error) { // Ignore, path might not exist } return false; }); } SymlinkSupport.existsFile = existsFile; /** * Figures out if the `path` exists and is a directory with support for * symlinks. * * Note: this will return `false` for a symlink that exists on * disk but is dangling (pointing to a nonexistent path). * * Use `exists` if you only care about the path existing on disk * or not without support for symbolic links. */ function existsDirectory(path) { return __awaiter(this, void 0, void 0, function* () { try { const { stat, symbolicLink } = yield SymlinkSupport.stat(path); return stat.isDirectory() && (symbolicLink === null || symbolicLink === void 0 ? void 0 : symbolicLink.dangling) !== true; } catch (error) { // Ignore, path might not exist } return false; }); } SymlinkSupport.existsDirectory = existsDirectory; })(SymlinkSupport = exports.SymlinkSupport || (exports.SymlinkSupport = {})); //#endregion //#region Write File // According to node.js docs (https://nodejs.org/docs/v14.16.0/api/fs.html#fs_fs_writefile_file_data_options_callback) // it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return. // Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly. const writeQueues = new async_1.ResourceQueue(); function writeFile(path, data, options) { return writeQueues.queueFor(uri_1.URI.file(path), resources_1.extUriBiasedIgnorePathCase).queue(() => { const ensuredOptions = ensureWriteOptions(options); return new Promise((resolve, reject) => doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve())); }); } let canFlush = true; // Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk // We do this in cases where we want to make sure the data is really on disk and // not in some cache. // // See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194 function doWriteFileAndFlush(path, data, options, callback) { if (!canFlush) { return fs.writeFile(path, data, { mode: options.mode, flag: options.flag }, callback); } // Open the file with same flags and mode as fs.writeFile() fs.open(path, options.flag, options.mode, (openError, fd) => { if (openError) { return callback(openError); } // It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open! fs.writeFile(fd, data, writeError => { if (writeError) { return fs.close(fd, () => callback(writeError)); // still need to close the handle on error! } // Flush contents (not metadata) of the file to disk // https://github.com/microsoft/vscode/issues/9589 fs.fdatasync(fd, (syncError) => { // In some exotic setups it is well possible that node fails to sync // In that case we disable flushing and warn to the console if (syncError) { console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError); canFlush = false; } return fs.close(fd, closeError => callback(closeError)); }); }); }); } /** * Same as `fs.writeFileSync` but with an additional call to * `fs.fdatasyncSync` after writing to ensure changes are * flushed to disk. */ function writeFileSync(path, data, options) { const ensuredOptions = ensureWriteOptions(options); if (!canFlush) { return fs.writeFileSync(path, data, { mode: ensuredOptions.mode, flag: ensuredOptions.flag }); } // Open the file with same flags and mode as fs.writeFile() const fd = fs.openSync(path, ensuredOptions.flag, ensuredOptions.mode); try { // It is valid to pass a fd handle to fs.writeFile() and this will keep the handle open! fs.writeFileSync(fd, data); // Flush contents (not metadata) of the file to disk try { fs.fdatasyncSync(fd); // https://github.com/microsoft/vscode/issues/9589 } catch (syncError) { console.warn('[node.js fs] fdatasyncSync is now disabled for this session because it failed: ', syncError); canFlush = false; } } finally { fs.closeSync(fd); } } exports.writeFileSync = writeFileSync; function ensureWriteOptions(options) { if (!options) { return { mode: 0o666 /* default node.js mode for files */, flag: 'w' }; } return { mode: typeof options.mode === 'number' ? options.mode : 0o666 /* default node.js mode for files */, flag: typeof options.flag === 'string' ? options.flag : 'w' }; } //#endregion //#region Move / Copy /** * A drop-in replacement for `fs.rename` that: * - updates the `mtime` of the `source` after the operation * - allows to move across multiple disks */ function move(source, target, coordinationArgs) { return __awaiter(this, void 0, void 0, function* () { if (source === target) { return; // simulate node.js behaviour here and do a no-op if paths match } /* * (modification for file-explorer https://github.com/pkerschbaum/file-explorer): * report that the progress for this source is indeterminate (`Promises.rename` does not allow to track progress) */ if (coordinationArgs === null || coordinationArgs === void 0 ? void 0 : coordinationArgs.reportProgress) { coordinationArgs.reportProgress({ forSource: uri_1.URI.file(source), progressDeterminateType: 'INDETERMINATE' }); } // We have been updating `mtime` for move operations for files since the // beginning for reasons that are no longer quite clear, but changing // this could be risky as well. As such, trying to reason about it: // It is very common as developer to have file watchers enabled that watch // the current workspace for changes. Updating the `mtime` might make it // easier for these watchers to recognize an actual change. Since changing // a source code file also updates the `mtime`, moving a file should do so // as well because conceptually it is a change of a similar category. function updateMtime(path) { return __awaiter(this, void 0, void 0, function* () { try { const stat = yield exports.Promises.lstat(path); if (stat.isDirectory() || stat.isSymbolicLink()) { return; // only for files } yield exports.Promises.utimes(path, stat.atime, new Date()); } catch (error) { // Ignore any error } }); } try { yield exports.Promises.rename(source, target); yield updateMtime(target); } catch (error) { // In two cases we fallback to classic copy and delete: // // 1.) The EXDEV error indicates that source and target are on different devices // In this case, fallback to using a copy() operation as there is no way to // rename() between different devices. // // 2.) The user tries to rename a file/folder that ends with a dot. This is not // really possible to move then, at least on UNC devices. if (source.toLowerCase() !== target.toLowerCase() && error.code === 'EXDEV' || source.endsWith('.')) { yield copy(source, target, { preserveSymlinks: false /* copying to another device */ }, coordinationArgs); yield rimraf(source, RimRafMode.MOVE); yield updateMtime(target); } else { throw error; } } }); } /** * Recursively copies all of `source` to `target`. * * The options `preserveSymlinks` configures how symbolic * links should be handled when encountered. Set to * `false` to not preserve them and `true` otherwise. */ function copy(source, target, options, coordinationArgs) { return __awaiter(this, void 0, void 0, function* () { return doCopy(source, target, { root: { source, target }, options, handledSourcePaths: new Set() }, coordinationArgs); }); } // When copying a file or folder, we want to preserve the mode // it had and as such provide it when creating. However, modes // can go beyond what we expect (see link below), so we mask it. // (https://github.com/nodejs/node-v0.x-archive/issues/3045#issuecomment-4862588) const COPY_MODE_MASK = 0o777; function doCopy(source, target, payload, coordinationArgs) { return __awaiter(this, void 0, void 0, function* () { // Keep track of paths already copied to prevent // cycles from symbolic links to cause issues if (payload.handledSourcePaths.has(source)) { return; } else { payload.handledSourcePaths.add(source); } const { stat, symbolicLink } = yield SymlinkSupport.stat(source); // Symlink if (symbolicLink) { // Try to re-create the symlink unless `preserveSymlinks: false` if (payload.options.preserveSymlinks) { try { return yield doCopySymlink(source, target, payload); } catch (error) { // in any case of an error fallback to normal copy via dereferencing console.warn('[node.js fs] copy of symlink failed: ', error); } } if (symbolicLink.dangling) { return; // skip dangling symbolic links from here on (https://github.com/microsoft/vscode/issues/111621) } } // Folder if (stat.isDirectory()) { return doCopyDirectory(source, target, stat.mode & COPY_MODE_MASK, payload, coordinationArgs); } // File or file-like else { return doCopyFile(source, target, stat.mode & COPY_MODE_MASK, coordinationArgs); } }); } function doCopyDirectory(source, target, mode, payload, coordinationArgs) { return __awaiter(this, void 0, void 0, function* () { // Create folder yield exports.Promises.mkdir(target, { recursive: true, mode }); // Copy each file recursively const files = yield readdir(source); for (const file of files) { yield doCopy((0, path_1.join)(source, file), (0, path_1.join)(target, file), payload, coordinationArgs); } }); } const COPY_FILE_HIGHWATERMARK = 1024 * 1024; // 1 MB function doCopyFile(source, target, mode, coordinationArgs) { var _a; return __awaiter(this, void 0, void 0, function* () { /* * (modification for file-explorer https://github.com/pkerschbaum/file-explorer): * - report that the progress for this source is determinate now * - if cancellation token got cancelled, abort * - instead of using fs.promises.copyFile, copy the file manually in order to be able to track * the progress on byte level */ if (coordinationArgs === null || coordinationArgs === void 0 ? void 0 : coordinationArgs.reportProgress) { coordinationArgs.reportProgress({ forSource: uri_1.URI.file(source), progressDeterminateType: 'DETERMINATE' }); } if (!!((_a = coordinationArgs === null || coordinationArgs === void 0 ? void 0 : coordinationArgs.token) === null || _a === void 0 ? void 0 : _a.isCancellationRequested)) { return; } return new Promise((resolve, reject) => { const reader = fs.createReadStream(source, { highWaterMark: COPY_FILE_HIGHWATERMARK }); const writer = fs.createWriteStream(target, { mode, highWaterMark: COPY_FILE_HIGHWATERMARK }); const progressWatcher = new stream.Transform({ highWaterMark: COPY_FILE_HIGHWATERMARK, transform(chunk, _, callback) { var _a; if (coordinationArgs === null || coordinationArgs === void 0 ? void 0 : coordinationArgs.reportProgress) { coordinationArgs.reportProgress({ forSource: sourceUri, newBytesRead: chunk.byteLength }); } if (!!((_a = coordinationArgs === null || coordinationArgs === void 0 ? void 0 : coordinationArgs.token) === null || _a === void 0 ? void 0 : _a.isCancellationRequested)) { callback(new Error(`aborted doCopyFile due to cancellation`)); } else { callback(undefined, chunk); } } }); const sourceUri = uri_1.URI.file(source); let finished = false; const finish = (error) => { if (!finished) { finished = true; reader.destroy(); writer.destroy(); progressWatcher.destroy(); // in error cases, pass to callback if (error) { return reject(error); } // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 fs.chmod(target, mode, error => error ? reject(error) : resolve()); } }; // handle errors properly reader.once('error', error => finish(error)); writer.once('error', error => finish(error)); // we are done (underlying fd has been closed) writer.once('close', () => finish()); progressWatcher.once('error', error => finish(error)); // start piping reader.pipe(progressWatcher).pipe(writer); }); }); } function doCopySymlink(source, target, payload) { return __awaiter(this, void 0, void 0, function* () { // Figure out link target let linkTarget = yield exports.Promises.readlink(source); // Special case: the symlink points to a target that is // actually within the path that is being copied. In that // case we want the symlink to point to the target and // not the source if ((0, extpath_1.isEqualOrParent)(linkTarget, payload.root.source, !platform_1.isLinux)) { linkTarget = (0, path_1.join)(payload.root.target, linkTarget.substr(payload.root.source.length + 1)); } // Create symlink yield exports.Promises.symlink(linkTarget, target); }); } //#endregion //#region Promise based fs methods /** * Prefer this helper class over the `fs.promises` API to * enable `graceful-fs` to function properly. Given issue * https://github.com/isaacs/node-graceful-fs/issues/160 it * is evident that the module only takes care of the non-promise * based fs methods. * * Another reason is `realpath` being entirely different in * the promise based implementation compared to the other * one (https://github.com/microsoft/vscode/issues/118562) * * Note: using getters for a reason, since `graceful-fs` * patching might kick in later after modules have been * loaded we need to defer access to fs methods. * (https://github.com/microsoft/vscode/issues/124176) */ exports.Promises = new class { //#region Implemented by node.js get access() { return (0, util_1.promisify)(fs.access); } get stat() { return (0, util_1.promisify)(fs.stat); } get lstat() { return (0, util_1.promisify)(fs.lstat); } get utimes() { return (0, util_1.promisify)(fs.utimes); } get read() { return (0, util_1.promisify)(fs.read); } get readFile() { return (0, util_1.promisify)(fs.readFile); } get write() { return (0, util_1.promisify)(fs.write); } get appendFile() { return (0, util_1.promisify)(fs.appendFile); } get fdatasync() { return (0, util_1.promisify)(fs.fdatasync); } get truncate() { return (0, util_1.promisify)(fs.truncate); } get rename() { return (0, util_1.promisify)(fs.rename); } get copyFile() { return (0, util_1.promisify)(fs.copyFile); } get open() { return (0, util_1.promisify)(fs.open); } get close() { return (0, util_1.promisify)(fs.close); } get symlink() { return (0, util_1.promisify)(fs.symlink); } get readlink() { return (0, util_1.promisify)(fs.readlink); } get chmod() { return (0, util_1.promisify)(fs.chmod); } get mkdir() { return (0, util_1.promisify)(fs.mkdir); } get unlink() { return (0, util_1.promisify)(fs.unlink); } get rmdir() { return (0, util_1.promisify)(fs.rmdir); } get rm() { return (0, util_1.promisify)(fs.rm); } get realpath() { return (0, util_1.promisify)(fs.realpath); } //#endregion //#region Implemented by us exists(path) { return __awaiter(this, void 0, void 0, function* () { try { yield exports.Promises.access(path); return true; } catch (_a) { return false; } }); } get readdir() { return readdir; } get readDirsInDir() { return readDirsInDir; } get writeFile() { return writeFile; } get rimraf() { return rimraf; } get move() { return move; } get copy() { return copy; } }; //#endregion //# sourceMappingURL=pfs.js.map