├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── spec ├── fixtures │ ├── Sample.markdown │ ├── Xample.md │ ├── binary-file.png │ ├── coffee.coffee │ ├── css.css │ ├── sample.js │ ├── sample.txt │ ├── test.cson │ └── test.json └── fs-plus-spec.coffee └── src └── fs-plus.mjs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | Test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: '14' 19 | - name: Install windows-build-tools 20 | if: ${{ matrix.os == 'windows-latest' }} 21 | run: npm config set msvs_version 2019 22 | - name: Install dependencies 23 | run: npm i 24 | - name: Run tests 25 | run: npm run test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | .DS_Store 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | *.coffee 3 | script/ 4 | .DS_Store 5 | npm-debug.log 6 | .travis.yml 7 | spec/ 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # fs plus 3 | [![CI](https://github.com/atom/fs-plus/actions/workflows/ci.yml/badge.svg)](https://github.com/atom/fs-plus/actions/workflows/ci.yml) 4 | 5 | Yet another filesystem helper based on node's [fs](http://nodejs.org/api/fs.html) 6 | module. This library exports everything from node's fs module but with some 7 | extra helpers. 8 | 9 | ## Using 10 | 11 | ```sh 12 | npm install fs-plus 13 | ``` 14 | 15 | ```coffee 16 | fs = require 'fs-plus' 17 | ``` 18 | 19 | ## Documentation 20 | 21 | ### `getHomeDirectory()` 22 | Returns the absolute path to the home directory. 23 | 24 | ### `absolute(relativePath)` 25 | Make the given path absolute by resolving it against the current 26 | working directory. 27 | 28 | ### Params 29 | 30 | - **String** `relativePath`: The string representing the relative path. If the 31 | path is prefixed with '~', it will be expanded to the current user's home 32 | directory. 33 | 34 | ### Return 35 | 36 | - **String**: The absolute path or the relative path if it's unable to 37 | determine its real path. 38 | 39 | ### `normalize(pathToNormalize)` 40 | Normalize the given path treating a leading `~` segment as referring to the 41 | home directory. This method does not query the filesystem. 42 | 43 | #### Params 44 | 45 | - **String** `pathToNormalize`: The string containing the abnormal path. If the 46 | path is prefixed with '~', it will be expanded to the current user's home 47 | directory. 48 | 49 | #### Return 50 | - **String** Returns a normalized path. 51 | 52 | ### `tildify(pathToTildify)` 53 | Convert an absolute path to tilde path on Linux and macOS: 54 | /Users/username/dev => ~/dev 55 | 56 | #### Params 57 | 58 | - **String** `pathToTildify`: The string containing the full path. 59 | 60 | #### Return 61 | - **String** Returns a tildified path. 62 | 63 | ### `getAppDataDirectory()` 64 | Get path to store application specific data. 65 | 66 | #### Return 67 | - **String** Returns the absolute path or null if platform isn't supported 68 | 69 | - macOS: `~/Library/Application Support/` 70 | - Windows: `%AppData%` 71 | - Linux: `/var/lib` 72 | 73 | ### `isAbsolute(pathToCheck)` 74 | Is the given path absolute? 75 | 76 | #### Params 77 | - **String** `pathToCheck`: The relative or absolute path to check. 78 | 79 | #### Return 80 | - **Bolean** Returns `true` if the path is absolute, `false` otherwise. 81 | 82 | ### `existsSync(pathToCheck)` 83 | Returns `true` if a file or folder at the specified path exists. 84 | 85 | ### `isDirectorySync(directoryPath)` 86 | Returns `true` if the given path exists and is a directory. 87 | 88 | ### `isDirectory(directoryPath)` 89 | Asynchronously checks that the given path exists and is a directory. 90 | 91 | ### `isFileSync(filePath)` 92 | Returns true if the specified path exists and is a file. 93 | 94 | ### `isSymbolicLinkSync(symlinkPath)` 95 | Returns `true` if the specified path is a symbolic link. 96 | 97 | ### `isSymbolicLink(symlinkPath, callback)` 98 | Calls back with `true` if the specified path is a symbolic link. 99 | 100 | ### `isExecutableSync(pathToCheck)` 101 | Returns `true` if the specified path is executable. 102 | 103 | ### `getSizeSync(pathToCheck)` 104 | Returns the size of the specified path. 105 | 106 | ### `listSync(rootPath, extensions)` 107 | Returns an Array with the paths of the files and directories 108 | contained within the directory path. It is not recursive. 109 | 110 | ## Params 111 | - **String** `rootPath`: The absolute path to the directory to list. 112 | - **Array** `extensions`: An array of extensions to filter the results by. If none are 113 | given, none are filtered (optional). 114 | 115 | ### `list(rootPath, extensions)` 116 | Asynchronously lists the files and directories in the given path. The listing is not recursive. 117 | 118 | ### `listTreeSync(rootPath)` 119 | Get all paths under the given path. 120 | 121 | #### Params 122 | - **String** `rootPath` The {String} path to start at. 123 | 124 | #### Return 125 | - **Array** Returns an array of strings under the given path. 126 | 127 | ### `moveSync(source, target)` 128 | Moves the file or directory to the target synchronously. 129 | 130 | ### `removeSync(pathToRemove)` 131 | Removes the file or directory at the given path synchronously. 132 | 133 | ### `writeFileSync(filePath, content, options)` 134 | Open, write, flush, and close a file, writing the given content synchronously. 135 | It also creates the necessary parent directories. 136 | 137 | ### `writeFile(filePath, content, options, callback)` 138 | Open, write, flush, and close a file, writing the given content 139 | asynchronously. 140 | It also creates the necessary parent directories. 141 | 142 | ### `copySync(sourcePath, destinationPath)` 143 | Copies the given path recursively and synchronously. 144 | 145 | ### `makeTreeSync(directoryPath)` 146 | Create a directory at the specified path including any missing 147 | parent directories synchronously. 148 | 149 | ### `makeTree(directoryPath, callback)` 150 | Create a directory at the specified path including any missing 151 | parent directories asynchronously. 152 | 153 | ### `traverseTreeSync(rootPath, onFile, onDirectory)` 154 | Recursively walk the given path and execute the given functions 155 | synchronously. 156 | 157 | #### Params 158 | - **String** `rootPath`: The string containing the directory to recurse into. 159 | - **Function** `onFile`: The function to execute on each file, receives a single argument 160 | the absolute path. 161 | - **Function** `onDirectory`: The function to execute on each directory, receives a single 162 | argument the absolute path (defaults to onFile). If this 163 | function returns a falsy value then the directory is not 164 | entered. 165 | 166 | ### `traverseTree(rootPath, onFile, onDirectory, onDone)` 167 | Public: Recursively walk the given path and execute the given functions 168 | asynchronously. 169 | 170 | ### `md5ForPath(pathToDigest)` 171 | Hashes the contents of the given file. 172 | 173 | #### Params 174 | - **String** `pathToDigest`: The string containing the absolute path. 175 | 176 | #### Return 177 | - **String** Returns a string containing the MD5 hexadecimal hash. 178 | 179 | ### `resolve(loadPaths, pathToResolve, extensions)` 180 | Finds a relative path among the given array of paths. 181 | 182 | #### Params 183 | - **Array** `loadPaths`: An array of absolute and relative paths to search. 184 | - **String** `pathToResolve` The string containing the path to resolve. 185 | - **Array** `extensions` An array of extensions to pass to {resolveExtensions} in 186 | which case pathToResolve should not contain an extension 187 | (optional). 188 | 189 | #### Return 190 | Returns the absolute path of the file to be resolved if it's found and 191 | undefined otherwise. 192 | 193 | ### `resolveOnLoadPath()` 194 | Like `.resolve` but uses node's modules paths as the load paths to 195 | search. 196 | 197 | ### `resolveExtension(pathToResolve, extensions)` 198 | Finds the first file in the given path which matches the extension 199 | in the order given. 200 | 201 | #### Params 202 | - **String** `pathToResolve`: the string containing relative or absolute path of the 203 | file in question without the extension or '.'. 204 | - **Array** `extensions`: the ordered array of extensions to try. 205 | 206 | #### Return 207 | Returns the absolute path of the file if it exists with any of the given 208 | extensions, otherwise it's undefined. 209 | 210 | ### `isCompressedExtension(ext)` 211 | Returns true for extensions associated with compressed files. 212 | 213 | ### `isImageExtension(ext)` 214 | Returns true for extensions associated with image files. 215 | 216 | ### `isPdfExtension(ext)` 217 | Returns true for extensions associated with pdf files. 218 | 219 | ### `isBinaryExtension(ext)` 220 | Returns true for extensions associated with binary files. 221 | 222 | ### `isReadmePath(readmePath)` 223 | Returns true for files named similarily to 'README' 224 | 225 | ### `isMarkdownExtension(ext)` 226 | Returns true for extensions associated with Markdown files. 227 | 228 | ### `isCaseInsensitive()` 229 | Is the filesystem case insensitive? 230 | Returns `true` if case insensitive, `false` otherwise. 231 | 232 | ### `isCaseSensitive()` 233 | Is the filesystem case sensitive? 234 | Returns `true` if case sensitive, `false` otherwise. 235 | 236 | ### `statSyncNoException(path[, options])` 237 | Calls [`fs.statSync`](https://nodejs.org/docs/latest-v10.x/api/fs.html#fs_fs_statsync_path_options), catching all exceptions raised. This method calls `fs.statSyncNoException` when provided by the underlying `fs` module (Electron < 3.0). 238 | Returns `fs.Stats` if the file exists, `false` otherwise. 239 | 240 | ### `lstatSyncNoException(path[, options])` 241 | Calls [`fs.lstatSync`](https://nodejs.org/docs/latest-v10.x/api/fs.html#fs_fs_lstatsync_path_options), catching all exceptions raised. This method calls `fs.lstatSyncNoException` when provided by the underlying `fs` module (Electron < 3.0). 242 | Returns `fs.Stats` if the file exists, `false` otherwise. 243 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | let presets = ["babel-preset-atomic"] 2 | 3 | let plugins = [] 4 | 5 | module.exports = { 6 | presets: presets, 7 | plugins: plugins, 8 | exclude: "node_modules/**", 9 | sourceMap: "inline", 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fs-plus", 3 | "version": "3.1.1", 4 | "description": "node's fs with more helpers", 5 | "main": "./lib/fs-plus.js", 6 | "scripts": { 7 | "clean": "shx rm -rf lib", 8 | "test": "jasmine-focused --captureExceptions --coffee spec", 9 | "babel": "npm run clean && cross-env NODE_ENV=development cross-env BABEL_KEEP_MODULES=false babel src --out-dir lib", 10 | "dev": "npm run clean && cross-env NODE_ENV=development cross-env BABEL_KEEP_MODULES=true rollup -c -w", 11 | "build": "npm run clean && cross-env NODE_ENV=production cross-env BABEL_KEEP_MODULES=true rollup -c ", 12 | "prepare": "npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/atom/fs-plus.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/atom/fs-plus/issues" 20 | }, 21 | "homepage": "http://atom.github.io/fs-plus", 22 | "license": "MIT", 23 | "keywords": [ 24 | "fs", 25 | "filesystem" 26 | ], 27 | "devDependencies": { 28 | "jasmine-focused": "1.x", 29 | "temp": "~0.8.1", 30 | "coffeelint": "^2.1.0", 31 | "rollup": "^2.18.2", 32 | "rollup-plugin-atomic": "^1.2.0", 33 | "@babel/cli": "7.10.3", 34 | "@babel/core": "7.10.3", 35 | "babel-preset-atomic": "^1.0.7", 36 | "shx": "^0.3.2", 37 | "cross-env": "^7.0.2" 38 | }, 39 | "dependencies": { 40 | "async": "^1.5.2", 41 | "mkdirp": "^0.5.1", 42 | "rimraf": "^2.5.2", 43 | "underscore-plus": "1.x" 44 | }, 45 | "coffeelintConfig": { 46 | "no_empty_param_list": { 47 | "level": "error" 48 | }, 49 | "max_line_length": { 50 | "level": "ignore" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { createPlugins } from "rollup-plugin-atomic" 2 | 3 | const plugins = createPlugins(["js", "babel"]) 4 | 5 | export default [ 6 | { 7 | input: "src/fs-plus.mjs", 8 | output: [ 9 | { 10 | dir: "lib", 11 | format: "cjs", 12 | sourcemap: true, 13 | }, 14 | ], 15 | // loaded externally 16 | external: ["atom"], 17 | plugins: plugins, 18 | }, 19 | ] 20 | -------------------------------------------------------------------------------- /spec/fixtures/Sample.markdown: -------------------------------------------------------------------------------- 1 | # Sample 2 | 3 | Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. 4 | -------------------------------------------------------------------------------- /spec/fixtures/Xample.md: -------------------------------------------------------------------------------- 1 | # Lorem Ipsum 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 4 | -------------------------------------------------------------------------------- /spec/fixtures/binary-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atom/fs-plus/ba56f01d4f434982dff734186b3b6522eb28de75/spec/fixtures/binary-file.png -------------------------------------------------------------------------------- /spec/fixtures/coffee.coffee: -------------------------------------------------------------------------------- 1 | a = => 'hello' 2 | -------------------------------------------------------------------------------- /spec/fixtures/css.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/sample.js: -------------------------------------------------------------------------------- 1 | var quicksort = function () { 2 | var sort = function(items) { 3 | if (items.length <= 1) return items; 4 | var pivot = items.shift(), current, left = [], right = []; 5 | while(items.length > 0) { 6 | current = items.shift(); 7 | current < pivot ? left.push(current) : right.push(current); 8 | } 9 | return sort(left).concat(pivot).concat(sort(right)); 10 | }; 11 | 12 | return sort(Array.apply(this, arguments)); 13 | }; -------------------------------------------------------------------------------- /spec/fixtures/sample.txt: -------------------------------------------------------------------------------- 1 | Some text. 2 | -------------------------------------------------------------------------------- /spec/fixtures/test.cson: -------------------------------------------------------------------------------- 1 | 'key': 'value' 2 | -------------------------------------------------------------------------------- /spec/fixtures/test.json: -------------------------------------------------------------------------------- 1 | {"key": "value"} 2 | -------------------------------------------------------------------------------- /spec/fs-plus-spec.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | temp = require 'temp' 3 | fs = require '../lib/fs-plus' 4 | 5 | temp.track() 6 | 7 | describe "fs", -> 8 | fixturesDir = path.join(__dirname, 'fixtures') 9 | sampleFile = path.join(fixturesDir, 'sample.js') 10 | linkToSampleFile = path.join(fixturesDir, 'link-to-sample.js') 11 | try 12 | fs.unlinkSync(linkToSampleFile) 13 | fs.symlinkSync(sampleFile, linkToSampleFile, 'junction') 14 | 15 | describe ".isFileSync(path)", -> 16 | it "returns true with a file path", -> 17 | expect(fs.isFileSync(path.join(fixturesDir, 'sample.js'))).toBe true 18 | 19 | it "returns false with a directory path", -> 20 | expect(fs.isFileSync(fixturesDir)).toBe false 21 | 22 | it "returns false with a non-existent path", -> 23 | expect(fs.isFileSync(path.join(fixturesDir, 'non-existent'))).toBe false 24 | expect(fs.isFileSync(null)).toBe false 25 | 26 | describe ".isSymbolicLinkSync(path)", -> 27 | it "returns true with a symbolic link path", -> 28 | expect(fs.isSymbolicLinkSync(linkToSampleFile)).toBe true 29 | 30 | it "returns false with a file path", -> 31 | expect(fs.isSymbolicLinkSync(sampleFile)).toBe false 32 | 33 | it "returns false with a non-existent path", -> 34 | expect(fs.isSymbolicLinkSync(path.join(fixturesDir, 'non-existent'))).toBe false 35 | expect(fs.isSymbolicLinkSync('')).toBe false 36 | expect(fs.isSymbolicLinkSync(null)).toBe false 37 | 38 | describe ".isSymbolicLink(path, callback)", -> 39 | it "calls back with true for a symbolic link path", -> 40 | callback = jasmine.createSpy('isSymbolicLink') 41 | fs.isSymbolicLink(linkToSampleFile, callback) 42 | waitsFor -> callback.callCount is 1 43 | runs -> expect(callback.mostRecentCall.args[0]).toBe true 44 | 45 | it "calls back with false for a file path", -> 46 | callback = jasmine.createSpy('isSymbolicLink') 47 | fs.isSymbolicLink(sampleFile, callback) 48 | waitsFor -> callback.callCount is 1 49 | runs -> expect(callback.mostRecentCall.args[0]).toBe false 50 | 51 | it "calls back with false for a non-existent path", -> 52 | callback = jasmine.createSpy('isSymbolicLink') 53 | 54 | fs.isSymbolicLink(path.join(fixturesDir, 'non-existent'), callback) 55 | waitsFor -> callback.callCount is 1 56 | runs -> 57 | expect(callback.mostRecentCall.args[0]).toBe false 58 | 59 | callback.reset() 60 | fs.isSymbolicLink('', callback) 61 | 62 | waitsFor -> callback.callCount is 1 63 | runs -> 64 | expect(callback.mostRecentCall.args[0]).toBe false 65 | 66 | callback.reset() 67 | fs.isSymbolicLink(null, callback) 68 | 69 | waitsFor -> callback.callCount is 1 70 | runs -> expect(callback.mostRecentCall.args[0]).toBe false 71 | 72 | describe ".existsSync(path)", -> 73 | it "returns true when the path exists", -> 74 | expect(fs.existsSync(fixturesDir)).toBe true 75 | 76 | it "returns false when the path doesn't exist", -> 77 | expect(fs.existsSync(path.join(fixturesDir, "-nope-does-not-exist"))).toBe false 78 | expect(fs.existsSync("")).toBe false 79 | expect(fs.existsSync(null)).toBe false 80 | 81 | describe ".remove(pathToRemove, callback)", -> 82 | tempDir = null 83 | 84 | beforeEach -> 85 | tempDir = temp.mkdirSync('fs-plus-') 86 | 87 | it "removes an existing file", -> 88 | filePath = path.join(tempDir, 'existing-file') 89 | fs.writeFileSync(filePath, '') 90 | 91 | done = false 92 | fs.remove filePath, -> 93 | done = true 94 | 95 | waitsFor -> 96 | done 97 | 98 | runs -> 99 | expect(fs.existsSync(filePath)).toBe false 100 | 101 | it "does nothing for a non-existent file", -> 102 | filePath = path.join(tempDir, 'non-existent-file') 103 | 104 | done = false 105 | fs.remove filePath, -> 106 | done = true 107 | 108 | waitsFor -> 109 | done 110 | 111 | runs -> 112 | expect(fs.existsSync(filePath)).toBe false 113 | 114 | it "removes a non-empty directory", -> 115 | directoryPath = path.join(tempDir, 'subdir') 116 | fs.makeTreeSync(path.join(directoryPath, 'subdir')) 117 | 118 | done = false 119 | fs.remove directoryPath, -> 120 | done = true 121 | 122 | waitsFor -> 123 | done 124 | 125 | runs -> 126 | expect(fs.existsSync(directoryPath)).toBe false 127 | 128 | describe ".makeTreeSync(path)", -> 129 | aPath = path.join(temp.dir, 'a') 130 | 131 | beforeEach -> 132 | fs.removeSync(aPath) if fs.existsSync(aPath) 133 | 134 | it "creates all directories in path including any missing parent directories", -> 135 | abcPath = path.join(aPath, 'b', 'c') 136 | fs.makeTreeSync(abcPath) 137 | expect(fs.isDirectorySync(abcPath)).toBeTruthy() 138 | 139 | it "throws an error when the provided path is a file", -> 140 | tempDir = temp.mkdirSync('fs-plus-') 141 | filePath = path.join(tempDir, 'file.txt') 142 | fs.writeFileSync(filePath, '') 143 | expect(fs.isFileSync(filePath)).toBe true 144 | 145 | makeTreeError = null 146 | 147 | try 148 | fs.makeTreeSync(filePath) 149 | catch error 150 | makeTreeError = error 151 | 152 | expect(makeTreeError.code).toBe 'EEXIST' 153 | expect(makeTreeError.path).toBe filePath 154 | 155 | describe ".makeTree(path)", -> 156 | aPath = path.join(temp.dir, 'a') 157 | 158 | beforeEach -> 159 | fs.removeSync(aPath) if fs.existsSync(aPath) 160 | 161 | it "creates all directories in path including any missing parent directories", -> 162 | callback = jasmine.createSpy('callback') 163 | abcPath = path.join(aPath, 'b', 'c') 164 | fs.makeTree(abcPath, callback) 165 | 166 | waitsFor -> 167 | callback.callCount is 1 168 | 169 | runs -> 170 | expect(callback.argsForCall[0][0]).toBeNull() 171 | expect(fs.isDirectorySync(abcPath)).toBeTruthy() 172 | 173 | fs.makeTree(abcPath, callback) 174 | 175 | waitsFor -> 176 | callback.callCount is 2 177 | 178 | runs -> 179 | expect(callback.argsForCall[1][0]).toBeUndefined() 180 | expect(fs.isDirectorySync(abcPath)).toBeTruthy() 181 | 182 | it "calls back with an error when the provided path is a file", -> 183 | callback = jasmine.createSpy('callback') 184 | tempDir = temp.mkdirSync('fs-plus-') 185 | filePath = path.join(tempDir, 'file.txt') 186 | fs.writeFileSync(filePath, '') 187 | expect(fs.isFileSync(filePath)).toBe true 188 | 189 | fs.makeTree(filePath, callback) 190 | 191 | waitsFor -> 192 | callback.callCount is 1 193 | 194 | runs -> 195 | expect(callback.argsForCall[0][0]).toBeTruthy() 196 | expect(callback.argsForCall[0][1]).toBeUndefined() 197 | expect(callback.argsForCall[0][0].code).toBe 'EEXIST' 198 | expect(callback.argsForCall[0][0].path).toBe filePath 199 | 200 | describe ".traverseTreeSync(path, onFile, onDirectory)", -> 201 | it "calls fn for every path in the tree at the given path", -> 202 | paths = [] 203 | onPath = (childPath) -> 204 | paths.push(childPath) 205 | true 206 | expect(fs.traverseTreeSync(fixturesDir, onPath, onPath)).toBeUndefined() 207 | expect(paths).toEqual fs.listTreeSync(fixturesDir) 208 | 209 | it "does not recurse into a directory if it is pruned", -> 210 | paths = [] 211 | onPath = (childPath) -> 212 | if childPath.match(/\/dir$/) 213 | false 214 | else 215 | paths.push(childPath) 216 | true 217 | fs.traverseTreeSync fixturesDir, onPath, onPath 218 | 219 | expect(paths.length).toBeGreaterThan 0 220 | for filePath in paths 221 | expect(filePath).not.toMatch /\/dir\// 222 | 223 | it "returns entries if path is a symlink", -> 224 | symlinkPath = path.join(fixturesDir, 'symlink-to-dir') 225 | symlinkPaths = [] 226 | onSymlinkPath = (path) -> symlinkPaths.push(path.substring(symlinkPath.length + 1)) 227 | 228 | regularPath = path.join(fixturesDir, 'dir') 229 | paths = [] 230 | onPath = (path) -> paths.push(path.substring(regularPath.length + 1)) 231 | 232 | fs.traverseTreeSync(symlinkPath, onSymlinkPath, onSymlinkPath) 233 | fs.traverseTreeSync(regularPath, onPath, onPath) 234 | 235 | expect(symlinkPaths).toEqual(paths) 236 | 237 | it "ignores missing symlinks", -> 238 | unless process.platform is 'win32' # Dir symlinks on Windows require admin 239 | directory = temp.mkdirSync('symlink-in-here') 240 | paths = [] 241 | onPath = (childPath) -> paths.push(childPath) 242 | fs.symlinkSync(path.join(directory, 'source'), path.join(directory, 'destination')) 243 | fs.traverseTreeSync(directory, onPath) 244 | expect(paths.length).toBe 0 245 | 246 | describe ".traverseTree(path, onFile, onDirectory, onDone)", -> 247 | it "calls fn for every path in the tree at the given path", -> 248 | paths = [] 249 | onPath = (childPath) -> 250 | paths.push(childPath) 251 | true 252 | done = false 253 | onDone = -> 254 | done = true 255 | fs.traverseTree fixturesDir, onPath, onPath, onDone 256 | 257 | waitsFor -> 258 | done 259 | 260 | runs -> 261 | expect(paths).toEqual fs.listTreeSync(fixturesDir) 262 | 263 | it "does not recurse into a directory if it is pruned", -> 264 | paths = [] 265 | onPath = (childPath) -> 266 | if childPath.match(/\/dir$/) 267 | false 268 | else 269 | paths.push(childPath) 270 | true 271 | done = false 272 | onDone = -> 273 | done = true 274 | 275 | fs.traverseTree fixturesDir, onPath, onPath, onDone 276 | 277 | waitsFor -> 278 | done 279 | 280 | runs -> 281 | expect(paths.length).toBeGreaterThan 0 282 | for filePath in paths 283 | expect(filePath).not.toMatch /\/dir\// 284 | 285 | it "returns entries if path is a symlink", -> 286 | symlinkPath = path.join(fixturesDir, 'symlink-to-dir') 287 | symlinkPaths = [] 288 | 289 | onSymlinkPath = (path) -> symlinkPaths.push(path.substring(symlinkPath.length + 1)) 290 | 291 | regularPath = path.join(fixturesDir, 'dir') 292 | paths = [] 293 | onPath = (path) -> paths.push(path.substring(regularPath.length + 1)) 294 | 295 | symlinkDone = false 296 | onSymlinkPathDone = -> 297 | symlinkDone = true 298 | 299 | regularDone = false 300 | onRegularPathDone = -> 301 | regularDone = true 302 | 303 | fs.traverseTree symlinkPath, onSymlinkPath, onSymlinkPath, onSymlinkPathDone 304 | fs.traverseTree regularPath, onPath, onPath, onRegularPathDone 305 | 306 | waitsFor -> 307 | symlinkDone && regularDone 308 | 309 | runs -> 310 | expect(symlinkPaths).toEqual(paths) 311 | 312 | it "ignores missing symlinks", -> 313 | directory = temp.mkdirSync('symlink-in-here') 314 | paths = [] 315 | onPath = (childPath) -> paths.push(childPath) 316 | fs.symlinkSync(path.join(directory, 'source'), path.join(directory, 'destination')) 317 | done = false 318 | onDone = -> 319 | done = true 320 | fs.traverseTree directory, onPath, onPath, onDone 321 | waitsFor -> 322 | done 323 | runs -> 324 | expect(paths.length).toBe 0 325 | 326 | describe ".traverseTree(path, onFile, onDirectory, onDone)", -> 327 | it "calls fn for every path in the tree at the given path", -> 328 | paths = [] 329 | onPath = (childPath) -> 330 | paths.push(childPath) 331 | true 332 | done = false 333 | onDone = -> 334 | done = true 335 | fs.traverseTree fixturesDir, onPath, onPath, onDone 336 | 337 | waitsFor -> 338 | done 339 | 340 | runs -> 341 | expect(paths).toEqual fs.listTreeSync(fixturesDir) 342 | 343 | it "does not recurse into a directory if it is pruned", -> 344 | paths = [] 345 | onPath = (childPath) -> 346 | if childPath.match(/\/dir$/) 347 | false 348 | else 349 | paths.push(childPath) 350 | true 351 | done = false 352 | onDone = -> 353 | done = true 354 | 355 | fs.traverseTree fixturesDir, onPath, onPath, onDone 356 | 357 | waitsFor -> 358 | done 359 | 360 | runs -> 361 | expect(paths.length).toBeGreaterThan 0 362 | for filePath in paths 363 | expect(filePath).not.toMatch /\/dir\// 364 | 365 | it "returns entries if path is a symlink", -> 366 | symlinkPath = path.join(fixturesDir, 'symlink-to-dir') 367 | symlinkPaths = [] 368 | 369 | onSymlinkPath = (path) -> symlinkPaths.push(path.substring(symlinkPath.length + 1)) 370 | 371 | regularPath = path.join(fixturesDir, 'dir') 372 | paths = [] 373 | onPath = (path) -> paths.push(path.substring(regularPath.length + 1)) 374 | 375 | symlinkDone = false 376 | onSymlinkPathDone = -> 377 | symlinkDone = true 378 | 379 | regularDone = false 380 | onRegularPathDone = -> 381 | regularDone = true 382 | 383 | fs.traverseTree symlinkPath, onSymlinkPath, onSymlinkPath, onSymlinkPathDone 384 | fs.traverseTree regularPath, onPath, onPath, onRegularPathDone 385 | 386 | waitsFor -> 387 | symlinkDone && regularDone 388 | 389 | runs -> 390 | expect(symlinkPaths).toEqual(paths) 391 | 392 | it "ignores missing symlinks", -> 393 | directory = temp.mkdirSync('symlink-in-here') 394 | paths = [] 395 | onPath = (childPath) -> paths.push(childPath) 396 | fs.symlinkSync(path.join(directory, 'source'), path.join(directory, 'destination')) 397 | done = false 398 | onDone = -> 399 | done = true 400 | fs.traverseTree directory, onPath, onPath, onDone 401 | waitsFor -> 402 | done 403 | runs -> 404 | expect(paths.length).toBe 0 405 | 406 | describe ".md5ForPath(path)", -> 407 | it "returns the MD5 hash of the file at the given path", -> 408 | expect(fs.md5ForPath(require.resolve('./fixtures/binary-file.png'))).toBe 'cdaad7483b17865b5f00728d189e90eb' 409 | 410 | describe ".list(path, extensions)", -> 411 | it "returns the absolute paths of entries within the given directory", -> 412 | paths = fs.listSync(fixturesDir) 413 | expect(paths).toContain path.join(fixturesDir, 'css.css') 414 | expect(paths).toContain path.join(fixturesDir, 'coffee.coffee') 415 | expect(paths).toContain path.join(fixturesDir, 'sample.txt') 416 | expect(paths).toContain path.join(fixturesDir, 'sample.js') 417 | expect(paths).toContain path.join(fixturesDir, 'binary-file.png') 418 | 419 | it "returns an empty array for paths that aren't directories or don't exist", -> 420 | expect(fs.listSync(path.join(fixturesDir, 'sample.js'))).toEqual [] 421 | expect(fs.listSync('/non/existent/directory')).toEqual [] 422 | 423 | it "can filter the paths by an optional array of file extensions", -> 424 | paths = fs.listSync(fixturesDir, ['.css', 'coffee']) 425 | expect(paths).toContain path.join(fixturesDir, 'css.css') 426 | expect(paths).toContain path.join(fixturesDir, 'coffee.coffee') 427 | expect(listedPath).toMatch /(css|coffee)$/ for listedPath in paths 428 | 429 | it "returns alphabetically sorted paths (lowercase first)", -> 430 | paths = fs.listSync(fixturesDir) 431 | sortedPaths = [ 432 | path.join(fixturesDir, 'binary-file.png') 433 | path.join(fixturesDir, 'coffee.coffee') 434 | path.join(fixturesDir, 'css.css') 435 | path.join(fixturesDir, 'link-to-sample.js') 436 | path.join(fixturesDir, 'sample.js') 437 | path.join(fixturesDir, 'Sample.markdown') 438 | path.join(fixturesDir, 'sample.txt') 439 | path.join(fixturesDir, 'test.cson') 440 | path.join(fixturesDir, 'test.json') 441 | path.join(fixturesDir, 'Xample.md') 442 | ] 443 | expect(sortedPaths).toEqual paths 444 | 445 | describe ".list(path, [extensions,] callback)", -> 446 | paths = null 447 | 448 | it "calls the callback with the absolute paths of entries within the given directory", -> 449 | done = false 450 | fs.list fixturesDir, (err, result) -> 451 | paths = result 452 | done = true 453 | 454 | waitsFor -> 455 | done 456 | 457 | runs -> 458 | expect(paths).toContain path.join(fixturesDir, 'css.css') 459 | expect(paths).toContain path.join(fixturesDir, 'coffee.coffee') 460 | expect(paths).toContain path.join(fixturesDir, 'sample.txt') 461 | expect(paths).toContain path.join(fixturesDir, 'sample.js') 462 | expect(paths).toContain path.join(fixturesDir, 'binary-file.png') 463 | 464 | it "can filter the paths by an optional array of file extensions", -> 465 | done = false 466 | fs.list fixturesDir, ['css', '.coffee'], (err, result) -> 467 | paths = result 468 | done = true 469 | 470 | waitsFor -> 471 | done 472 | 473 | runs -> 474 | expect(paths).toContain path.join(fixturesDir, 'css.css') 475 | expect(paths).toContain path.join(fixturesDir, 'coffee.coffee') 476 | expect(listedPath).toMatch /(css|coffee)$/ for listedPath in paths 477 | 478 | describe ".absolute(relativePath)", -> 479 | it "converts a leading ~ segment to the HOME directory", -> 480 | homeDir = fs.getHomeDirectory() 481 | expect(fs.absolute('~')).toBe fs.realpathSync(homeDir) 482 | expect(fs.absolute(path.join('~', 'does', 'not', 'exist'))).toBe path.join(homeDir, 'does', 'not', 'exist') 483 | expect(fs.absolute('~test')).toBe '~test' 484 | 485 | describe ".getAppDataDirectory", -> 486 | originalPlatform = null 487 | 488 | beforeEach -> 489 | originalPlatform = process.platform 490 | 491 | afterEach -> 492 | Object.defineProperty process, 'platform', value: originalPlatform 493 | 494 | it "returns the Application Support path on Mac", -> 495 | Object.defineProperty process, 'platform', value: 'darwin' 496 | unless process.env.HOME 497 | Object.defineProperty process.env, 'HOME', value: path.join(path.sep, 'Users', 'Buzz') 498 | expect(fs.getAppDataDirectory()).toBe path.join(fs.getHomeDirectory(), 'Library', 'Application Support') 499 | 500 | it "returns %AppData% on Windows", -> 501 | Object.defineProperty process, 'platform', value: 'win32' 502 | unless process.env.APPDATA 503 | Object.defineProperty process.env, 'APPDATA', value: 'C:\\Users\\test\\AppData\\Roaming' 504 | expect(fs.getAppDataDirectory()).toBe process.env.APPDATA 505 | 506 | it "returns /var/lib on linux", -> 507 | Object.defineProperty process, 'platform', value: 'linux' 508 | expect(fs.getAppDataDirectory()).toBe '/var/lib' 509 | 510 | it "returns null on other platforms", -> 511 | Object.defineProperty process, 'platform', value: 'foobar' 512 | expect(fs.getAppDataDirectory()).toBe null 513 | 514 | describe ".getSizeSync(pathToCheck)", -> 515 | it "returns the size of the file at the path", -> 516 | expect(fs.getSizeSync()).toBe -1 517 | expect(fs.getSizeSync('')).toBe -1 518 | expect(fs.getSizeSync(null)).toBe -1 519 | expect(fs.getSizeSync(path.join(fixturesDir, 'binary-file.png'))).toBe 392 520 | expect(fs.getSizeSync(path.join(fixturesDir, 'does.not.exist'))).toBe -1 521 | 522 | describe ".writeFileSync(filePath)", -> 523 | it "creates any missing parent directories", -> 524 | directory = temp.mkdirSync('fs-plus-') 525 | file = path.join(directory, 'a', 'b', 'c.txt') 526 | expect(fs.existsSync(path.dirname(file))).toBeFalsy() 527 | 528 | fs.writeFileSync(file, 'contents') 529 | expect(fs.readFileSync(file, 'utf8')).toBe 'contents' 530 | expect(fs.existsSync(path.dirname(file))).toBeTruthy() 531 | 532 | describe ".writeFile(filePath)", -> 533 | it "creates any missing parent directories", -> 534 | directory = temp.mkdirSync('fs-plus-') 535 | file = path.join(directory, 'a', 'b', 'c.txt') 536 | expect(fs.existsSync(path.dirname(file))).toBeFalsy() 537 | 538 | handler = jasmine.createSpy('writeFileHandler') 539 | fs.writeFile(file, 'contents', handler) 540 | 541 | waitsFor -> 542 | handler.callCount is 1 543 | 544 | runs -> 545 | expect(fs.readFileSync(file, 'utf8')).toBe 'contents' 546 | expect(fs.existsSync(path.dirname(file))).toBeTruthy() 547 | 548 | describe ".copySync(sourcePath, destinationPath)", -> 549 | [source, destination] = [] 550 | 551 | beforeEach -> 552 | source = temp.mkdirSync('fs-plus-') 553 | destination = temp.mkdirSync('fs-plus-') 554 | 555 | describe "with just files", -> 556 | beforeEach -> 557 | fs.writeFileSync(path.join(source, 'a.txt'), 'a') 558 | fs.copySync(source, destination) 559 | 560 | it "copies the file", -> 561 | expect(fs.isFileSync(path.join(destination, 'a.txt'))).toBeTruthy() 562 | 563 | describe "with folders and files", -> 564 | beforeEach -> 565 | fs.writeFileSync(path.join(source, 'a.txt'), 'a') 566 | fs.makeTreeSync(path.join(source, 'b')) 567 | fs.copySync(source, destination) 568 | 569 | it "copies the file and folder", -> 570 | expect(fs.isFileSync(path.join(destination, 'a.txt'))).toBeTruthy() 571 | expect(fs.isDirectorySync(path.join(destination, 'b'))).toBeTruthy() 572 | 573 | describe "source is copied into itself", -> 574 | beforeEach -> 575 | source = temp.mkdirSync('fs-plus-') 576 | destination = source 577 | fs.writeFileSync(path.join(source, 'a.txt'), 'a') 578 | fs.makeTreeSync(path.join(source, 'b')) 579 | fs.copySync(source, path.join(destination, path.basename(source))) 580 | 581 | it "copies the directory once", -> 582 | expect(fs.isDirectorySync(path.join(destination, path.basename(source)))).toBeTruthy() 583 | expect(fs.isDirectorySync(path.join(destination, path.basename(source), 'b'))).toBeTruthy() 584 | expect(fs.isDirectorySync(path.join(destination, path.basename(source), path.basename(source)))).toBeFalsy() 585 | 586 | describe ".copyFileSync(sourceFilePath, destinationFilePath)", -> 587 | it "copies the specified file", -> 588 | sourceFilePath = temp.path() 589 | destinationFilePath = path.join(temp.path(), '/unexisting-dir/foo.bar') 590 | content = '' 591 | content += 'ABCDE' for i in [0...20000] by 1 592 | fs.writeFileSync(sourceFilePath, content) 593 | fs.copyFileSync(sourceFilePath, destinationFilePath) 594 | expect(fs.readFileSync(destinationFilePath, 'utf8')).toBe(fs.readFileSync(sourceFilePath, 'utf8')) 595 | 596 | describe ".isCaseSensitive()/isCaseInsensitive()", -> 597 | it "does not return the same value for both", -> 598 | expect(fs.isCaseInsensitive()).not.toBe fs.isCaseSensitive() 599 | 600 | describe ".resolve(loadPaths, pathToResolve, extensions)", -> 601 | it "returns the resolved path or undefined if it does not exist", -> 602 | expect(fs.resolve(fixturesDir, 'sample.js')).toBe path.join(fixturesDir, 'sample.js') 603 | expect(fs.resolve(fixturesDir, 'sample', ['js'])).toBe path.join(fixturesDir, 'sample.js') 604 | expect(fs.resolve(fixturesDir, 'sample', ['abc', 'txt'])).toBe path.join(fixturesDir, 'sample.txt') 605 | expect(fs.resolve(fixturesDir)).toBe fixturesDir 606 | 607 | expect(fs.resolve()).toBeUndefined() 608 | expect(fs.resolve(fixturesDir, 'sample', ['badext'])).toBeUndefined() 609 | expect(fs.resolve(fixturesDir, 'doesnotexist.js')).toBeUndefined() 610 | expect(fs.resolve(fixturesDir, undefined)).toBeUndefined() 611 | expect(fs.resolve(fixturesDir, 3)).toBeUndefined() 612 | expect(fs.resolve(fixturesDir, false)).toBeUndefined() 613 | expect(fs.resolve(fixturesDir, null)).toBeUndefined() 614 | expect(fs.resolve(fixturesDir, '')).toBeUndefined() 615 | 616 | describe ".isAbsolute(pathToCheck)", -> 617 | originalPlatform = null 618 | 619 | beforeEach -> 620 | originalPlatform = process.platform 621 | 622 | afterEach -> 623 | Object.defineProperty process, 'platform', value: originalPlatform 624 | 625 | it "returns false when passed \\", -> 626 | expect(fs.isAbsolute('\\')).toBe false 627 | 628 | it "returns true when the path is absolute, false otherwise", -> 629 | Object.defineProperty process, 'platform', value: 'win32' 630 | 631 | expect(fs.isAbsolute()).toBe false 632 | expect(fs.isAbsolute(null)).toBe false 633 | expect(fs.isAbsolute('')).toBe false 634 | expect(fs.isAbsolute('test')).toBe false 635 | expect(fs.isAbsolute('a\\b')).toBe false 636 | expect(fs.isAbsolute('/a/b/c')).toBe false 637 | expect(fs.isAbsolute('\\\\server\\share')).toBe true 638 | expect(fs.isAbsolute('C:\\Drive')).toBe true 639 | 640 | Object.defineProperty process, 'platform', value: 'linux' 641 | 642 | expect(fs.isAbsolute()).toBe false 643 | expect(fs.isAbsolute(null)).toBe false 644 | expect(fs.isAbsolute('')).toBe false 645 | expect(fs.isAbsolute('test')).toBe false 646 | expect(fs.isAbsolute('a/b')).toBe false 647 | expect(fs.isAbsolute('\\\\server\\share')).toBe false 648 | expect(fs.isAbsolute('C:\\Drive')).toBe false 649 | expect(fs.isAbsolute('/')).toBe true 650 | expect(fs.isAbsolute('/a/b/c')).toBe true 651 | 652 | describe ".normalize(pathToNormalize)", -> 653 | it "normalizes the path", -> 654 | expect(fs.normalize()).toBe null 655 | expect(fs.normalize(null)).toBe null 656 | expect(fs.normalize(true)).toBe 'true' 657 | expect(fs.normalize('')).toBe '.' 658 | expect(fs.normalize(3)).toBe '3' 659 | expect(fs.normalize('a')).toBe 'a' 660 | expect(fs.normalize('a/b/c/../d')).toBe path.join('a', 'b', 'd') 661 | expect(fs.normalize('./a')).toBe 'a' 662 | expect(fs.normalize('~')).toBe fs.getHomeDirectory() 663 | expect(fs.normalize('~/foo')).toBe path.join(fs.getHomeDirectory(), 'foo') 664 | 665 | describe ".tildify(pathToTildify)", -> 666 | getHomeDirectory = null 667 | 668 | beforeEach -> 669 | getHomeDirectory = fs.getHomeDirectory 670 | 671 | afterEach -> 672 | fs.getHomeDirectory = getHomeDirectory 673 | 674 | it "tildifys the path on Linux and macOS", -> 675 | return if process.platform is 'win32' 676 | 677 | home = fs.getHomeDirectory() 678 | 679 | expect(fs.tildify(home)).toBe '~' 680 | expect(fs.tildify(path.join(home, 'foo'))).toBe '~/foo' 681 | fixture = path.join('foo', home) 682 | expect(fs.tildify(fixture)).toBe fixture 683 | fixture = path.resolve("#{home}foo", 'tildify') 684 | expect(fs.tildify(fixture)).toBe fixture 685 | expect(fs.tildify('foo')).toBe 'foo' 686 | 687 | it "does not tildify if home is unset", -> 688 | return if process.platform is 'win32' 689 | 690 | home = fs.getHomeDirectory() 691 | fs.getHomeDirectory = -> return undefined 692 | 693 | fixture = path.join(home, 'foo') 694 | expect(fs.tildify(fixture)).toBe fixture 695 | 696 | it "doesn't change URLs or paths not tildified", -> 697 | urlToLeaveAlone = "https://atom.io/something/fun?abc" 698 | expect(fs.tildify(urlToLeaveAlone)).toBe urlToLeaveAlone 699 | 700 | pathToLeaveAlone = "/Library/Support/Atom/State" 701 | expect(fs.tildify(pathToLeaveAlone)).toBe pathToLeaveAlone 702 | 703 | describe ".move", -> 704 | tempDir = null 705 | 706 | beforeEach -> 707 | tempDir = temp.mkdirSync('fs-plus-') 708 | 709 | it 'calls back with an error if the source does not exist', -> 710 | callback = jasmine.createSpy('callback') 711 | directoryPath = path.join(tempDir, 'subdir') 712 | newDirectoryPath = path.join(tempDir, 'subdir2', 'subdir2') 713 | 714 | fs.move(directoryPath, newDirectoryPath, callback) 715 | 716 | waitsFor -> 717 | callback.callCount is 1 718 | 719 | runs -> 720 | expect(callback.argsForCall[0][0]).toBeTruthy() 721 | expect(callback.argsForCall[0][0].code).toBe 'ENOENT' 722 | 723 | it 'calls back with an error if the target already exists', -> 724 | callback = jasmine.createSpy('callback') 725 | directoryPath = path.join(tempDir, 'subdir') 726 | fs.mkdirSync(directoryPath) 727 | newDirectoryPath = path.join(tempDir, 'subdir2') 728 | fs.mkdirSync(newDirectoryPath) 729 | 730 | fs.move(directoryPath, newDirectoryPath, callback) 731 | 732 | waitsFor -> 733 | callback.callCount is 1 734 | 735 | runs -> 736 | expect(callback.argsForCall[0][0]).toBeTruthy() 737 | expect(callback.argsForCall[0][0].code).toBe 'EEXIST' 738 | 739 | it 'renames if the target just has different letter casing', -> 740 | callback = jasmine.createSpy('callback') 741 | directoryPath = path.join(tempDir, 'subdir') 742 | fs.mkdirSync(directoryPath) 743 | newDirectoryPath = path.join(tempDir, 'SUBDIR') 744 | 745 | fs.move(directoryPath, newDirectoryPath, callback) 746 | 747 | waitsFor -> 748 | callback.callCount is 1 749 | 750 | runs -> 751 | # If the filesystem is case-insensitive, the old directory should still exist. 752 | expect(fs.existsSync(directoryPath)).toBe fs.isCaseInsensitive() 753 | expect(fs.existsSync(newDirectoryPath)).toBe true 754 | 755 | it 'renames to a target with an existent parent directory', -> 756 | callback = jasmine.createSpy('callback') 757 | directoryPath = path.join(tempDir, 'subdir') 758 | fs.mkdirSync(directoryPath) 759 | newDirectoryPath = path.join(tempDir, 'subdir2') 760 | 761 | fs.move(directoryPath, newDirectoryPath, callback) 762 | 763 | waitsFor -> 764 | callback.callCount is 1 765 | 766 | runs -> 767 | expect(fs.existsSync(directoryPath)).toBe false 768 | expect(fs.existsSync(newDirectoryPath)).toBe true 769 | 770 | it 'renames to a target with a non-existent parent directory', -> 771 | callback = jasmine.createSpy('callback') 772 | directoryPath = path.join(tempDir, 'subdir') 773 | fs.mkdirSync(directoryPath) 774 | newDirectoryPath = path.join(tempDir, 'subdir2/subdir2') 775 | 776 | fs.move(directoryPath, newDirectoryPath, callback) 777 | 778 | waitsFor -> 779 | callback.callCount is 1 780 | 781 | runs -> 782 | expect(fs.existsSync(directoryPath)).toBe false 783 | expect(fs.existsSync(newDirectoryPath)).toBe true 784 | 785 | it 'renames files', -> 786 | callback = jasmine.createSpy('callback') 787 | filePath = path.join(tempDir, 'subdir') 788 | fs.writeFileSync(filePath, '') 789 | newFilePath = path.join(tempDir, 'subdir2') 790 | 791 | fs.move(filePath, newFilePath, callback) 792 | 793 | waitsFor -> 794 | callback.callCount is 1 795 | 796 | runs -> 797 | expect(fs.existsSync(filePath)).toBe false 798 | expect(fs.existsSync(newFilePath)).toBe true 799 | 800 | describe ".moveSync", -> 801 | tempDir = null 802 | 803 | beforeEach -> 804 | tempDir = temp.mkdirSync('fs-plus-') 805 | 806 | it 'throws an error if the source does not exist', -> 807 | directoryPath = path.join(tempDir, 'subdir') 808 | newDirectoryPath = path.join(tempDir, 'subdir2', 'subdir2') 809 | 810 | expect(-> fs.moveSync(directoryPath, newDirectoryPath)).toThrow() 811 | 812 | it 'throws an error if the target already exists', -> 813 | directoryPath = path.join(tempDir, 'subdir') 814 | fs.mkdirSync(directoryPath) 815 | newDirectoryPath = path.join(tempDir, 'subdir2') 816 | fs.mkdirSync(newDirectoryPath) 817 | 818 | expect(-> fs.moveSync(directoryPath, newDirectoryPath)).toThrow() 819 | 820 | it 'renames if the target just has different letter casing', -> 821 | directoryPath = path.join(tempDir, 'subdir') 822 | fs.mkdirSync(directoryPath) 823 | newDirectoryPath = path.join(tempDir, 'SUBDIR') 824 | 825 | fs.moveSync(directoryPath, newDirectoryPath) 826 | 827 | # If the filesystem is case-insensitive, the old directory should still exist. 828 | expect(fs.existsSync(directoryPath)).toBe fs.isCaseInsensitive() 829 | expect(fs.existsSync(newDirectoryPath)).toBe true 830 | 831 | it 'renames to a target with an existent parent directory', -> 832 | directoryPath = path.join(tempDir, 'subdir') 833 | fs.mkdirSync(directoryPath) 834 | newDirectoryPath = path.join(tempDir, 'subdir2') 835 | 836 | fs.moveSync(directoryPath, newDirectoryPath) 837 | 838 | expect(fs.existsSync(directoryPath)).toBe false 839 | expect(fs.existsSync(newDirectoryPath)).toBe true 840 | 841 | it 'renames to a target with a non-existent parent directory', -> 842 | directoryPath = path.join(tempDir, 'subdir') 843 | fs.mkdirSync(directoryPath) 844 | newDirectoryPath = path.join(tempDir, 'subdir2/subdir2') 845 | 846 | fs.moveSync(directoryPath, newDirectoryPath) 847 | 848 | expect(fs.existsSync(directoryPath)).toBe false 849 | expect(fs.existsSync(newDirectoryPath)).toBe true 850 | 851 | it 'renames files', -> 852 | filePath = path.join(tempDir, 'subdir') 853 | fs.writeFileSync(filePath, '') 854 | newFilePath = path.join(tempDir, 'subdir2') 855 | 856 | fs.moveSync(filePath, newFilePath) 857 | 858 | expect(fs.existsSync(filePath)).toBe false 859 | expect(fs.existsSync(newFilePath)).toBe true 860 | 861 | describe '.isBinaryExtension', -> 862 | it 'returns true for a recognized binary file extension', -> 863 | expect(fs.isBinaryExtension('.DS_Store')).toBe true 864 | 865 | it 'returns false for non-binary file extension', -> 866 | expect(fs.isBinaryExtension('.bz2')).toBe false 867 | 868 | it 'returns true for an uppercase binary file extension', -> 869 | expect(fs.isBinaryExtension('.EXE')).toBe true 870 | 871 | describe ".isCompressedExtension", -> 872 | it 'returns true for a recognized compressed file extension', -> 873 | expect(fs.isCompressedExtension('.bz2')).toBe true 874 | 875 | it 'returns false for non-compressed file extension', -> 876 | expect(fs.isCompressedExtension('.jpg')).toBe false 877 | 878 | describe '.isImageExtension', -> 879 | it 'returns true for a recognized image file extension', -> 880 | expect(fs.isImageExtension('.jpg')).toBe true 881 | 882 | it 'returns false for non-image file extension', -> 883 | expect(fs.isImageExtension('.bz2')).toBe false 884 | 885 | describe '.isMarkdownExtension', -> 886 | it 'returns true for a recognized Markdown file extension', -> 887 | expect(fs.isMarkdownExtension('.md')).toBe true 888 | 889 | it 'returns false for non-Markdown file extension', -> 890 | expect(fs.isMarkdownExtension('.bz2')).toBe false 891 | 892 | it 'returns true for a recognised Markdown file extension with unusual capitalisation', -> 893 | expect(fs.isMarkdownExtension('.MaRKdOwN')).toBe true 894 | 895 | describe '.isPdfExtension', -> 896 | it 'returns true for a recognized PDF file extension', -> 897 | expect(fs.isPdfExtension('.pdf')).toBe true 898 | 899 | it 'returns false for non-PDF file extension', -> 900 | expect(fs.isPdfExtension('.bz2')).toBe false 901 | 902 | it 'returns true for an uppercase PDF file extension', -> 903 | expect(fs.isPdfExtension('.PDF')).toBe true 904 | 905 | describe '.isReadmePath', -> 906 | it 'returns true for a recognized README path', -> 907 | expect(fs.isReadmePath('./path/to/README.md')).toBe true 908 | 909 | it 'returns false for non README path', -> 910 | expect(fs.isReadmePath('./path/foo.txt')).toBe false 911 | -------------------------------------------------------------------------------- /src/fs-plus.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Module from 'module'; 3 | import path from 'path'; 4 | 5 | const _ = require('underscore-plus'); 6 | const async = require('async'); 7 | const mkdirp = require('mkdirp'); 8 | const rimraf = require('rimraf'); 9 | 10 | // Public: Useful extensions to node's built-in fs module 11 | // 12 | // Important, this extends Node's builtin in ['fs' module][fs], which means that you 13 | // can do anything that you can do with Node's 'fs' module plus a few extra 14 | // functions that we've found to be helpful. 15 | // 16 | // [fs]: http://nodejs.org/api/fs.html 17 | const fsPlus = { 18 | __esModule: false, 19 | 20 | getHomeDirectory() { 21 | if ((process.platform === 'win32') && !process.env.HOME) { 22 | return process.env.USERPROFILE; 23 | } else { 24 | return process.env.HOME; 25 | } 26 | }, 27 | 28 | // Public: Make the given path absolute by resolving it against the current 29 | // working directory. 30 | // 31 | // relativePath - The {String} containing the relative path. If the path is 32 | // prefixed with '~', it will be expanded to the current user's 33 | // home directory. 34 | // 35 | // Returns the {String} absolute path or the relative path if it's unable to 36 | // determine its real path. 37 | absolute(relativePath) { 38 | if (relativePath == null) { return null; } 39 | 40 | relativePath = fsPlus.resolveHome(relativePath); 41 | 42 | try { 43 | return fs.realpathSync(relativePath); 44 | } catch (e) { 45 | return relativePath; 46 | } 47 | }, 48 | 49 | // Public: Normalize the given path treating a leading `~` segment as referring 50 | // to the home directory. This method does not query the filesystem. 51 | // 52 | // pathToNormalize - The {String} containing the abnormal path. If the path is 53 | // prefixed with '~', it will be expanded to the current 54 | // user's home directory. 55 | // 56 | // Returns a normalized path {String}. 57 | normalize(pathToNormalize) { 58 | if (pathToNormalize == null) { return null; } 59 | 60 | return fsPlus.resolveHome(path.normalize(pathToNormalize.toString())); 61 | }, 62 | 63 | resolveHome(relativePath) { 64 | if (relativePath === '~') { 65 | return fsPlus.getHomeDirectory(); 66 | } else if (relativePath.indexOf(`~${path.sep}`) === 0) { 67 | return `${fsPlus.getHomeDirectory()}${relativePath.substring(1)}`; 68 | } 69 | return relativePath; 70 | }, 71 | 72 | // Public: Convert an absolute path to tilde path for Linux and macOS. 73 | // /Users/username/dev => ~/dev 74 | // 75 | // pathToTildify - The {String} containing the full path. 76 | // 77 | // Returns a tildified path {String}. 78 | tildify(pathToTildify) { 79 | if (process.platform === 'win32') { return pathToTildify; } 80 | 81 | const normalized = fsPlus.normalize(pathToTildify); 82 | const homeDir = fsPlus.getHomeDirectory(); 83 | if (homeDir == null) { return pathToTildify; } 84 | 85 | if (normalized === homeDir) { return '~'; } 86 | if (!normalized.startsWith(path.join(homeDir, path.sep))) { return pathToTildify; } 87 | 88 | return path.join('~', path.sep, normalized.substring(homeDir.length + 1)); 89 | }, 90 | 91 | // Public: Get path to store application specific data. 92 | // 93 | // Returns the {String} absolute path or null if platform isn't supported 94 | // Mac: ~/Library/Application Support/ 95 | // Win: %AppData% 96 | // Linux: /var/lib 97 | getAppDataDirectory() { 98 | switch (process.platform) { 99 | case 'darwin': return fsPlus.absolute(path.join('~', 'Library', 'Application Support')); 100 | case 'linux': return '/var/lib'; 101 | case 'win32': return process.env.APPDATA; 102 | default: return null; 103 | } 104 | }, 105 | 106 | // Public: Is the given path absolute? 107 | // 108 | // pathToCheck - The relative or absolute {String} path to check. 109 | // 110 | // Returns a {Boolean}, true if the path is absolute, false otherwise. 111 | isAbsolute(pathToCheck='') { 112 | if (pathToCheck == null) { pathToCheck = ''; } 113 | if (process.platform === 'win32') { 114 | if (pathToCheck[1] === ':') { return true; } // C:\ style 115 | if ((pathToCheck[0] === '\\') && (pathToCheck[1] === '\\')) { return true; } // \\server\share style 116 | } else { 117 | return pathToCheck[0] === '/'; // /usr style 118 | } 119 | 120 | return false; 121 | }, 122 | 123 | // Public: Returns true if a file or folder at the specified path exists. 124 | existsSync(pathToCheck) { 125 | return isPathValid(pathToCheck) && (statSyncNoException(pathToCheck) !== false); 126 | }, 127 | 128 | // Public: Returns true if the given path exists and is a directory. 129 | isDirectorySync(directoryPath) { 130 | if (!isPathValid(directoryPath)) { 131 | return false; 132 | } 133 | const stat = statSyncNoException(directoryPath) 134 | if (stat) { 135 | return stat.isDirectory(); 136 | } else { 137 | return false; 138 | } 139 | }, 140 | 141 | // Public: Asynchronously checks that the given path exists and is a directory. 142 | isDirectory(directoryPath, done) { 143 | if (!isPathValid(directoryPath)) { return done(false); } 144 | return fs.stat(directoryPath, function(error, stat) { 145 | if (error != null) { 146 | return done(false); 147 | } else { 148 | return done(stat.isDirectory()); 149 | } 150 | }); 151 | }, 152 | 153 | // Public: Returns true if the specified path exists and is a file. 154 | isFileSync(filePath) { 155 | if (!isPathValid(filePath)) { 156 | return false; 157 | } 158 | const stat = statSyncNoException(filePath) 159 | if (stat) { 160 | return stat.isFile(); 161 | } else { 162 | return false; 163 | } 164 | }, 165 | 166 | // Public: Returns true if the specified path is a symbolic link. 167 | isSymbolicLinkSync(symlinkPath) { 168 | if (!isPathValid(symlinkPath)) { 169 | return false; 170 | } 171 | const stat = lstatSyncNoException(symlinkPath) 172 | if (stat) { 173 | return stat.isSymbolicLink(); 174 | } else { 175 | return false; 176 | } 177 | }, 178 | 179 | // Public: Calls back with true if the specified path is a symbolic link. 180 | isSymbolicLink(symlinkPath, callback) { 181 | if (isPathValid(symlinkPath)) { 182 | return fs.lstat(symlinkPath, (error, stat) => callback?.((stat != null) && stat.isSymbolicLink())); 183 | } else { 184 | return process.nextTick(() => callback?.(false)); 185 | } 186 | }, 187 | 188 | // Public: Returns true if the specified path is executable. 189 | isExecutableSync(pathToCheck) { 190 | let stat; 191 | if (!isPathValid(pathToCheck)) { return false; } 192 | if ((stat = statSyncNoException(pathToCheck))) { 193 | return (stat.mode & 0o777 & 1) !== 0; 194 | } else { 195 | return false; 196 | } 197 | }, 198 | 199 | // Public: Returns the size of the specified path. 200 | getSizeSync(pathToCheck) { 201 | if (isPathValid(pathToCheck)) { 202 | return statSyncNoException(pathToCheck).size ?? -1 203 | } else { 204 | return -1; 205 | } 206 | }, 207 | 208 | // Public: Returns an Array with the paths of the files and directories 209 | // contained within the directory path. It is not recursive. 210 | // 211 | // rootPath - The absolute {String} path to the directory to list. 212 | // extensions - An {Array} of extensions to filter the results by. If none are 213 | // given, none are filtered (optional). 214 | listSync(rootPath, extensions) { 215 | if (!fsPlus.isDirectorySync(rootPath)) { return []; } 216 | let paths = fs.readdirSync(rootPath); 217 | if (extensions) { paths = fsPlus.filterExtensions(paths, extensions); } 218 | paths = paths.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); 219 | paths = paths.map(childPath => path.join(rootPath, childPath)); 220 | return paths; 221 | }, 222 | 223 | // Public: Asynchronously lists the files and directories in the given path. 224 | // The listing is not recursive. 225 | // 226 | // rootPath - The absolute {String} path to the directory to list. 227 | // extensions - An {Array} of extensions to filter the results by. If none are 228 | // given, none are filtered (optional). 229 | // callback - The {Function} to call. 230 | list(rootPath, ...rest) { 231 | let extensions; 232 | if (rest.length > 1) { extensions = rest.shift(); } 233 | const done = rest.shift(); 234 | return fs.readdir(rootPath, (error, paths) => { 235 | if (error != null) { 236 | return done(error); 237 | } else { 238 | if (extensions) { paths = fsPlus.filterExtensions(paths, extensions); } 239 | paths = paths.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); 240 | paths = paths.map(childPath => path.join(rootPath, childPath)); 241 | return done(null, paths); 242 | } 243 | }); 244 | }, 245 | 246 | // Returns only the paths which end with one of the given extensions. 247 | filterExtensions(paths, extensions) { 248 | extensions = extensions.map( (ext) => { 249 | if (ext === '') { 250 | return ext; 251 | } else { 252 | return '.' + ext.replace(/^\./, ''); 253 | } 254 | }); 255 | return paths.filter(pathToCheck => _.include(extensions, path.extname(pathToCheck))); 256 | }, 257 | 258 | // Public: Get all paths under the given path. 259 | // 260 | // rootPath - The {String} path to start at. 261 | // 262 | // Return an {Array} of {String}s under the given path. 263 | listTreeSync(rootPath) { 264 | const paths = []; 265 | const onPath = (childPath) => { 266 | paths.push(childPath); 267 | return true; 268 | }; 269 | fsPlus.traverseTreeSync(rootPath, onPath, onPath); 270 | return paths; 271 | }, 272 | 273 | // Public: Moves the source file or directory to the target asynchronously. 274 | move(source, target, callback) { 275 | return isMoveTargetValid(source, target, (isMoveTargetValidErr, isTargetValid) => { 276 | if (isMoveTargetValidErr) { 277 | callback(isMoveTargetValidErr); 278 | return; 279 | } 280 | 281 | if (!isTargetValid) { 282 | const error = new Error(`'${target}' already exists.`); 283 | error.code = 'EEXIST'; 284 | callback(error); 285 | return; 286 | } 287 | 288 | const targetParentPath = path.dirname(target); 289 | return fs.exists(targetParentPath, (targetParentExists) => { 290 | if (targetParentExists) { 291 | fs.rename(source, target, callback); 292 | return; 293 | } 294 | 295 | return fsPlus.makeTree(targetParentPath, (makeTreeErr) => { 296 | if (makeTreeErr) { 297 | callback(makeTreeErr); 298 | return; 299 | } 300 | 301 | fs.rename(source, target, callback); 302 | }); 303 | }); 304 | }); 305 | }, 306 | 307 | // Public: Moves the source file or directory to the target synchronously. 308 | moveSync(source, target) { 309 | if (!isMoveTargetValidSync(source, target)) { 310 | const error = new Error(`'${target}' already exists.`); 311 | error.code = 'EEXIST'; 312 | throw error; 313 | } 314 | 315 | const targetParentPath = path.dirname(target); 316 | if (!fs.existsSync(targetParentPath)) { fsPlus.makeTreeSync(targetParentPath); } 317 | fs.renameSync(source, target); 318 | }, 319 | 320 | // Public: Removes the file or directory at the given path synchronously. 321 | removeSync(pathToRemove) { 322 | return rimraf.sync(pathToRemove); 323 | }, 324 | 325 | // Public: Removes the file or directory at the given path asynchronously. 326 | remove(pathToRemove, callback) { 327 | return rimraf(pathToRemove, callback); 328 | }, 329 | 330 | // Public: Open, write, flush, and close a file, writing the given content 331 | // synchronously. 332 | // 333 | // It also creates the necessary parent directories. 334 | writeFileSync(filePath, content, options) { 335 | mkdirp.sync(path.dirname(filePath)); 336 | fs.writeFileSync(filePath, content, options); 337 | }, 338 | 339 | // Public: Open, write, flush, and close a file, writing the given content 340 | // asynchronously. 341 | // 342 | // It also creates the necessary parent directories. 343 | writeFile(filePath, content, options, callback) { 344 | callback = _.last(arguments); 345 | mkdirp(path.dirname(filePath), (error) => { 346 | if (error != null) { 347 | callback?.(error) 348 | } else { 349 | fs.writeFile(filePath, content, options, callback); 350 | } 351 | }); 352 | }, 353 | 354 | // Public: Copies the given path asynchronously. 355 | copy(sourcePath, destinationPath, done) { 356 | mkdirp(path.dirname(destinationPath), (error) => { 357 | if (error != null) { 358 | done?.(error); 359 | return; 360 | } 361 | 362 | const sourceStream = fs.createReadStream(sourcePath); 363 | sourceStream.on('error', (error) => { 364 | done?.(error); 365 | return done = null; 366 | }); 367 | 368 | const destinationStream = fs.createWriteStream(destinationPath); 369 | destinationStream.on('error', (error) => { 370 | done?.(error); 371 | return done = null; 372 | }); 373 | destinationStream.on('close', () => { 374 | done?.() 375 | return done = null; 376 | }); 377 | 378 | return sourceStream.pipe(destinationStream); 379 | }); 380 | }, 381 | 382 | // Public: Copies the given path recursively and synchronously. 383 | copySync(sourcePath, destinationPath) { 384 | // We need to save the sources before creaing the new directory to avoid 385 | // infinitely creating copies of the directory when copying inside itself 386 | const sources = fs.readdirSync(sourcePath); 387 | mkdirp.sync(destinationPath); 388 | for (let source of sources) { 389 | const sourceFilePath = path.join(sourcePath, source); 390 | const destinationFilePath = path.join(destinationPath, source); 391 | 392 | if (fsPlus.isDirectorySync(sourceFilePath)) { 393 | fsPlus.copySync(sourceFilePath, destinationFilePath); 394 | } else { 395 | fsPlus.copyFileSync(sourceFilePath, destinationFilePath); 396 | } 397 | } 398 | }, 399 | 400 | // Public: Copies the given path synchronously, buffering reads and writes to 401 | // keep memory footprint to a minimum. If the destination directory doesn't 402 | // exist, it creates it. 403 | // 404 | // * sourceFilePath - A {String} representing the file path you want to copy. 405 | // * destinationFilePath - A {String} representing the file path where the file will be copied. 406 | // * bufferSize - An {Integer} representing the size in bytes of the buffer 407 | // when reading from and writing to disk. The default is 16KB. 408 | copyFileSync(sourceFilePath, destinationFilePath, bufferSize) { 409 | if (bufferSize == null) { bufferSize = 16 * 1024; } 410 | mkdirp.sync(path.dirname(destinationFilePath)); 411 | 412 | let readFd = null; 413 | let writeFd = null; 414 | try { 415 | readFd = fs.openSync(sourceFilePath, 'r'); 416 | writeFd = fs.openSync(destinationFilePath, 'w'); 417 | let bytesRead = 1; 418 | let position = 0; 419 | while (bytesRead > 0) { 420 | const buffer = new Buffer(bufferSize); 421 | bytesRead = fs.readSync(readFd, buffer, 0, buffer.length, position); 422 | fs.writeSync(writeFd, buffer, 0, bytesRead, position); 423 | position += bytesRead; 424 | } 425 | } finally { 426 | if (readFd != null) { fs.closeSync(readFd); } 427 | if (writeFd != null) { fs.closeSync(writeFd); } 428 | } 429 | }, 430 | 431 | // Public: Create a directory at the specified path including any missing 432 | // parent directories synchronously. 433 | makeTreeSync(directoryPath) { 434 | if (!fsPlus.isDirectorySync(directoryPath)) { mkdirp.sync(directoryPath); } 435 | }, 436 | 437 | // Public: Create a directory at the specified path including any missing 438 | // parent directories asynchronously. 439 | makeTree(directoryPath, callback) { 440 | fsPlus.isDirectory(directoryPath, (exists) => { 441 | if (exists) { return callback?.(); } 442 | mkdirp(directoryPath, error => callback?.(error)); 443 | }); 444 | }, 445 | 446 | // Public: Recursively walk the given path and execute the given functions 447 | // synchronously. 448 | // 449 | // rootPath - The {String} containing the directory to recurse into. 450 | // onFile - The {Function} to execute on each file, receives a single argument 451 | // the absolute path. 452 | // onDirectory - The {Function} to execute on each directory, receives a single 453 | // argument the absolute path (defaults to onFile). If this 454 | // function returns a falsy value then the directory is not 455 | // entered. 456 | traverseTreeSync(rootPath, onFile, onDirectory) { 457 | if (onDirectory == null) { onDirectory = onFile; } 458 | if (!fsPlus.isDirectorySync(rootPath)) { return; } 459 | 460 | const traverse = function(directoryPath, onFile, onDirectory) { 461 | for (let file of fs.readdirSync(directoryPath)) { 462 | const childPath = path.join(directoryPath, file); 463 | let stats = fs.lstatSync(childPath); 464 | if (stats.isSymbolicLink()) { 465 | const linkStats = statSyncNoException(childPath) 466 | if (linkStats) { 467 | stats = linkStats; 468 | } 469 | } 470 | if (stats.isDirectory()) { 471 | if (onDirectory(childPath)) { traverse(childPath, onFile, onDirectory); } 472 | } else if (stats.isFile()) { 473 | onFile(childPath); 474 | } 475 | } 476 | 477 | return undefined; 478 | }; 479 | 480 | return traverse(rootPath, onFile, onDirectory); 481 | }, 482 | 483 | // Public: Recursively walk the given path and execute the given functions 484 | // asynchronously. 485 | // 486 | // rootPath - The {String} containing the directory to recurse into. 487 | // onFile - The {Function} to execute on each file, receives a single argument 488 | // the absolute path. 489 | // onDirectory - The {Function} to execute on each directory, receives a single 490 | // argument the absolute path (defaults to onFile). 491 | traverseTree(rootPath, onFile, onDirectory, onDone) { 492 | return fs.readdir(rootPath, (error, files) => { 493 | if (error) { 494 | return onDone?.() 495 | } else { 496 | let queue = async.queue((childPath, callback) => 497 | fs.stat(childPath, (error, stats) => { 498 | if (error) { 499 | return callback(error); 500 | } else if (stats.isFile()) { 501 | onFile(childPath); 502 | return callback(); 503 | } else if (stats.isDirectory()) { 504 | if (onDirectory(childPath)) { 505 | return fs.readdir(childPath, (error, files) => { 506 | if (error) { 507 | return callback(error); 508 | } else { 509 | for (let file of files) { 510 | queue.unshift(path.join(childPath, file)); 511 | } 512 | return callback(); 513 | } 514 | }); 515 | } else { 516 | return callback(); 517 | } 518 | } else { 519 | return callback(); 520 | } 521 | }) 522 | ); 523 | queue.concurrency = 1; 524 | queue.drain = onDone; 525 | for (let file of files) { 526 | queue.push(path.join(rootPath, file)); 527 | } 528 | } 529 | }); 530 | }, 531 | 532 | // Public: Hashes the contents of the given file. 533 | // 534 | // pathToDigest - The {String} containing the absolute path. 535 | // 536 | // Returns a String containing the MD5 hexadecimal hash. 537 | md5ForPath(pathToDigest) { 538 | const contents = fs.readFileSync(pathToDigest); 539 | return require('crypto').createHash('md5').update(contents).digest('hex'); 540 | }, 541 | 542 | // Public: Finds a relative path among the given array of paths. 543 | // 544 | // loadPaths - An {Array} of absolute and relative paths to search. 545 | // pathToResolve - The {String} containing the path to resolve. 546 | // extensions - An {Array} of extensions to pass to {resolveExtensions} in 547 | // which case pathToResolve should not contain an extension 548 | // (optional). 549 | // 550 | // Returns the absolute path of the file to be resolved if it's found and 551 | // undefined otherwise. 552 | resolve(...args) { 553 | let extensions; 554 | if (_.isArray(_.last(args))) { extensions = args.pop(); } 555 | const pathToResolve = args.pop()?.toString(); 556 | const loadPaths = args; 557 | 558 | if (!pathToResolve) { return undefined; } 559 | 560 | let resolvedPath; 561 | if (fsPlus.isAbsolute(pathToResolve)) { 562 | if (extensions && (resolvedPath = fsPlus.resolveExtension(pathToResolve, extensions))) { 563 | return resolvedPath; 564 | } else { 565 | if (fsPlus.existsSync(pathToResolve)) { return pathToResolve; } 566 | } 567 | } 568 | 569 | for (let loadPath of Array.from(loadPaths)) { 570 | const candidatePath = path.join(loadPath, pathToResolve); 571 | if (extensions) { 572 | resolvedPath = fsPlus.resolveExtension(candidatePath, extensions) 573 | if (resolvedPath) { 574 | return resolvedPath; 575 | } 576 | } else { 577 | if (fsPlus.existsSync(candidatePath)) { return fsPlus.absolute(candidatePath); } 578 | } 579 | } 580 | return undefined; 581 | }, 582 | 583 | // Public: Like {.resolve} but uses node's modules paths as the load paths to 584 | // search. 585 | resolveOnLoadPath(...args) { 586 | let modulePaths = null; 587 | if (module.paths != null) { 588 | modulePaths = module.paths; 589 | } else if (process.resourcesPath) { 590 | modulePaths = [path.join(process.resourcesPath, 'app', 'node_modules')]; 591 | } else { 592 | modulePaths = []; 593 | } 594 | 595 | const loadPaths = Module.globalPaths.concat(modulePaths); 596 | return fsPlus.resolve(...loadPaths, ...args); 597 | }, 598 | 599 | // Public: Finds the first file in the given path which matches the extension 600 | // in the order given. 601 | // 602 | // pathToResolve - The {String} containing relative or absolute path of the 603 | // file in question without the extension or '.'. 604 | // extensions - The ordered {Array} of extensions to try. 605 | // 606 | // Returns the absolute path of the file if it exists with any of the given 607 | // extensions, otherwise it's undefined. 608 | resolveExtension(pathToResolve, extensions) { 609 | for (let extension of Array.from(extensions)) { 610 | if (extension === "") { 611 | if (fsPlus.existsSync(pathToResolve)) { return fsPlus.absolute(pathToResolve); } 612 | } else { 613 | const pathWithExtension = pathToResolve + "." + extension.replace(/^\./, ""); 614 | if (fsPlus.existsSync(pathWithExtension)) { return fsPlus.absolute(pathWithExtension); } 615 | } 616 | } 617 | return undefined; 618 | }, 619 | 620 | // Public: Returns true for extensions associated with compressed files. 621 | isCompressedExtension(ext) { 622 | if (ext == null) { return false; } 623 | return COMPRESSED_EXTENSIONS.hasOwnProperty(ext.toLowerCase()); 624 | }, 625 | 626 | // Public: Returns true for extensions associated with image files. 627 | isImageExtension(ext) { 628 | if (ext == null) { return false; } 629 | return IMAGE_EXTENSIONS.hasOwnProperty(ext.toLowerCase()); 630 | }, 631 | 632 | // Public: Returns true for extensions associated with pdf files. 633 | isPdfExtension(ext) { 634 | return ext?.toLowerCase() === '.pdf'; 635 | }, 636 | 637 | // Public: Returns true for extensions associated with binary files. 638 | isBinaryExtension(ext) { 639 | if (ext == null) { return false; } 640 | return BINARY_EXTENSIONS.hasOwnProperty(ext.toLowerCase()); 641 | }, 642 | 643 | // Public: Returns true for files named similarily to 'README' 644 | isReadmePath(readmePath) { 645 | const extension = path.extname(readmePath); 646 | const base = path.basename(readmePath, extension).toLowerCase(); 647 | return (base === 'readme') && ((extension === '') || fsPlus.isMarkdownExtension(extension)); 648 | }, 649 | 650 | // Public: Returns true for extensions associated with Markdown files. 651 | isMarkdownExtension(ext) { 652 | if (ext == null) { return false; } 653 | return MARKDOWN_EXTENSIONS.hasOwnProperty(ext.toLowerCase()); 654 | }, 655 | 656 | // Public: Is the filesystem case insensitive? 657 | // 658 | // Returns `true` if case insensitive, `false` otherwise. 659 | isCaseInsensitive() { 660 | if (fsPlus.caseInsensitiveFs == null) { 661 | const lowerCaseStat = statSyncNoException(process.execPath.toLowerCase()); 662 | const upperCaseStat = statSyncNoException(process.execPath.toUpperCase()); 663 | if (lowerCaseStat && upperCaseStat) { 664 | fsPlus.caseInsensitiveFs = (lowerCaseStat.dev === upperCaseStat.dev) && (lowerCaseStat.ino === upperCaseStat.ino); 665 | } else { 666 | fsPlus.caseInsensitiveFs = false; 667 | } 668 | } 669 | 670 | return fsPlus.caseInsensitiveFs; 671 | }, 672 | 673 | // Public: Is the filesystem case sensitive? 674 | // 675 | // Returns `true` if case sensitive, `false` otherwise. 676 | isCaseSensitive() { return !fsPlus.isCaseInsensitive(); }, 677 | 678 | // Public: Calls `fs.statSync`, catching all exceptions raised. This 679 | // method calls `fs.statSyncNoException` when provided by the underlying 680 | // `fs` module (Electron < 3.0). 681 | // 682 | // Returns `fs.Stats` if the file exists, `false` otherwise. 683 | statSyncNoException(...args) { 684 | return statSyncNoException(...args); 685 | }, 686 | 687 | // Public: Calls `fs.lstatSync`, catching all exceptions raised. This 688 | // method calls `fs.lstatSyncNoException` when provided by the underlying 689 | // `fs` module (Electron < 3.0). 690 | // 691 | // Returns `fs.Stats` if the file exists, `false` otherwise. 692 | lstatSyncNoException(...args) { 693 | return lstatSyncNoException(...args); 694 | } 695 | }; 696 | 697 | // Built-in [l]statSyncNoException methods are only provided in Electron releases 698 | // before 3.0. We delay the version check until first request so that Electron 699 | // application snapshots can be generated successfully. 700 | let isElectron2OrLower = null; 701 | const checkIfElectron2OrLower = function() { 702 | if (isElectron2OrLower === null) { 703 | isElectron2OrLower = 704 | process.versions.electron && 705 | (parseInt(process.versions.electron.split('.')[0]) <= 2); 706 | } 707 | return isElectron2OrLower; 708 | }; 709 | 710 | let statSyncNoException = function(...args) { 711 | if (fs.statSyncNoException && checkIfElectron2OrLower()) { 712 | return fs.statSyncNoException(...args); 713 | } else { 714 | try { 715 | return fs.statSync(...args); 716 | } catch (error) { 717 | return false; 718 | } 719 | } 720 | }; 721 | 722 | let lstatSyncNoException = function(...args) { 723 | if (fs.lstatSyncNoException && checkIfElectron2OrLower()) { 724 | return fs.lstatSyncNoException(...args); 725 | } else { 726 | try { 727 | return fs.lstatSync(...args); 728 | } catch (error) { 729 | return false; 730 | } 731 | } 732 | }; 733 | 734 | const BINARY_EXTENSIONS = { 735 | '.ds_store': true, 736 | '.a': true, 737 | '.exe': true, 738 | '.o': true, 739 | '.pyc': true, 740 | '.pyo': true, 741 | '.so': true, 742 | '.woff': true 743 | }; 744 | 745 | const COMPRESSED_EXTENSIONS = { 746 | '.bz2': true, 747 | '.egg': true, 748 | '.epub': true, 749 | '.gem': true, 750 | '.gz': true, 751 | '.jar': true, 752 | '.lz': true, 753 | '.lzma': true, 754 | '.lzo': true, 755 | '.rar': true, 756 | '.tar': true, 757 | '.tgz': true, 758 | '.war': true, 759 | '.whl': true, 760 | '.xpi': true, 761 | '.xz': true, 762 | '.z': true, 763 | '.zip': true 764 | }; 765 | 766 | const IMAGE_EXTENSIONS = { 767 | '.gif': true, 768 | '.ico': true, 769 | '.jpeg': true, 770 | '.jpg': true, 771 | '.png': true, 772 | '.tif': true, 773 | '.tiff': true, 774 | '.webp': true 775 | }; 776 | 777 | const MARKDOWN_EXTENSIONS = { 778 | '.markdown': true, 779 | '.md': true, 780 | '.mdown': true, 781 | '.mkd': true, 782 | '.mkdown': true, 783 | '.rmd': true, 784 | '.ron': true 785 | }; 786 | 787 | let isPathValid = function(pathToCheck) { 788 | return (pathToCheck != null) && (typeof pathToCheck === 'string') && (pathToCheck.length > 0); 789 | } 790 | 791 | let isMoveTargetValid = function(source, target, callback) { 792 | return fs.stat(source, (oldErr, oldStat) => { 793 | if (oldErr) { 794 | callback(oldErr); 795 | return; 796 | } 797 | 798 | return fs.stat(target, (newErr, newStat) => { 799 | if (newErr && (newErr.code === 'ENOENT')) { 800 | callback(undefined, true); // new path does not exist so it is valid 801 | return; 802 | } 803 | 804 | // New path exists so check if it points to the same file as the initial 805 | // path to see if the case of the file name is being changed on a case 806 | // insensitive filesystem. 807 | return callback(undefined, (source.toLowerCase() === target.toLowerCase()) && 808 | (oldStat.dev === newStat.dev) && 809 | (oldStat.ino === newStat.ino)); 810 | }); 811 | }); 812 | } 813 | 814 | let isMoveTargetValidSync = function(source, target) { 815 | const oldStat = statSyncNoException(source); 816 | const newStat = statSyncNoException(target); 817 | 818 | if (!oldStat || !newStat) { return true; } 819 | 820 | // New path exists so check if it points to the same file as the initial 821 | // path to see if the case of the file name is being changed on a case 822 | // insensitive filesystem. 823 | return (source.toLowerCase() === target.toLowerCase()) && 824 | (oldStat.dev === newStat.dev) && 825 | (oldStat.ino === newStat.ino); 826 | }; 827 | 828 | module.exports = new Proxy({}, { 829 | get(target, key) { 830 | if (fsPlus.hasOwnProperty(key)) { 831 | return fsPlus[key]; 832 | } else { 833 | return fs[key]; 834 | } 835 | }, 836 | 837 | set(target, key, value) { 838 | return fsPlus[key] = value; 839 | } 840 | }); 841 | --------------------------------------------------------------------------------