/** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @flow * @format */ import fs from 'fs'; import fsPlus from 'fs-plus'; import globLib from 'glob'; import mkdirpLib from 'mkdirp'; import rimraf from 'rimraf'; import temp from 'temp'; import nuclideUri from './nuclideUri'; import {runCommand} from './process'; /** * Create a temp directory with given prefix. The caller is responsible for cleaning up the * drectory. * @param prefix optinal prefix for the temp directory name. * @return path to a temporary directory. */ function tempdir(prefix: string = ''): Promise { return new Promise((resolve, reject) => { temp.mkdir(prefix, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } /** * @return path to a temporary file. The caller is responsible for cleaning up * the file. */ function tempfile(options: any): Promise { return new Promise((resolve, reject) => { temp.open(options, (err, info) => { if (err) { reject(err); } else { fs.close(info.fd, closeErr => { if (closeErr) { reject(closeErr); } else { resolve(info.path); } }); } }); }); } /** * Searches upward through the filesystem from pathToDirectory to find a file with * fileName. * @param fileName The name of the file to find. * @param pathToDirectory Where to begin the search. Must be a path to a directory, * not a file. * @return directory that contains the nearest file or null. */ async function findNearestFile( fileName: string, pathToDirectory: string, ): Promise { // TODO(5586355): If this becomes a bottleneck, we should consider memoizing // this function. The downside would be that if someone added a closer file // with fileName to pathToFile (or deleted the one that was cached), then we // would have a bug. This would probably be pretty rare, though. let currentPath = nuclideUri.resolve(pathToDirectory); for (;;) { const fileToFind = nuclideUri.join(currentPath, fileName); // eslint-disable-next-line no-await-in-loop const hasFile = await exists(fileToFind); if (hasFile) { return currentPath; } if (nuclideUri.isRoot(currentPath)) { return null; } currentPath = nuclideUri.dirname(currentPath); } } /** * Searches upward through the filesystem from pathToDirectory to find the furthest * file with fileName. * @param fileName The name of the file to find. * @param pathToDirectory Where to begin the search. Must be a path to a directory, * not a file. * @param stopOnMissing Stop searching when we reach a directory without fileName. * @return directory that contains the furthest file or null. */ async function findFurthestFile( fileName: string, pathToDirectory: string, stopOnMissing: boolean = false, ): Promise { let currentPath = nuclideUri.resolve(pathToDirectory); let result = null; for (;;) { const fileToFind = nuclideUri.join(currentPath, fileName); // eslint-disable-next-line no-await-in-loop const hasFile = await exists(fileToFind); if ((!hasFile && stopOnMissing) || nuclideUri.isRoot(currentPath)) { return result; } else if (hasFile) { result = currentPath; } currentPath = nuclideUri.dirname(currentPath); } } function getCommonAncestorDirectory(filePaths: Array): string { let commonDirectoryPath = nuclideUri.dirname(filePaths[0]); while ( filePaths.some(filePath => !filePath.startsWith(commonDirectoryPath)) ) { commonDirectoryPath = nuclideUri.dirname(commonDirectoryPath); } return commonDirectoryPath; } function exists(filePath: string): Promise { return new Promise((resolve, reject) => { fs.exists(filePath, resolve); }); } /** * Runs the equivalent of `mkdir -p` with the given path. * * Like most implementations of mkdirp, if it fails, it is possible that * directories were created for some prefix of the given path. * @return true if the path was created; false if it already existed. */ async function mkdirp(filePath: string): Promise { const isExistingDirectory = await exists(filePath); if (isExistingDirectory) { return false; } else { return new Promise((resolve, reject) => { mkdirpLib(filePath, err => { if (err) { reject(err); } else { resolve(true); } }); }); } } /** * Removes directories even if they are non-empty. Does not fail if the directory doesn't exist. */ function rmdir(filePath: string): Promise { return new Promise((resolve, reject) => { rimraf(filePath, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } /** @return true only if we are sure directoryPath is on NFS. */ async function isNfs(entityPath: string): Promise { if (process.platform === 'linux' || process.platform === 'darwin') { try { const stdout = await runCommand('stat', [ '-f', '-L', '-c', '%T', entityPath, ]).toPromise(); return stdout.trim() === 'nfs'; } catch (err) { return false; } } else { // TODO Handle other platforms (windows?): t9917576. return false; } } function glob(pattern: string, options?: Object): Promise> { return new Promise((resolve, reject) => { globLib(pattern, options, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } async function isNonNfsDirectory(directoryPath: string): Promise { try { const stats = await stat(directoryPath); if (stats.isDirectory()) { return !await isNfs(directoryPath); } else { return false; } } catch (e) { // If the directory cannot be probed for whatever reason, just // indicate that this is not a valid candidate directory. // Typically this is ENOENT for missing directory. return false; } } /** * Promisified wrappers around fs-plus functions. */ function copy(source: string, dest: string): Promise { return new Promise((resolve, reject) => { fsPlus.copy(source, dest, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function move(source: string, dest: string): Promise { return new Promise((resolve, reject) => { fsPlus.move(source, dest, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } /** * TODO: the fs-plus `writeFile` implementation runs `mkdirp` first. * We should use `fs.writeFile` and have callsites explicitly opt-in to this behaviour. */ function writeFile( filename: string, data: Buffer | string, options?: Object | string, ): Promise { return new Promise((resolve, reject) => { fsPlus.writeFile(filename, data, options, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } /** * Promisified wrappers around fs functions. */ function chmod(path: string, mode: number | string): Promise { return new Promise((resolve, reject) => { fs.chmod(path, mode, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function chown(path: string, uid: number, gid: number): Promise { return new Promise((resolve, reject) => { fs.chown(path, uid, gid, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function lstat(path: string): Promise { return new Promise((resolve, reject) => { fs.lstat(path, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function mkdir(path: string, mode?: number): Promise { return new Promise((resolve, reject) => { fs.mkdir(path, mode, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } // `fs.readFile` returns a Buffer unless an encoding is specified. // This workaround is adapted from the Flow declarations. type ReadFileType = ((filename: string, encoding: string) => Promise) & (( filename: string, options: {encoding: string, flag?: string}, ) => Promise) & ((filename: string, options?: {flag?: string}) => Promise); const readFile: ReadFileType = (function(...args: Array) { return new Promise((resolve, reject) => { // $FlowIssue: spread operator doesn't preserve any-type fs.readFile(...args, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); }: any); function readdir(path: string): Promise> { return new Promise((resolve, reject) => { fs.readdir(path, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function readlink(path: string): Promise { return new Promise((resolve, reject) => { fs.readlink(path, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function realpath(path: string, cache?: Object): Promise { return new Promise((resolve, reject) => { fs.realpath(path, cache, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function rename(oldPath: string, newPath: string): Promise { return new Promise((resolve, reject) => { fs.rename(oldPath, newPath, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function stat(path: string): Promise { return new Promise((resolve, reject) => { fs.stat(path, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function symlink(source: string, dest: string, type?: string): Promise { return new Promise((resolve, reject) => { fs.symlink(source, dest, type, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } function unlink(path: string): Promise { return new Promise((resolve, reject) => { fs.unlink(path, (err, result) => { if (err == null) { resolve(result); } else { reject(err); } }); }); } export default { tempdir, tempfile, findNearestFile, findFurthestFile, getCommonAncestorDirectory, exists, mkdirp, rmdir, isNfs, glob, isNonNfsDirectory, copy, move, writeFile, chmod, chown, lstat, mkdir, readFile, readdir, readlink, realpath, rename, stat, symlink, unlink, };