/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; import { tmpdir } from 'os'; import { join } from 'vs/base/common/path'; import { Queue } from 'vs/base/common/async'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; import { promisify } from 'util'; import { isRootOrDriveLetter } from 'vs/base/common/extpath'; import { generateUuid } from 'vs/base/common/uuid'; import { normalizeNFC } from 'vs/base/common/normalization'; // See https://github.com/microsoft/vscode/issues/30180 const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB // See https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149 const WIN32_MAX_HEAP_SIZE = 700 * 1024 * 1024; // 700 MB const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE; export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE; export enum RimRafMode { /** * Slow version that unlinks each file and folder. */ UNLINK, /** * Fast version that first moves the file/folder * into a temp directory and then deletes that * without waiting for it. */ MOVE } export async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { if (isRootOrDriveLetter(path)) { throw new Error('rimraf - will refuse to recursively delete root'); } // delete: via unlink if (mode === RimRafMode.UNLINK) { return rimrafUnlink(path); } // delete: via move return rimrafMove(path); } async function rimrafUnlink(path: string): Promise { try { const stat = await lstat(path); // Folder delete (recursive) - NOT for symbolic links though! if (stat.isDirectory() && !stat.isSymbolicLink()) { // Children const children = await readdir(path); await Promise.all(children.map(child => rimrafUnlink(join(path, child)))); // Folder await promisify(fs.rmdir)(path); } // Single file delete else { // chmod as needed to allow for unlink const mode = stat.mode; if (!(mode & fs.constants.S_IWUSR)) { await chmod(path, mode | fs.constants.S_IWUSR); } return unlink(path); } } catch (error) { if (error.code !== 'ENOENT') { throw error; } } } async function rimrafMove(path: string): Promise { try { const pathInTemp = join(tmpdir(), generateUuid()); try { await 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) { if (error.code !== 'ENOENT') { throw error; } } } export function rimrafSync(path: string): void { if (isRootOrDriveLetter(path)) { throw new Error('rimraf - will refuse to recursively delete root'); } try { const stat = fs.lstatSync(path); // Folder delete (recursive) - NOT for symbolic links though! if (stat.isDirectory() && !stat.isSymbolicLink()) { // Children const children = readdirSync(path); children.map(child => rimrafSync(join(path, child))); // Folder fs.rmdirSync(path); } // Single file delete else { // chmod as needed to allow for unlink const mode = stat.mode; if (!(mode & fs.constants.S_IWUSR)) { fs.chmodSync(path, mode | fs.constants.S_IWUSR); } return fs.unlinkSync(path); } } catch (error) { if (error.code !== 'ENOENT') { throw error; } } } export async function readdir(path: string): Promise { return handleDirectoryChildren(await promisify(fs.readdir)(path)); } export async function readdirWithFileTypes(path: string): Promise { const children = await promisify(fs.readdir)(path, { withFileTypes: true }); // Mac: uses NFD unicode form on disk, but we want NFC // See also https://github.com/nodejs/node/issues/2165 if (isMacintosh) { for (const child of children) { child.name = normalizeNFC(child.name); } } return children; } export function readdirSync(path: string): string[] { return handleDirectoryChildren(fs.readdirSync(path)); } function handleDirectoryChildren(children: string[]): string[] { // Mac: uses NFD unicode form on disk, but we want NFC // See also https://github.com/nodejs/node/issues/2165 if (isMacintosh) { return children.map(child => normalizeNFC(child)); } return children; } export function exists(path: string): Promise { return promisify(fs.exists)(path); } export function chmod(path: string, mode: number): Promise { return promisify(fs.chmod)(path, mode); } export function stat(path: string): Promise { return promisify(fs.stat)(path); } export interface IStatAndLink { // The stats of the file. If the file is a symbolic // link, the stats will be of that target file and // not the link itself. // If the file is a symbolic link pointing to a non // existing file, the stat will be of the link and // the `dangling` flag will indicate this. stat: fs.Stats; // Will be provided if the resource is a symbolic link // on disk. Use the `dangling` flag to find out if it // points to a resource that does not exist on disk. symbolicLink?: { dangling: boolean }; } export async function statLink(path: string): Promise { // First stat the link let lstats: fs.Stats | undefined; try { lstats = await 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 = await stat(path); return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; } catch (error) { // If the link points to a non-existing 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 (isWindows && error.code === 'EACCES' && lstats) { try { const stats = await stat(await readlink(path)); return { stat: stats, symbolicLink: lstats.isSymbolicLink() ? { dangling: false } : undefined }; } catch (error) { // If the link points to a non-existing file we still want // to return it as result while setting dangling: true flag if (error.code === 'ENOENT') { return { stat: lstats, symbolicLink: { dangling: true } }; } throw error; } } throw error; } } export function lstat(path: string): Promise { return promisify(fs.lstat)(path); } export function rename(oldPath: string, newPath: string): Promise { return promisify(fs.rename)(oldPath, newPath); } export function renameIgnoreError(oldPath: string, newPath: string): Promise { return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve())); } export function readlink(path: string): Promise { return promisify(fs.readlink)(path); } export function unlink(path: string): Promise { return promisify(fs.unlink)(path); } export function symlink(target: string, path: string, type?: string): Promise { return promisify(fs.symlink)(target, path, type); } export function truncate(path: string, len: number): Promise { return promisify(fs.truncate)(path, len); } export function readFile(path: string): Promise; export function readFile(path: string, encoding: string): Promise; export function readFile(path: string, encoding?: string): Promise { return promisify(fs.readFile)(path, encoding); } export async function mkdirp(path: string, mode?: number): Promise { return promisify(fs.mkdir)(path, { mode, recursive: true }); } // According to node.js docs (https://nodejs.org/docs/v6.5.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 writeFilePathQueues: Map> = new Map(); export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise { const queueKey = toQueueKey(path); return ensureWriteFileQueue(queueKey).queue(() => { const ensuredOptions = ensureWriteOptions(options); return new Promise((resolve, reject) => doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve())); }); } function toQueueKey(path: string): string { let queueKey = path; if (isWindows || isMacintosh) { queueKey = queueKey.toLowerCase(); // accommodate for case insensitive file systems } return queueKey; } function ensureWriteFileQueue(queueKey: string): Queue { const existingWriteFileQueue = writeFilePathQueues.get(queueKey); if (existingWriteFileQueue) { return existingWriteFileQueue; } const writeFileQueue = new Queue(); writeFilePathQueues.set(queueKey, writeFileQueue); const onFinish = Event.once(writeFileQueue.onFinished); onFinish(() => { writeFilePathQueues.delete(queueKey); writeFileQueue.dispose(); }); return writeFileQueue; } export interface IWriteFileOptions { mode?: number; flag?: string; } interface IEnsuredWriteFileOptions extends IWriteFileOptions { mode: number; flag: string; } 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: string, data: string | Buffer | Uint8Array, options: IEnsuredWriteFileOptions, callback: (error: Error | null) => void): void { 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 fs.fdatasync(fd, (syncError: Error | null) => { // 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)); }); }); }); } export function writeFileSync(path: string, data: string | Buffer, options?: IWriteFileOptions): void { 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); } catch (syncError) { console.warn('[node.js fs] fdatasyncSync is now disabled for this session because it failed: ', syncError); canFlush = false; } } finally { fs.closeSync(fd); } } function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptions { 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' }; } export async function readDirsInDir(dirPath: string): Promise { const children = await readdir(dirPath); const directories: string[] = []; for (const child of children) { if (await dirExists(join(dirPath, child))) { directories.push(child); } } return directories; } export async function dirExists(path: string): Promise { try { const { stat, symbolicLink } = await statLink(path); return stat.isDirectory() && symbolicLink?.dangling !== true; } catch (error) { // Ignore, path might not exist } return false; } export async function fileExists(path: string): Promise { try { const { stat, symbolicLink } = await statLink(path); return stat.isFile() && symbolicLink?.dangling !== true; } catch (error) { // Ignore, path might not exist } return false; } export function whenDeleted(path: string): Promise { // Complete when wait marker file is deleted return new Promise(resolve => { let running = false; const interval = setInterval(() => { if (!running) { running = true; fs.exists(path, exists => { running = false; if (!exists) { clearInterval(interval); resolve(undefined); } }); } }, 1000); }); } export async function move(source: string, target: string): Promise { if (source === target) { return; } async function updateMtime(path: string): Promise { const stat = await lstat(path); if (stat.isDirectory() || stat.isSymbolicLink()) { return; // only for files } const fd = await promisify(fs.open)(path, 'a'); try { await promisify(fs.futimes)(fd, stat.atime, new Date()); } catch (error) { //ignore } return promisify(fs.close)(fd); } try { await rename(source, target); await 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('.')) { await copy(source, target); await rimraf(source, RimRafMode.MOVE); await updateMtime(target); } else { throw error; } } } // 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) // // The `copy` method is very old so we should probably revisit // it's implementation and check wether this mask is still needed. const COPY_MODE_MASK = 0o777; export async function copy(source: string, target: string, handledSourcesIn?: { [path: string]: boolean }): Promise { // Keep track of paths already copied to prevent // cycles from symbolic links to cause issues const handledSources = handledSourcesIn ?? Object.create(null); if (handledSources[source]) { return; } else { handledSources[source] = true; } const { stat, symbolicLink } = await statLink(source); if (symbolicLink?.dangling) { return; // skip over dangling symbolic links (https://github.com/microsoft/vscode/issues/111621) } if (!stat.isDirectory()) { return doCopyFile(source, target, stat.mode & COPY_MODE_MASK); } // Create folder await mkdirp(target, stat.mode & COPY_MODE_MASK); // Copy each file recursively const files = await readdir(source); for (let i = 0; i < files.length; i++) { const file = files[i]; await copy(join(source, file), join(target, file), handledSources); } } async function doCopyFile(source: string, target: string, mode: number): Promise { return new Promise((resolve, reject) => { const reader = fs.createReadStream(source); const writer = fs.createWriteStream(target, { mode }); let finished = false; const finish = (error?: Error) => { if (!finished) { finished = true; // 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()); // start piping reader.pipe(writer); }); }