├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .npmignore ├── .prettierrc ├── README.md ├── jest.config.json ├── license.md ├── package-lock.json ├── package.json ├── scripts └── build.mjs ├── src ├── ApiError.ts ├── backends │ ├── AsyncMirror.ts │ ├── Dropbox.ts │ ├── Emscripten.ts │ ├── FileSystemAccess.ts │ ├── FolderAdapter.ts │ ├── HTTPRequest.ts │ ├── InMemory.ts │ ├── IndexedDB.ts │ ├── IsoFS.ts │ ├── OverlayFS.ts │ ├── Storage.ts │ ├── WorkerFS.ts │ ├── ZipFS.ts │ ├── backend.ts │ └── index.ts ├── cred.ts ├── emulation │ ├── callbacks.ts │ ├── constants.ts │ ├── fs.ts │ ├── index.ts │ ├── promises.ts │ ├── shared.ts │ └── sync.ts ├── file.ts ├── filesystem.ts ├── generic │ ├── dropbox_bridge.d.ts │ ├── emscripten_fs.ts │ ├── fetch.ts │ ├── file_index.ts │ ├── key_value_filesystem.ts │ ├── locked_fs.ts │ ├── mutex.ts │ └── preload_file.ts ├── index.ts ├── inode.ts ├── stats.ts └── utils.ts ├── test ├── common.ts ├── fixtures │ ├── README.md │ ├── files │ │ ├── emscripten │ │ │ ├── files.err │ │ │ ├── files.out │ │ │ └── somefile.binary │ │ ├── isofs │ │ │ └── 1 │ │ │ │ └── 2 │ │ │ │ └── 3 │ │ │ │ └── 4 │ │ │ │ └── 5 │ │ │ │ └── 6 │ │ │ │ └── 7 │ │ │ │ └── 8 │ │ │ │ └── test_file.txt │ │ └── node │ │ │ ├── a.js │ │ │ ├── a1.js │ │ │ ├── elipses.txt │ │ │ ├── empty.txt │ │ │ ├── exit.js │ │ │ └── x.txt │ ├── isofs │ │ ├── test_joliet.iso │ │ └── test_rock_ridge.iso │ └── static │ │ └── 49chars.txt ├── tests │ ├── HTTPRequest │ │ └── listing.ts │ ├── OverlayFS │ │ └── delete-log-test.ts │ └── all │ │ ├── appendFile.test.ts │ │ ├── chmod.test.ts │ │ ├── error-messages.test.ts │ │ ├── exists.test.ts │ │ ├── fsync.test.ts │ │ ├── long-path.test.ts │ │ ├── mkdir.test.ts │ │ ├── mode.test.ts │ │ ├── null-bytes.test.ts │ │ ├── open.test.ts │ │ ├── read.test.ts │ │ ├── readFile.test.ts │ │ ├── readFileSync.test.ts │ │ ├── readdir.test.ts │ │ ├── rename.test.ts │ │ ├── rmdir.test.ts │ │ ├── stat.test.ts │ │ ├── symlink.test.ts │ │ ├── truncate.test.ts │ │ ├── utimes.test.ts │ │ ├── write.test.ts │ │ ├── writeFile.test.ts │ │ ├── writeFileSync.test.ts │ │ └── writeSync.test.ts └── tsconfig.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.{js,ts}] 10 | indent_style = tab 11 | indent_size = 4 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": "latest" 10 | }, 11 | "rules": { 12 | "no-useless-escape": "warn", 13 | "no-unused-vars": "off", 14 | "no-mixed-spaces-and-tabs": "warn", 15 | "no-unreachable": "warn", 16 | "no-extra-semi": "warn", 17 | "no-fallthrough": "off", 18 | "no-empty": "warn", 19 | "no-case-declarations": "off", 20 | "prefer-const": "warn", 21 | "prefer-rest-params": "warn", 22 | "prefer-spread": "warn", 23 | "@typescript-eslint/no-unused-vars": "warn", 24 | "@typescript-eslint/no-inferrable-types": "off", 25 | "@typescript-eslint/no-this-alias": "off", 26 | "@typescript-eslint/ban-types": "warn", 27 | "@typescript-eslint/triple-slash-reference": "warn", 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/no-namespace": "off" 30 | }, 31 | "ignorePatterns": ["scripts/*.js", "src/*.config.js", "test/fixtures"], 32 | "plugins": ["@typescript-eslint"] 33 | } 34 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | workflow_dispatch: 9 | 10 | jobs: 11 | ci: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | name: ${{ matrix.os }} 17 | permissions: 18 | contents: read 19 | id-token: write 20 | defaults: 21 | run: 22 | shell: bash 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 18 31 | 32 | - name: Install dependencies 33 | run: npm install 34 | 35 | - name: Formatting 36 | run: npm run format:check 37 | 38 | - name: Linting 39 | run: npm run lint 40 | 41 | # Once unit tests are working 42 | #- name: Unit tests 43 | # run: npm run test 44 | 45 | - name: Build 46 | run: npm run build 47 | docs: 48 | needs: ci 49 | runs-on: ubuntu-latest 50 | name: Docs build and deploy 51 | permissions: 52 | contents: write 53 | id-token: write 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v3 57 | 58 | - name: Set up Node.js 59 | uses: actions/setup-node@v3 60 | with: 61 | node-version: 18 62 | 63 | - name: Install dependencies 64 | run: npm install 65 | 66 | - name: Build docs 67 | run: npm run build:docs 68 | 69 | - name: Deploy docs 70 | uses: peaceiris/actions-gh-pages@v3 71 | with: 72 | github_token: ${{ secrets.GITHUB_TOKEN }} 73 | publish_dir: ./docs 74 | publish_branch: docs 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | dist 4 | docs 5 | *.log 6 | tmp 7 | build 8 | test/fixtures 9 | !test/fixtures/files 10 | !test/fixtures/README.md 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !src/**/* 3 | !dist/**/* 4 | !license.md 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 4, 6 | "printWidth": 180, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest/presets/default-esm", 3 | "testEnvironment": "node", 4 | "extensionsToTreatAsEsm": [ 5 | ".ts" 6 | ], 7 | "testMatch": [ 8 | "./**/*.test.ts" 9 | ], 10 | "moduleNameMapper": { 11 | "^(\\.{1,2}/.*)\\.js$": "$1" 12 | }, 13 | "transform": { 14 | "^.+\\.ts$": [ 15 | "ts-jest", 16 | { 17 | "tsconfig": "test/tsconfig.json", 18 | "useESM": true 19 | } 20 | ] 21 | }, 22 | "verbose": true 23 | } -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | BrowserFS's license follows: 2 | 3 | ==== 4 | 5 | Copyright (c) 2013-2023 John Vilk and other BrowserFS contributors. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | of the Software, and to permit persons to whom the Software is furnished to do 12 | so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ==== 26 | 27 | This license applies to all parts of BrowserFS, except for the following items: 28 | 29 | - The test fixtures located in `test/fixtures/files/node`. Their license follows: 30 | """ 31 | Copyright Joyent, Inc. and other Node contributors. All rights reserved. 32 | Permission is hereby granted, free of charge, to any person obtaining a copy 33 | of this software and associated documentation files (the "Software"), to 34 | deal in the Software without restriction, including without limitation the 35 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 36 | sell copies of the Software, and to permit persons to whom the Software is 37 | furnished to do so, subject to the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be included in 40 | all copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 43 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 44 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 45 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 46 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 47 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 48 | IN THE SOFTWARE. 49 | """ 50 | 51 | - The Emscripten file system in src/generic/emscripten_fs.ts is a modified 52 | version of Emscripten's NODEFS. Emscripten's license follows: 53 | """ 54 | Emscripten is available under 2 licenses, the MIT license and the 55 | University of Illinois/NCSA Open Source License. 56 | 57 | Both are permissive open source licenses, with little if any 58 | practical difference between them. 59 | 60 | The reason for offering both is that (1) the MIT license is 61 | well-known, while (2) the University of Illinois/NCSA Open Source 62 | License allows Emscripten's code to be integrated upstream into 63 | LLVM, which uses that license, should the opportunity arise. 64 | 65 | The full text of both licenses follows. 66 | 67 | ============================================================================== 68 | 69 | Copyright (c) 2010-2011 Emscripten authors, see AUTHORS file. 70 | 71 | Permission is hereby granted, free of charge, to any person obtaining a copy 72 | of this software and associated documentation files (the "Software"), to deal 73 | in the Software without restriction, including without limitation the rights 74 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 75 | copies of the Software, and to permit persons to whom the Software is 76 | furnished to do so, subject to the following conditions: 77 | 78 | The above copyright notice and this permission notice shall be included in 79 | all copies or substantial portions of the Software. 80 | 81 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 82 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 83 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 84 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 85 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 86 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 87 | THE SOFTWARE. 88 | 89 | ============================================================================== 90 | 91 | Copyright (c) 2010-2011 Emscripten authors, see AUTHORS file. 92 | All rights reserved. 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a 95 | copy of this software and associated documentation files (the 96 | "Software"), to deal with the Software without restriction, including 97 | without limitation the rights to use, copy, modify, merge, publish, 98 | distribute, sublicense, and/or sell copies of the Software, and to 99 | permit persons to whom the Software is furnished to do so, subject to 100 | the following conditions: 101 | 102 | Redistributions of source code must retain the above copyright 103 | notice, this list of conditions and the following disclaimers. 104 | 105 | Redistributions in binary form must reproduce the above 106 | copyright notice, this list of conditions and the following disclaimers 107 | in the documentation and/or other materials provided with the 108 | distribution. 109 | 110 | Neither the names of Mozilla, 111 | nor the names of its contributors may be used to endorse 112 | or promote products derived from this Software without specific prior 113 | written permission. 114 | 115 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 116 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 117 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 118 | IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR 119 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 120 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 121 | SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. 122 | """ 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserfs", 3 | "version": "2.0.0", 4 | "description": "A filesystem in your browser!", 5 | "main": "dist/browserfs.js", 6 | "types": "dist", 7 | "keywords": [ 8 | "filesystem", 9 | "node", 10 | "storage" 11 | ], 12 | "type": "module", 13 | "homepage": "https://github.com/jvilk/BrowserFS", 14 | "author": "John Vilk (http://people.cs.umass.edu/~jvilk)", 15 | "contributors": [ 16 | "James Prevett (https://jamespre.dev)" 17 | ], 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/jvilk/BrowserFS.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/jvilk/BrowserFS/issues" 25 | }, 26 | "engines": { 27 | "node": ">= 18" 28 | }, 29 | "exports": { 30 | ".": { 31 | "types": "./dist/index.d.ts", 32 | "require": "./dist/browserfs.min.cjs", 33 | "import": "./dist/browserfs.min.mjs" 34 | } 35 | }, 36 | "scripts": { 37 | "lint": "eslint src test/**/*.test.ts", 38 | "build": "rm -rf dist/* && node scripts/build.mjs", 39 | "test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest", 40 | "prepublishOnly": "npm run build", 41 | "build:docs": "typedoc --out docs --name BrowserFS src/index.ts", 42 | "format:check": "prettier --check src test", 43 | "format": "prettier --write src test" 44 | }, 45 | "devDependencies": { 46 | "@jest/globals": "^29.5.0", 47 | "@types/archiver": "~2.1.2", 48 | "@types/jest": "^29.5.1", 49 | "@types/node": "^14.18.62", 50 | "@types/wicg-file-system-access": "^2020.9.6", 51 | "@typescript-eslint/eslint-plugin": "^5.55.0", 52 | "@typescript-eslint/parser": "^5.55.0", 53 | "archiver": "~2.1.1", 54 | "bfs-path": "~0.1.2", 55 | "bfs-process": "~0.1.6", 56 | "buffer": "~5.1.0", 57 | "cross-env": "^7.0.3", 58 | "dropbox": "~4.0.9", 59 | "esbuild": "^0.17.18", 60 | "esbuild-plugin-polyfill-node": "^0.3.0", 61 | "eslint": "^8.36.0", 62 | "jest": "^29.5.0", 63 | "path": "^0.12.7", 64 | "prettier": "^2.8.7", 65 | "source-map-loader": "~0.2.3", 66 | "ts-jest": "^29.1.0", 67 | "typedoc": "^0.25.1", 68 | "typescript": "^4.9.5" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | import { polyfillNode } from 'esbuild-plugin-polyfill-node'; 3 | 4 | const common = { 5 | entryPoints: ['src/index.ts'], 6 | target: ['es6'], 7 | globalName: 'BrowserFS', 8 | sourcemap: true, 9 | keepNames: true, 10 | bundle: true, 11 | alias: { process: 'bfs-process', path: 'path' }, 12 | plugins: [polyfillNode()], 13 | }; 14 | 15 | const configs = { 16 | 'browser, unminified': { outfile: 'dist/browserfs.js', platform: 'browser' }, 17 | 'browser, minified': { outfile: 'dist/browserfs.min.js', platform: 'browser', minify: true }, 18 | 'ESM, unminified': { outfile: 'dist/browserfs.mjs', platform: 'neutral', format: 'esm' }, 19 | 'ESM, minified': { outfile: 'dist/browserfs.min.mjs', platform: 'neutral', format: 'esm', minify: true }, 20 | 'node, unminified': { outfile: 'dist/browserfs.cjs', platform: 'node', format: 'cjs', alias: {}, plugins: [] }, 21 | 'node, minified': { outfile: 'dist/browserfs.min.cjs', platform: 'node', format: 'cjs', minify: true, alias: {}, plugins: [] }, 22 | }; 23 | 24 | for (const [name, config] of Object.entries(configs)) { 25 | console.log(`Building for ${name}...`); 26 | await build({ ...common, ...config }); 27 | console.log(`Built for ${name}.`); 28 | } 29 | -------------------------------------------------------------------------------- /src/ApiError.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | /** 3 | * Standard libc error codes. Add more to this enum and ErrorStrings as they are 4 | * needed. 5 | * @url http://www.gnu.org/software/libc/manual/html_node/Error-Codes.html 6 | */ 7 | export enum ErrorCode { 8 | EPERM = 1, 9 | ENOENT = 2, 10 | EIO = 5, 11 | EBADF = 9, 12 | EACCES = 13, 13 | EBUSY = 16, 14 | EEXIST = 17, 15 | ENOTDIR = 20, 16 | EISDIR = 21, 17 | EINVAL = 22, 18 | EFBIG = 27, 19 | ENOSPC = 28, 20 | EROFS = 30, 21 | ENOTEMPTY = 39, 22 | ENOTSUP = 95, 23 | } 24 | 25 | /** 26 | * Strings associated with each error code. 27 | * @hidden 28 | */ 29 | export const ErrorStrings: { [code: string | number]: string } = {}; 30 | ErrorStrings[ErrorCode.EPERM] = 'Operation not permitted.'; 31 | ErrorStrings[ErrorCode.ENOENT] = 'No such file or directory.'; 32 | ErrorStrings[ErrorCode.EIO] = 'Input/output error.'; 33 | ErrorStrings[ErrorCode.EBADF] = 'Bad file descriptor.'; 34 | ErrorStrings[ErrorCode.EACCES] = 'Permission denied.'; 35 | ErrorStrings[ErrorCode.EBUSY] = 'Resource busy or locked.'; 36 | ErrorStrings[ErrorCode.EEXIST] = 'File exists.'; 37 | ErrorStrings[ErrorCode.ENOTDIR] = 'File is not a directory.'; 38 | ErrorStrings[ErrorCode.EISDIR] = 'File is a directory.'; 39 | ErrorStrings[ErrorCode.EINVAL] = 'Invalid argument.'; 40 | ErrorStrings[ErrorCode.EFBIG] = 'File is too big.'; 41 | ErrorStrings[ErrorCode.ENOSPC] = 'No space left on disk.'; 42 | ErrorStrings[ErrorCode.EROFS] = 'Cannot modify a read-only file system.'; 43 | ErrorStrings[ErrorCode.ENOTEMPTY] = 'Directory is not empty.'; 44 | ErrorStrings[ErrorCode.ENOTSUP] = 'Operation is not supported.'; 45 | 46 | interface ApiErrorJSON { 47 | errno: ErrorCode; 48 | message: string; 49 | path: string; 50 | code: string; 51 | stack: string; 52 | } 53 | 54 | /** 55 | * Represents a BrowserFS error. Passed back to applications after a failed 56 | * call to the BrowserFS API. 57 | */ 58 | export class ApiError extends Error implements NodeJS.ErrnoException { 59 | public static fromJSON(json: ApiErrorJSON): ApiError { 60 | const err = new ApiError(json.errno, json.message, json.path); 61 | err.code = json.code; 62 | err.stack = json.stack; 63 | return err; 64 | } 65 | 66 | /** 67 | * Creates an ApiError object from a buffer. 68 | */ 69 | public static fromBuffer(buffer: Buffer, i: number = 0): ApiError { 70 | return ApiError.fromJSON(JSON.parse(buffer.toString('utf8', i + 4, i + 4 + buffer.readUInt32LE(i)))); 71 | } 72 | 73 | public static FileError(code: ErrorCode, p: string): ApiError { 74 | return new ApiError(code, ErrorStrings[code], p); 75 | } 76 | 77 | public static EACCES(path: string): ApiError { 78 | return this.FileError(ErrorCode.EACCES, path); 79 | } 80 | 81 | public static ENOENT(path: string): ApiError { 82 | return this.FileError(ErrorCode.ENOENT, path); 83 | } 84 | 85 | public static EEXIST(path: string): ApiError { 86 | return this.FileError(ErrorCode.EEXIST, path); 87 | } 88 | 89 | public static EISDIR(path: string): ApiError { 90 | return this.FileError(ErrorCode.EISDIR, path); 91 | } 92 | 93 | public static ENOTDIR(path: string): ApiError { 94 | return this.FileError(ErrorCode.ENOTDIR, path); 95 | } 96 | 97 | public static EPERM(path: string): ApiError { 98 | return this.FileError(ErrorCode.EPERM, path); 99 | } 100 | 101 | public static ENOTEMPTY(path: string): ApiError { 102 | return this.FileError(ErrorCode.ENOTEMPTY, path); 103 | } 104 | 105 | public errno: ErrorCode; 106 | public code: string; 107 | public path?: string; 108 | // Unsupported. 109 | public syscall: string = ''; 110 | public stack?: string; 111 | 112 | /** 113 | * Represents a BrowserFS error. Passed back to applications after a failed 114 | * call to the BrowserFS API. 115 | * 116 | * Error codes mirror those returned by regular Unix file operations, which is 117 | * what Node returns. 118 | * @constructor ApiError 119 | * @param type The type of the error. 120 | * @param [message] A descriptive error message. 121 | */ 122 | constructor(type: ErrorCode, message: string = ErrorStrings[type], path?: string) { 123 | super(message); 124 | this.errno = type; 125 | this.code = ErrorCode[type]; 126 | this.path = path; 127 | this.message = `Error: ${this.code}: ${message}${this.path ? `, '${this.path}'` : ''}`; 128 | } 129 | 130 | /** 131 | * @return A friendly error message. 132 | */ 133 | public toString(): string { 134 | return this.message; 135 | } 136 | 137 | public toJSON(): any { 138 | return { 139 | errno: this.errno, 140 | code: this.code, 141 | path: this.path, 142 | stack: this.stack, 143 | message: this.message, 144 | }; 145 | } 146 | 147 | /** 148 | * Writes the API error into a buffer. 149 | */ 150 | public writeToBuffer(buffer: Buffer = Buffer.alloc(this.bufferSize()), i: number = 0): Buffer { 151 | const bytesWritten = buffer.write(JSON.stringify(this.toJSON()), i + 4); 152 | buffer.writeUInt32LE(bytesWritten, i); 153 | return buffer; 154 | } 155 | 156 | /** 157 | * The size of the API error in buffer-form in bytes. 158 | */ 159 | public bufferSize(): number { 160 | // 4 bytes for string length. 161 | return 4 + Buffer.byteLength(JSON.stringify(this.toJSON())); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/backends/AsyncMirror.ts: -------------------------------------------------------------------------------- 1 | import { type FileSystem, SynchronousFileSystem, FileSystemMetadata } from '../filesystem'; 2 | import { ApiError, ErrorCode } from '../ApiError'; 3 | import { File, FileFlag } from '../file'; 4 | import { Stats } from '../stats'; 5 | import PreloadFile from '../generic/preload_file'; 6 | import * as path from 'path'; 7 | import { Cred } from '../cred'; 8 | import { CreateBackend, type BackendOptions } from './backend'; 9 | 10 | /** 11 | * @hidden 12 | */ 13 | interface AsyncOperation { 14 | apiMethod: string; 15 | arguments: any[]; 16 | } 17 | 18 | /** 19 | * We define our own file to interpose on syncSync() for mirroring purposes. 20 | */ 21 | class MirrorFile extends PreloadFile implements File { 22 | constructor(fs: AsyncMirror, path: string, flag: FileFlag, stat: Stats, data: Buffer) { 23 | super(fs, path, flag, stat, data); 24 | } 25 | 26 | public syncSync(): void { 27 | if (this.isDirty()) { 28 | this._fs._syncSync(this); 29 | this.resetDirty(); 30 | } 31 | } 32 | 33 | public closeSync(): void { 34 | this.syncSync(); 35 | } 36 | } 37 | 38 | export namespace AsyncMirror { 39 | /** 40 | * Configuration options for the AsyncMirror file system. 41 | */ 42 | export interface Options { 43 | /** 44 | * The synchronous file system to mirror the asynchronous file system to. 45 | */ 46 | sync: FileSystem; 47 | /** 48 | * The asynchronous file system to mirror. 49 | */ 50 | async: FileSystem; 51 | } 52 | } 53 | 54 | /** 55 | * AsyncMirrorFS mirrors a synchronous filesystem into an asynchronous filesystem 56 | * by: 57 | * 58 | * * Performing operations over the in-memory copy, while asynchronously pipelining them 59 | * to the backing store. 60 | * * During application loading, the contents of the async file system can be reloaded into 61 | * the synchronous store, if desired. 62 | * 63 | * The two stores will be kept in sync. The most common use-case is to pair a synchronous 64 | * in-memory filesystem with an asynchronous backing store. 65 | * 66 | * Example: Mirroring an IndexedDB file system to an in memory file system. Now, you can use 67 | * IndexedDB synchronously. 68 | * 69 | * ```javascript 70 | * BrowserFS.configure({ 71 | * fs: "AsyncMirror", 72 | * options: { 73 | * sync: { fs: "InMemory" }, 74 | * async: { fs: "IndexedDB" } 75 | * } 76 | * }, function(e) { 77 | * // BrowserFS is initialized and ready-to-use! 78 | * }); 79 | * ``` 80 | * 81 | * Or, alternatively: 82 | * 83 | * ```javascript 84 | * BrowserFS.Backend.IndexedDB.Create(function(e, idbfs) { 85 | * BrowserFS.Backend.InMemory.Create(function(e, inMemory) { 86 | * BrowserFS.Backend.AsyncMirror({ 87 | * sync: inMemory, async: idbfs 88 | * }, function(e, mirrored) { 89 | * BrowserFS.initialize(mirrored); 90 | * }); 91 | * }); 92 | * }); 93 | * ``` 94 | */ 95 | export class AsyncMirror extends SynchronousFileSystem { 96 | public static readonly Name = 'AsyncMirror'; 97 | 98 | public static Create = CreateBackend.bind(this); 99 | 100 | public static readonly Options: BackendOptions = { 101 | sync: { 102 | type: 'object', 103 | description: 'The synchronous file system to mirror the asynchronous file system to.', 104 | validator: async (v: FileSystem): Promise => { 105 | if (!v?.metadata.synchronous) { 106 | throw new ApiError(ErrorCode.EINVAL, `'sync' option must be a file system that supports synchronous operations`); 107 | } 108 | }, 109 | }, 110 | async: { 111 | type: 'object', 112 | description: 'The asynchronous file system to mirror.', 113 | }, 114 | }; 115 | 116 | public static isAvailable(): boolean { 117 | return true; 118 | } 119 | 120 | /** 121 | * Queue of pending asynchronous operations. 122 | */ 123 | private _queue: AsyncOperation[] = []; 124 | private _queueRunning: boolean = false; 125 | private _sync: FileSystem; 126 | private _async: FileSystem; 127 | private _isInitialized: boolean = false; 128 | private _initializeCallbacks: ((e?: ApiError) => void)[] = []; 129 | 130 | /** 131 | * 132 | * Mirrors the synchronous file system into the asynchronous file system. 133 | * 134 | * @param sync The synchronous file system to mirror the asynchronous file system to. 135 | * @param async The asynchronous file system to mirror. 136 | */ 137 | constructor({ sync, async }: AsyncMirror.Options) { 138 | super(); 139 | this._sync = sync; 140 | this._async = async; 141 | this._ready = this._initialize(); 142 | } 143 | 144 | public get metadata(): FileSystemMetadata { 145 | return { 146 | ...super.metadata, 147 | name: AsyncMirror.Name, 148 | synchronous: true, 149 | supportsProperties: this._sync.metadata.supportsProperties && this._async.metadata.supportsProperties, 150 | }; 151 | } 152 | 153 | public _syncSync(fd: PreloadFile) { 154 | const stats = fd.getStats(); 155 | this._sync.writeFileSync(fd.getPath(), fd.getBuffer(), null, FileFlag.getFileFlag('w'), stats.mode, stats.getCred(0, 0)); 156 | this.enqueueOp({ 157 | apiMethod: 'writeFile', 158 | arguments: [fd.getPath(), fd.getBuffer(), null, fd.getFlag(), stats.mode, stats.getCred(0, 0)], 159 | }); 160 | } 161 | 162 | public renameSync(oldPath: string, newPath: string, cred: Cred): void { 163 | this._sync.renameSync(oldPath, newPath, cred); 164 | this.enqueueOp({ 165 | apiMethod: 'rename', 166 | arguments: [oldPath, newPath, cred], 167 | }); 168 | } 169 | 170 | public statSync(p: string, cred: Cred): Stats { 171 | return this._sync.statSync(p, cred); 172 | } 173 | 174 | public openSync(p: string, flag: FileFlag, mode: number, cred: Cred): File { 175 | // Sanity check: Is this open/close permitted? 176 | const fd = this._sync.openSync(p, flag, mode, cred); 177 | fd.closeSync(); 178 | return new MirrorFile(this, p, flag, this._sync.statSync(p, cred), this._sync.readFileSync(p, null, FileFlag.getFileFlag('r'), cred)); 179 | } 180 | 181 | public unlinkSync(p: string, cred: Cred): void { 182 | this._sync.unlinkSync(p, cred); 183 | this.enqueueOp({ 184 | apiMethod: 'unlink', 185 | arguments: [p, cred], 186 | }); 187 | } 188 | 189 | public rmdirSync(p: string, cred: Cred): void { 190 | this._sync.rmdirSync(p, cred); 191 | this.enqueueOp({ 192 | apiMethod: 'rmdir', 193 | arguments: [p, cred], 194 | }); 195 | } 196 | 197 | public mkdirSync(p: string, mode: number, cred: Cred): void { 198 | this._sync.mkdirSync(p, mode, cred); 199 | this.enqueueOp({ 200 | apiMethod: 'mkdir', 201 | arguments: [p, mode, cred], 202 | }); 203 | } 204 | 205 | public readdirSync(p: string, cred: Cred): string[] { 206 | return this._sync.readdirSync(p, cred); 207 | } 208 | 209 | public existsSync(p: string, cred: Cred): boolean { 210 | return this._sync.existsSync(p, cred); 211 | } 212 | 213 | public chmodSync(p: string, mode: number, cred: Cred): void { 214 | this._sync.chmodSync(p, mode, cred); 215 | this.enqueueOp({ 216 | apiMethod: 'chmod', 217 | arguments: [p, mode, cred], 218 | }); 219 | } 220 | 221 | public chownSync(p: string, new_uid: number, new_gid: number, cred: Cred): void { 222 | this._sync.chownSync(p, new_uid, new_gid, cred); 223 | this.enqueueOp({ 224 | apiMethod: 'chown', 225 | arguments: [p, new_uid, new_gid, cred], 226 | }); 227 | } 228 | 229 | public utimesSync(p: string, atime: Date, mtime: Date, cred: Cred): void { 230 | this._sync.utimesSync(p, atime, mtime, cred); 231 | this.enqueueOp({ 232 | apiMethod: 'utimes', 233 | arguments: [p, atime, mtime, cred], 234 | }); 235 | } 236 | 237 | /** 238 | * Called once to load up files from async storage into sync storage. 239 | */ 240 | private async _initialize(): Promise { 241 | if (!this._isInitialized) { 242 | // First call triggers initialization, the rest wait. 243 | const copyDirectory = async (p: string, mode: number): Promise => { 244 | if (p !== '/') { 245 | const stats = await this._async.stat(p, Cred.Root); 246 | this._sync.mkdirSync(p, mode, stats.getCred()); 247 | } 248 | const files = await this._async.readdir(p, Cred.Root); 249 | for (const file of files) { 250 | await copyItem(path.join(p, file)); 251 | } 252 | }, 253 | copyFile = async (p: string, mode: number): Promise => { 254 | const data = await this._async.readFile(p, null, FileFlag.getFileFlag('r'), Cred.Root); 255 | this._sync.writeFileSync(p, data, null, FileFlag.getFileFlag('w'), mode, Cred.Root); 256 | }, 257 | copyItem = async (p: string): Promise => { 258 | const stats = await this._async.stat(p, Cred.Root); 259 | if (stats.isDirectory()) { 260 | await copyDirectory(p, stats.mode); 261 | } else { 262 | await copyFile(p, stats.mode); 263 | } 264 | }; 265 | try { 266 | await copyDirectory('/', 0); 267 | this._isInitialized = true; 268 | } catch (e) { 269 | this._isInitialized = false; 270 | throw e; 271 | } 272 | } 273 | return this; 274 | } 275 | 276 | private enqueueOp(op: AsyncOperation) { 277 | this._queue.push(op); 278 | if (!this._queueRunning) { 279 | this._queueRunning = true; 280 | const doNextOp = (err?: ApiError) => { 281 | if (err) { 282 | throw new Error(`WARNING: File system has desynchronized. Received following error: ${err}\n$`); 283 | } 284 | if (this._queue.length > 0) { 285 | const op = this._queue.shift()!; 286 | op.arguments.push(doNextOp); 287 | (this._async[op.apiMethod]).apply(this._async, op.arguments); 288 | } else { 289 | this._queueRunning = false; 290 | } 291 | }; 292 | doNextOp(); 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/backends/Emscripten.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemMetadata, SynchronousFileSystem } from '../filesystem'; 2 | import { Stats, FileType } from '../stats'; 3 | import { BaseFile, File, FileFlag } from '../file'; 4 | import { ApiError, ErrorCode, ErrorStrings } from '../ApiError'; 5 | import { EmscriptenEntry } from '../generic/emscripten_fs'; 6 | import { Cred } from '../cred'; 7 | import { Buffer } from 'buffer'; 8 | import { CreateBackend, type BackendOptions } from './backend'; 9 | 10 | /** 11 | * @hidden 12 | */ 13 | interface EmscriptenError { 14 | node: EmscriptenEntry; 15 | errno: number; 16 | } 17 | 18 | /** 19 | * @hidden 20 | */ 21 | function convertError(e: EmscriptenError, path: string = ''): ApiError { 22 | const errno = e.errno; 23 | let parent = e.node; 24 | const paths: string[] = []; 25 | while (parent) { 26 | paths.unshift(parent.name); 27 | if (parent === parent.parent) { 28 | break; 29 | } 30 | parent = parent.parent; 31 | } 32 | return new ApiError(errno, ErrorStrings[errno], paths.length > 0 ? '/' + paths.join('/') : path); 33 | } 34 | 35 | export class EmscriptenFile extends BaseFile implements File { 36 | constructor(private _fs: EmscriptenFileSystem, private _FS: any, private _path: string, private _stream: any) { 37 | super(); 38 | } 39 | public getPos(): number | undefined { 40 | return undefined; 41 | } 42 | public async close(): Promise { 43 | return this.closeSync(); 44 | } 45 | public closeSync(): void { 46 | try { 47 | this._FS.close(this._stream); 48 | } catch (e) { 49 | throw convertError(e, this._path); 50 | } 51 | } 52 | public async stat(): Promise { 53 | return this.statSync(); 54 | } 55 | public statSync(): Stats { 56 | try { 57 | return this._fs.statSync(this._path, Cred.Root); 58 | } catch (e) { 59 | throw convertError(e, this._path); 60 | } 61 | } 62 | public async truncate(len: number): Promise { 63 | return this.truncateSync(len); 64 | } 65 | public truncateSync(len: number): void { 66 | try { 67 | this._FS.ftruncate(this._stream.fd, len); 68 | } catch (e) { 69 | throw convertError(e, this._path); 70 | } 71 | } 72 | public async write(buffer: Buffer, offset: number, length: number, position: number): Promise { 73 | return this.writeSync(buffer, offset, length, position); 74 | } 75 | public writeSync(buffer: Buffer, offset: number, length: number, position: number | null): number { 76 | try { 77 | // Emscripten is particular about what position is set to. 78 | const emPosition = position === null ? undefined : position; 79 | return this._FS.write(this._stream, buffer, offset, length, emPosition); 80 | } catch (e) { 81 | throw convertError(e, this._path); 82 | } 83 | } 84 | public async read(buffer: Buffer, offset: number, length: number, position: number): Promise<{ bytesRead: number; buffer: Buffer }> { 85 | return { bytesRead: this.readSync(buffer, offset, length, position), buffer }; 86 | } 87 | public readSync(buffer: Buffer, offset: number, length: number, position: number | null): number { 88 | try { 89 | // Emscripten is particular about what position is set to. 90 | const emPosition = position === null ? undefined : position; 91 | return this._FS.read(this._stream, buffer, offset, length, emPosition); 92 | } catch (e) { 93 | throw convertError(e, this._path); 94 | } 95 | } 96 | public async sync(): Promise { 97 | this.syncSync(); 98 | } 99 | public syncSync(): void { 100 | // NOP. 101 | } 102 | public async chown(uid: number, gid: number): Promise { 103 | return this.chownSync(uid, gid); 104 | } 105 | public chownSync(uid: number, gid: number): void { 106 | try { 107 | this._FS.fchown(this._stream.fd, uid, gid); 108 | } catch (e) { 109 | throw convertError(e, this._path); 110 | } 111 | } 112 | public async chmod(mode: number): Promise { 113 | return this.chmodSync(mode); 114 | } 115 | public chmodSync(mode: number): void { 116 | try { 117 | this._FS.fchmod(this._stream.fd, mode); 118 | } catch (e) { 119 | throw convertError(e, this._path); 120 | } 121 | } 122 | public async utimes(atime: Date, mtime: Date): Promise { 123 | return this.utimesSync(atime, mtime); 124 | } 125 | public utimesSync(atime: Date, mtime: Date): void { 126 | this._fs.utimesSync(this._path, atime, mtime, Cred.Root); 127 | } 128 | } 129 | 130 | export namespace EmscriptenFileSystem { 131 | /** 132 | * Configuration options for Emscripten file systems. 133 | */ 134 | export interface Options { 135 | // The Emscripten file system to use (`FS`) 136 | FS: any; 137 | } 138 | } 139 | 140 | /** 141 | * Mounts an Emscripten file system into the BrowserFS file system. 142 | */ 143 | export class EmscriptenFileSystem extends SynchronousFileSystem { 144 | public static readonly Name = 'EmscriptenFileSystem'; 145 | 146 | public static Create = CreateBackend.bind(this); 147 | 148 | public static readonly Options: BackendOptions = { 149 | FS: { 150 | type: 'object', 151 | description: 'The Emscripten file system to use (the `FS` variable)', 152 | }, 153 | }; 154 | 155 | public static isAvailable(): boolean { 156 | return true; 157 | } 158 | 159 | private _FS: any; 160 | 161 | public constructor({ FS }: EmscriptenFileSystem.Options) { 162 | super(); 163 | this._FS = FS; 164 | } 165 | 166 | public get metadata(): FileSystemMetadata { 167 | return { 168 | ...super.metadata, 169 | name: this._FS.DB_NAME(), 170 | supportsProperties: true, 171 | supportsLinks: true, 172 | }; 173 | } 174 | 175 | public renameSync(oldPath: string, newPath: string, cred: Cred): void { 176 | try { 177 | this._FS.rename(oldPath, newPath); 178 | } catch (e) { 179 | if (e.errno === ErrorCode.ENOENT) { 180 | throw convertError(e, this.existsSync(oldPath, cred) ? newPath : oldPath); 181 | } else { 182 | throw convertError(e); 183 | } 184 | } 185 | } 186 | 187 | public statSync(p: string, cred: Cred): Stats { 188 | try { 189 | const stats = this._FS.stat(p); 190 | const itemType = this.modeToFileType(stats.mode); 191 | return new Stats(itemType, stats.size, stats.mode, stats.atime.getTime(), stats.mtime.getTime(), stats.ctime.getTime()); 192 | } catch (e) { 193 | throw convertError(e, p); 194 | } 195 | } 196 | 197 | public openSync(p: string, flag: FileFlag, mode: number, cred: Cred): EmscriptenFile { 198 | try { 199 | const stream = this._FS.open(p, flag.getFlagString(), mode); 200 | return new EmscriptenFile(this, this._FS, p, stream); 201 | } catch (e) { 202 | throw convertError(e, p); 203 | } 204 | } 205 | 206 | public unlinkSync(p: string, cred: Cred): void { 207 | try { 208 | this._FS.unlink(p); 209 | } catch (e) { 210 | throw convertError(e, p); 211 | } 212 | } 213 | 214 | public rmdirSync(p: string, cred: Cred): void { 215 | try { 216 | this._FS.rmdir(p); 217 | } catch (e) { 218 | throw convertError(e, p); 219 | } 220 | } 221 | 222 | public mkdirSync(p: string, mode: number, cred: Cred): void { 223 | try { 224 | this._FS.mkdir(p, mode); 225 | } catch (e) { 226 | throw convertError(e, p); 227 | } 228 | } 229 | 230 | public readdirSync(p: string, cred: Cred): string[] { 231 | try { 232 | // Emscripten returns items for '.' and '..'. Node does not. 233 | return this._FS.readdir(p).filter((p: string) => p !== '.' && p !== '..'); 234 | } catch (e) { 235 | throw convertError(e, p); 236 | } 237 | } 238 | 239 | public truncateSync(p: string, len: number, cred: Cred): void { 240 | try { 241 | this._FS.truncate(p, len); 242 | } catch (e) { 243 | throw convertError(e, p); 244 | } 245 | } 246 | 247 | public readFileSync(p: string, encoding: BufferEncoding, flag: FileFlag, cred: Cred): any { 248 | try { 249 | const data: Uint8Array = this._FS.readFile(p, { flags: flag.getFlagString() }); 250 | const buff = Buffer.from(data); 251 | if (encoding) { 252 | return buff.toString(encoding); 253 | } else { 254 | return buff; 255 | } 256 | } catch (e) { 257 | throw convertError(e, p); 258 | } 259 | } 260 | 261 | public writeFileSync(p: string, data: any, encoding: BufferEncoding, flag: FileFlag, mode: number, cred: Cred): void { 262 | try { 263 | if (encoding) { 264 | data = Buffer.from(data, encoding); 265 | } 266 | this._FS.writeFile(p, data, { flags: flag.getFlagString(), encoding: 'binary' }); 267 | this._FS.chmod(p, mode); 268 | } catch (e) { 269 | throw convertError(e, p); 270 | } 271 | } 272 | 273 | public chmodSync(p: string, mode: number, cred: Cred) { 274 | try { 275 | this._FS.chmod(p, mode); 276 | } catch (e) { 277 | throw convertError(e, p); 278 | } 279 | } 280 | 281 | public chownSync(p: string, new_uid: number, new_gid: number, cred: Cred): void { 282 | try { 283 | this._FS.chown(p, new_uid, new_gid); 284 | } catch (e) { 285 | throw convertError(e, p); 286 | } 287 | } 288 | 289 | public symlinkSync(srcpath: string, dstpath: string, type: string, cred: Cred): void { 290 | try { 291 | this._FS.symlink(srcpath, dstpath); 292 | } catch (e) { 293 | throw convertError(e); 294 | } 295 | } 296 | 297 | public readlinkSync(p: string, cred: Cred): string { 298 | try { 299 | return this._FS.readlink(p); 300 | } catch (e) { 301 | throw convertError(e, p); 302 | } 303 | } 304 | 305 | public utimesSync(p: string, atime: Date, mtime: Date, cred: Cred): void { 306 | try { 307 | this._FS.utime(p, atime.getTime(), mtime.getTime()); 308 | } catch (e) { 309 | throw convertError(e, p); 310 | } 311 | } 312 | 313 | private modeToFileType(mode: number): FileType { 314 | if (this._FS.isDir(mode)) { 315 | return FileType.DIRECTORY; 316 | } else if (this._FS.isFile(mode)) { 317 | return FileType.FILE; 318 | } else if (this._FS.isLink(mode)) { 319 | return FileType.SYMLINK; 320 | } else { 321 | throw ApiError.EPERM(`Invalid mode: ${mode}`); 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/backends/FileSystemAccess.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { basename, dirname, join } from 'path'; 3 | import { ApiError, ErrorCode } from '../ApiError'; 4 | import { Cred } from '../cred'; 5 | import { File, FileFlag } from '../file'; 6 | import { BaseFileSystem, FileSystemMetadata, type FileSystem } from '../filesystem'; 7 | import { Stats, FileType } from '../stats'; 8 | import PreloadFile from '../generic/preload_file'; 9 | import { Buffer } from 'buffer'; 10 | import { CreateBackend, type BackendOptions } from './backend'; 11 | 12 | interface FileSystemAccessFileSystemOptions { 13 | handle: FileSystemDirectoryHandle; 14 | } 15 | 16 | const handleError = (path = '', error: Error) => { 17 | if (error.name === 'NotFoundError') { 18 | throw ApiError.ENOENT(path); 19 | } 20 | 21 | throw error as ApiError; 22 | }; 23 | 24 | export class FileSystemAccessFile extends PreloadFile implements File { 25 | constructor(_fs: FileSystemAccessFileSystem, _path: string, _flag: FileFlag, _stat: Stats, contents?: Buffer) { 26 | super(_fs, _path, _flag, _stat, contents); 27 | } 28 | 29 | public async sync(): Promise { 30 | if (this.isDirty()) { 31 | await this._fs._sync(this.getPath(), this.getBuffer(), this.getStats(), Cred.Root); 32 | this.resetDirty(); 33 | } 34 | } 35 | 36 | public async close(): Promise { 37 | await this.sync(); 38 | } 39 | } 40 | 41 | export class FileSystemAccessFileSystem extends BaseFileSystem { 42 | public static readonly Name = 'FileSystemAccess'; 43 | 44 | public static Create = CreateBackend.bind(this); 45 | 46 | public static readonly Options: BackendOptions = {}; 47 | 48 | public static isAvailable(): boolean { 49 | return typeof FileSystemHandle === 'function'; 50 | } 51 | 52 | private _handles: { [path: string]: FileSystemHandle }; 53 | 54 | public constructor({ handle }: FileSystemAccessFileSystemOptions) { 55 | super(); 56 | this._handles = { '/': handle }; 57 | } 58 | 59 | public get metadata(): FileSystemMetadata { 60 | return { 61 | ...super.metadata, 62 | name: FileSystemAccessFileSystem.Name, 63 | }; 64 | } 65 | 66 | public async _sync(p: string, data: Buffer, stats: Stats, cred: Cred): Promise { 67 | const currentStats = await this.stat(p, cred); 68 | if (stats.mtime !== currentStats!.mtime) { 69 | await this.writeFile(p, data, null, FileFlag.getFileFlag('w'), currentStats!.mode, cred); 70 | } 71 | } 72 | 73 | public async rename(oldPath: string, newPath: string, cred: Cred): Promise { 74 | try { 75 | const handle = await this.getHandle(oldPath); 76 | if (handle instanceof FileSystemDirectoryHandle) { 77 | const files = await this.readdir(oldPath, cred); 78 | 79 | await this.mkdir(newPath, 'wx', cred); 80 | if (files.length === 0) { 81 | await this.unlink(oldPath, cred); 82 | } else { 83 | for (const file of files) { 84 | await this.rename(join(oldPath, file), join(newPath, file), cred); 85 | await this.unlink(oldPath, cred); 86 | } 87 | } 88 | } 89 | if (handle instanceof FileSystemFileHandle) { 90 | const oldFile = await handle.getFile(), 91 | destFolder = await this.getHandle(dirname(newPath)); 92 | if (destFolder instanceof FileSystemDirectoryHandle) { 93 | const newFile = await destFolder.getFileHandle(basename(newPath), { create: true }); 94 | const writable = await newFile.createWritable(); 95 | const buffer = await oldFile.arrayBuffer(); 96 | await writable.write(buffer); 97 | 98 | writable.close(); 99 | await this.unlink(oldPath, cred); 100 | } 101 | } 102 | } catch (err) { 103 | handleError(oldPath, err); 104 | } 105 | } 106 | 107 | public async writeFile(fname: string, data: any, encoding: string | null, flag: FileFlag, mode: number, cred: Cred, createFile?: boolean): Promise { 108 | const handle = await this.getHandle(dirname(fname)); 109 | if (handle instanceof FileSystemDirectoryHandle) { 110 | const file = await handle.getFileHandle(basename(fname), { create: true }); 111 | const writable = await file.createWritable(); 112 | await writable.write(data); 113 | await writable.close(); 114 | //return createFile ? this.newFile(fname, flag, data) : undefined; 115 | } 116 | } 117 | 118 | public async createFile(p: string, flag: FileFlag, mode: number, cred: Cred): Promise { 119 | await this.writeFile(p, Buffer.alloc(0), null, flag, mode, cred, true); 120 | return this.openFile(p, flag, cred); 121 | } 122 | 123 | public async stat(path: string, cred: Cred): Promise { 124 | const handle = await this.getHandle(path); 125 | if (!handle) { 126 | throw ApiError.FileError(ErrorCode.EINVAL, path); 127 | } 128 | if (handle instanceof FileSystemDirectoryHandle) { 129 | return new Stats(FileType.DIRECTORY, 4096); 130 | } 131 | if (handle instanceof FileSystemFileHandle) { 132 | const { lastModified, size } = await handle.getFile(); 133 | return new Stats(FileType.FILE, size, undefined, undefined, lastModified); 134 | } 135 | } 136 | 137 | public async exists(p: string, cred: Cred): Promise { 138 | try { 139 | await this.getHandle(p); 140 | return true; 141 | } catch (e) { 142 | return false; 143 | } 144 | } 145 | 146 | public async openFile(path: string, flags: FileFlag, cred: Cred): Promise { 147 | const handle = await this.getHandle(path); 148 | if (handle instanceof FileSystemFileHandle) { 149 | const file = await handle.getFile(); 150 | const buffer = await file.arrayBuffer(); 151 | return this.newFile(path, flags, buffer, file.size, file.lastModified); 152 | } 153 | } 154 | 155 | public async unlink(path: string, cred: Cred): Promise { 156 | const handle = await this.getHandle(dirname(path)); 157 | if (handle instanceof FileSystemDirectoryHandle) { 158 | try { 159 | await handle.removeEntry(basename(path), { recursive: true }); 160 | } catch (e) { 161 | handleError(path, e); 162 | } 163 | } 164 | } 165 | 166 | public async rmdir(path: string, cred: Cred): Promise { 167 | return this.unlink(path, cred); 168 | } 169 | 170 | public async mkdir(p: string, mode: any, cred: Cred): Promise { 171 | const overwrite = mode && mode.flag && mode.flag.includes('w') && !mode.flag.includes('x'); 172 | 173 | const existingHandle = await this.getHandle(p); 174 | if (existingHandle && !overwrite) { 175 | throw ApiError.EEXIST(p); 176 | } 177 | 178 | const handle = await this.getHandle(dirname(p)); 179 | if (handle instanceof FileSystemDirectoryHandle) { 180 | await handle.getDirectoryHandle(basename(p), { create: true }); 181 | } 182 | } 183 | 184 | public async readdir(path: string, cred: Cred): Promise { 185 | const handle = await this.getHandle(path); 186 | if (handle instanceof FileSystemDirectoryHandle) { 187 | const _keys: string[] = []; 188 | for await (const key of handle.keys()) { 189 | _keys.push(join(path, key)); 190 | } 191 | return _keys; 192 | } 193 | } 194 | 195 | private newFile(path: string, flag: FileFlag, data: ArrayBuffer, size?: number, lastModified?: number): File { 196 | return new FileSystemAccessFile(this, path, flag, new Stats(FileType.FILE, size || 0, undefined, undefined, lastModified || new Date().getTime()), Buffer.from(data)); 197 | } 198 | 199 | private async getHandle(path: string): Promise { 200 | if (path === '/') { 201 | return this._handles['/']; 202 | } 203 | 204 | let walkedPath = '/'; 205 | const [, ...pathParts] = path.split('/'); 206 | const getHandleParts = async ([pathPart, ...remainingPathParts]: string[]) => { 207 | const walkingPath = join(walkedPath, pathPart); 208 | const continueWalk = (handle: FileSystemHandle) => { 209 | walkedPath = walkingPath; 210 | this._handles[walkedPath] = handle; 211 | 212 | if (remainingPathParts.length === 0) { 213 | return this._handles[path]; 214 | } 215 | 216 | getHandleParts(remainingPathParts); 217 | }; 218 | const handle = this._handles[walkedPath] as FileSystemDirectoryHandle; 219 | 220 | try { 221 | return await continueWalk(await handle.getDirectoryHandle(pathPart)); 222 | } catch (error) { 223 | if (error.name === 'TypeMismatchError') { 224 | try { 225 | return await continueWalk(await handle.getFileHandle(pathPart)); 226 | } catch (err) { 227 | handleError(walkingPath, err); 228 | } 229 | } else if (error.message === 'Name is not allowed.') { 230 | throw new ApiError(ErrorCode.ENOENT, error.message, walkingPath); 231 | } else { 232 | handleError(walkingPath, error); 233 | } 234 | } 235 | }; 236 | 237 | getHandleParts(pathParts); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/backends/FolderAdapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseFileSystem, type FileSystem } from '../filesystem'; 2 | import * as path from 'path'; 3 | import { ApiError } from '../ApiError'; 4 | import { Cred } from '../cred'; 5 | import { CreateBackend, type BackendOptions } from './backend'; 6 | 7 | export namespace FolderAdapter { 8 | /** 9 | * Configuration options for a FolderAdapter file system. 10 | */ 11 | export interface Options { 12 | // The folder to use as the root directory. 13 | folder: string; 14 | // The file system to wrap. 15 | wrapped: FileSystem; 16 | } 17 | } 18 | 19 | /** 20 | * The FolderAdapter file system wraps a file system, and scopes all interactions to a subfolder of that file system. 21 | * 22 | * Example: Given a file system `foo` with folder `bar` and file `bar/baz`... 23 | * 24 | * ```javascript 25 | * BrowserFS.configure({ 26 | * fs: "FolderAdapter", 27 | * options: { 28 | * folder: "bar", 29 | * wrapped: foo 30 | * } 31 | * }, function(e) { 32 | * var fs = BrowserFS.BFSRequire('fs'); 33 | * fs.readdirSync('/'); // ['baz'] 34 | * }); 35 | * ``` 36 | */ 37 | export class FolderAdapter extends BaseFileSystem { 38 | public static readonly Name = 'FolderAdapter'; 39 | 40 | public static Create = CreateBackend.bind(this); 41 | 42 | public static readonly Options: BackendOptions = { 43 | folder: { 44 | type: 'string', 45 | description: 'The folder to use as the root directory', 46 | }, 47 | wrapped: { 48 | type: 'object', 49 | description: 'The file system to wrap', 50 | }, 51 | }; 52 | 53 | public static isAvailable(): boolean { 54 | return true; 55 | } 56 | 57 | public _wrapped: FileSystem; 58 | public _folder: string; 59 | 60 | public constructor({ folder, wrapped }: FolderAdapter.Options) { 61 | super(); 62 | this._folder = folder; 63 | this._wrapped = wrapped; 64 | this._ready = this._initialize(); 65 | } 66 | 67 | public get metadata() { 68 | return { ...super.metadata, ...this._wrapped.metadata, supportsLinks: false }; 69 | } 70 | 71 | /** 72 | * Initialize the file system. Ensures that the wrapped file system 73 | * has the given folder. 74 | */ 75 | private async _initialize(): Promise { 76 | const exists = await this._wrapped.exists(this._folder, Cred.Root); 77 | if (!exists && this._wrapped.metadata.readonly) { 78 | throw ApiError.ENOENT(this._folder); 79 | } 80 | await this._wrapped.mkdir(this._folder, 0o777, Cred.Root); 81 | return this; 82 | } 83 | } 84 | 85 | /** 86 | * @hidden 87 | */ 88 | function translateError(folder: string, e: any): any { 89 | if (e !== null && typeof e === 'object') { 90 | const err = e; 91 | let p = err.path; 92 | if (p) { 93 | p = '/' + path.relative(folder, p); 94 | err.message = err.message.replace(err.path!, p); 95 | err.path = p; 96 | } 97 | } 98 | return e; 99 | } 100 | 101 | /** 102 | * @hidden 103 | */ 104 | function wrapCallback(folder: string, cb: any): any { 105 | if (typeof cb === 'function') { 106 | return function (err: ApiError) { 107 | if (arguments.length > 0) { 108 | arguments[0] = translateError(folder, err); 109 | } 110 | (cb).apply(null, arguments); 111 | }; 112 | } else { 113 | return cb; 114 | } 115 | } 116 | 117 | /** 118 | * @hidden 119 | */ 120 | function wrapFunction(name: string, wrapFirst: boolean, wrapSecond: boolean): Function { 121 | if (name.slice(name.length - 4) !== 'Sync') { 122 | // Async function. Translate error in callback. 123 | return function (this: FolderAdapter) { 124 | if (arguments.length > 0) { 125 | if (wrapFirst) { 126 | arguments[0] = path.join(this._folder, arguments[0]); 127 | } 128 | if (wrapSecond) { 129 | arguments[1] = path.join(this._folder, arguments[1]); 130 | } 131 | arguments[arguments.length - 1] = wrapCallback(this._folder, arguments[arguments.length - 1]); 132 | } 133 | return (this._wrapped)[name].apply(this._wrapped, arguments); 134 | }; 135 | } else { 136 | // Sync function. Translate error in catch. 137 | return function (this: FolderAdapter) { 138 | try { 139 | if (wrapFirst) { 140 | arguments[0] = path.join(this._folder, arguments[0]); 141 | } 142 | if (wrapSecond) { 143 | arguments[1] = path.join(this._folder, arguments[1]); 144 | } 145 | return (this._wrapped)[name].apply(this._wrapped, arguments); 146 | } catch (e) { 147 | throw translateError(this._folder, e); 148 | } 149 | }; 150 | } 151 | } 152 | 153 | // First argument is a path. 154 | [ 155 | 'diskSpace', 156 | 'stat', 157 | 'statSync', 158 | 'open', 159 | 'openSync', 160 | 'unlink', 161 | 'unlinkSync', 162 | 'rmdir', 163 | 'rmdirSync', 164 | 'mkdir', 165 | 'mkdirSync', 166 | 'readdir', 167 | 'readdirSync', 168 | 'exists', 169 | 'existsSync', 170 | 'realpath', 171 | 'realpathSync', 172 | 'truncate', 173 | 'truncateSync', 174 | 'readFile', 175 | 'readFileSync', 176 | 'writeFile', 177 | 'writeFileSync', 178 | 'appendFile', 179 | 'appendFileSync', 180 | 'chmod', 181 | 'chmodSync', 182 | 'chown', 183 | 'chownSync', 184 | 'utimes', 185 | 'utimesSync', 186 | 'readlink', 187 | 'readlinkSync', 188 | ].forEach((name: string) => { 189 | (FolderAdapter.prototype)[name] = wrapFunction(name, true, false); 190 | }); 191 | 192 | // First and second arguments are paths. 193 | ['rename', 'renameSync', 'link', 'linkSync', 'symlink', 'symlinkSync'].forEach((name: string) => { 194 | (FolderAdapter.prototype)[name] = wrapFunction(name, true, true); 195 | }); 196 | -------------------------------------------------------------------------------- /src/backends/HTTPRequest.ts: -------------------------------------------------------------------------------- 1 | import { BaseFileSystem, type FileSystem, FileContents, FileSystemMetadata } from '../filesystem'; 2 | import { ApiError, ErrorCode } from '../ApiError'; 3 | import { copyingSlice } from '../utils'; 4 | import { File, FileFlag, ActionType } from '../file'; 5 | import { Stats } from '../stats'; 6 | import { NoSyncFile } from '../generic/preload_file'; 7 | import { fetchIsAvailable, fetchFile, fetchFileSize } from '../generic/fetch'; 8 | import { FileIndex, isFileInode, isDirInode } from '../generic/file_index'; 9 | import { Cred } from '../cred'; 10 | import { CreateBackend, type BackendOptions } from './backend'; 11 | import { R_OK } from '../emulation/constants'; 12 | 13 | export interface HTTPRequestIndex { 14 | [key: string]: string; 15 | } 16 | 17 | export namespace HTTPRequest { 18 | /** 19 | * Configuration options for a HTTPRequest file system. 20 | */ 21 | export interface Options { 22 | /** 23 | * URL to a file index as a JSON file or the file index object itself, generated with the make_http_index script. 24 | * Defaults to `index.json`. 25 | */ 26 | index?: string | HTTPRequestIndex; 27 | 28 | /** Used as the URL prefix for fetched files. 29 | * Default: Fetch files relative to the index. 30 | */ 31 | baseUrl?: string; 32 | } 33 | } 34 | 35 | /** 36 | * A simple filesystem backed by HTTP downloads. You must create a directory listing using the 37 | * `make_http_index` tool provided by BrowserFS. 38 | * 39 | * If you install BrowserFS globally with `npm i -g browserfs`, you can generate a listing by 40 | * running `make_http_index` in your terminal in the directory you would like to index: 41 | * 42 | * ``` 43 | * make_http_index > index.json 44 | * ``` 45 | * 46 | * Listings objects look like the following: 47 | * 48 | * ```json 49 | * { 50 | * "home": { 51 | * "jvilk": { 52 | * "someFile.txt": null, 53 | * "someDir": { 54 | * // Empty directory 55 | * } 56 | * } 57 | * } 58 | * } 59 | * ``` 60 | * 61 | * *This example has the folder `/home/jvilk` with subfile `someFile.txt` and subfolder `someDir`.* 62 | */ 63 | export class HTTPRequest extends BaseFileSystem { 64 | public static readonly Name = 'HTTPRequest'; 65 | 66 | public static Create = CreateBackend.bind(this); 67 | 68 | public static readonly Options: BackendOptions = { 69 | index: { 70 | type: ['string', 'object'], 71 | optional: true, 72 | description: 'URL to a file index as a JSON file or the file index object itself, generated with the make_http_index script. Defaults to `index.json`.', 73 | }, 74 | baseUrl: { 75 | type: 'string', 76 | optional: true, 77 | description: 'Used as the URL prefix for fetched files. Default: Fetch files relative to the index.', 78 | }, 79 | }; 80 | 81 | public static isAvailable(): boolean { 82 | return fetchIsAvailable; 83 | } 84 | 85 | public readonly prefixUrl: string; 86 | private _index: FileIndex<{}>; 87 | private _requestFileInternal: typeof fetchFile; 88 | private _requestFileSizeInternal: typeof fetchFileSize; 89 | 90 | constructor({ index, baseUrl = '' }: HTTPRequest.Options) { 91 | super(); 92 | if (!index) { 93 | index = 'index.json'; 94 | } 95 | 96 | const indexRequest = typeof index == 'string' ? fetchFile(index, 'json') : Promise.resolve(index); 97 | this._ready = indexRequest.then(data => { 98 | this._index = FileIndex.fromListing(data); 99 | return this; 100 | }); 101 | 102 | // prefix_url must end in a directory separator. 103 | if (baseUrl.length > 0 && baseUrl.charAt(baseUrl.length - 1) !== '/') { 104 | baseUrl = baseUrl + '/'; 105 | } 106 | this.prefixUrl = baseUrl; 107 | 108 | this._requestFileInternal = fetchFile; 109 | this._requestFileSizeInternal = fetchFileSize; 110 | } 111 | 112 | public get metadata(): FileSystemMetadata { 113 | return { 114 | ...super.metadata, 115 | name: HTTPRequest.Name, 116 | readonly: true, 117 | }; 118 | } 119 | 120 | public empty(): void { 121 | this._index.fileIterator(function (file: Stats) { 122 | file.fileData = null; 123 | }); 124 | } 125 | 126 | /** 127 | * Special HTTPFS function: Preload the given file into the index. 128 | * @param [String] path 129 | * @param [BrowserFS.Buffer] buffer 130 | */ 131 | public preloadFile(path: string, buffer: Buffer): void { 132 | const inode = this._index.getInode(path); 133 | if (isFileInode(inode)) { 134 | if (inode === null) { 135 | throw ApiError.ENOENT(path); 136 | } 137 | const stats = inode.getData(); 138 | stats.size = buffer.length; 139 | stats.fileData = buffer; 140 | } else { 141 | throw ApiError.EISDIR(path); 142 | } 143 | } 144 | 145 | public async stat(path: string, cred: Cred): Promise { 146 | const inode = this._index.getInode(path); 147 | if (inode === null) { 148 | throw ApiError.ENOENT(path); 149 | } 150 | if (!inode.toStats().hasAccess(R_OK, cred)) { 151 | throw ApiError.EACCES(path); 152 | } 153 | let stats: Stats; 154 | if (isFileInode(inode)) { 155 | stats = inode.getData(); 156 | // At this point, a non-opened file will still have default stats from the listing. 157 | if (stats.size < 0) { 158 | stats.size = await this._requestFileSize(path); 159 | } 160 | } else if (isDirInode(inode)) { 161 | stats = inode.getStats(); 162 | } else { 163 | throw ApiError.FileError(ErrorCode.EINVAL, path); 164 | } 165 | return stats; 166 | } 167 | 168 | public async open(path: string, flags: FileFlag, mode: number, cred: Cred): Promise { 169 | // INVARIANT: You can't write to files on this file system. 170 | if (flags.isWriteable()) { 171 | throw new ApiError(ErrorCode.EPERM, path); 172 | } 173 | // Check if the path exists, and is a file. 174 | const inode = this._index.getInode(path); 175 | if (inode === null) { 176 | throw ApiError.ENOENT(path); 177 | } 178 | if (!inode.toStats().hasAccess(flags.getMode(), cred)) { 179 | throw ApiError.EACCES(path); 180 | } 181 | if (isFileInode(inode) || isDirInode(inode)) { 182 | switch (flags.pathExistsAction()) { 183 | case ActionType.THROW_EXCEPTION: 184 | case ActionType.TRUNCATE_FILE: 185 | throw ApiError.EEXIST(path); 186 | case ActionType.NOP: 187 | if (isDirInode(inode)) { 188 | const stats = inode.getStats(); 189 | return new NoSyncFile(this, path, flags, stats, stats.fileData || undefined); 190 | } 191 | const stats = inode.getData(); 192 | // Use existing file contents. 193 | // XXX: Uh, this maintains the previously-used flag. 194 | if (stats.fileData) { 195 | return new NoSyncFile(this, path, flags, Stats.clone(stats), stats.fileData); 196 | } 197 | // @todo be lazier about actually requesting the file 198 | const buffer = await this._requestFile(path, 'buffer'); 199 | // we don't initially have file sizes 200 | stats.size = buffer.length; 201 | stats.fileData = buffer; 202 | return new NoSyncFile(this, path, flags, Stats.clone(stats), buffer); 203 | default: 204 | throw new ApiError(ErrorCode.EINVAL, 'Invalid FileMode object.'); 205 | } 206 | } else { 207 | throw ApiError.EPERM(path); 208 | } 209 | } 210 | 211 | public async readdir(path: string, cred: Cred): Promise { 212 | return this.readdirSync(path, cred); 213 | } 214 | 215 | /** 216 | * We have the entire file as a buffer; optimize readFile. 217 | */ 218 | public async readFile(fname: string, encoding: BufferEncoding, flag: FileFlag, cred: Cred): Promise { 219 | // Get file. 220 | const fd = await this.open(fname, flag, 0o644, cred); 221 | try { 222 | const fdCast = >fd; 223 | const fdBuff = fdCast.getBuffer(); 224 | if (encoding === null) { 225 | return copyingSlice(fdBuff); 226 | } 227 | return fdBuff.toString(encoding); 228 | } finally { 229 | await fd.close(); 230 | } 231 | } 232 | 233 | private _getHTTPPath(filePath: string): string { 234 | if (filePath.charAt(0) === '/') { 235 | filePath = filePath.slice(1); 236 | } 237 | return this.prefixUrl + filePath; 238 | } 239 | 240 | /** 241 | * Asynchronously download the given file. 242 | */ 243 | private _requestFile(p: string, type: 'buffer'): Promise; 244 | private _requestFile(p: string, type: 'json'): Promise; 245 | private _requestFile(p: string, type: string): Promise; 246 | private _requestFile(p: string, type: string): Promise { 247 | return this._requestFileInternal(this._getHTTPPath(p), type); 248 | } 249 | 250 | /** 251 | * Only requests the HEAD content, for the file size. 252 | */ 253 | private _requestFileSize(path: string): Promise { 254 | return this._requestFileSizeInternal(this._getHTTPPath(path)); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/backends/InMemory.ts: -------------------------------------------------------------------------------- 1 | import { SyncKeyValueStore, SimpleSyncStore, SimpleSyncRWTransaction, SyncKeyValueRWTransaction, SyncKeyValueFileSystem } from '../generic/key_value_filesystem'; 2 | import { CreateBackend, type BackendOptions } from './backend'; 3 | 4 | /** 5 | * A simple in-memory key-value store backed by a JavaScript object. 6 | */ 7 | export class InMemoryStore implements SyncKeyValueStore, SimpleSyncStore { 8 | private store: Map = new Map(); 9 | 10 | public name() { 11 | return InMemoryFileSystem.Name; 12 | } 13 | public clear() { 14 | this.store.clear(); 15 | } 16 | 17 | public beginTransaction(type: string): SyncKeyValueRWTransaction { 18 | return new SimpleSyncRWTransaction(this); 19 | } 20 | 21 | public get(key: string): Buffer { 22 | return this.store.get(key); 23 | } 24 | 25 | public put(key: string, data: Buffer, overwrite: boolean): boolean { 26 | if (!overwrite && this.store.has(key)) { 27 | return false; 28 | } 29 | this.store.set(key, data); 30 | return true; 31 | } 32 | 33 | public del(key: string): void { 34 | this.store.delete(key); 35 | } 36 | } 37 | 38 | /** 39 | * A simple in-memory file system backed by an InMemoryStore. 40 | * Files are not persisted across page loads. 41 | */ 42 | export class InMemoryFileSystem extends SyncKeyValueFileSystem { 43 | public static readonly Name = 'InMemory'; 44 | 45 | public static Create = CreateBackend.bind(this); 46 | 47 | public static readonly Options: BackendOptions = {}; 48 | 49 | public constructor() { 50 | super({ store: new InMemoryStore() }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/backends/IndexedDB.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { AsyncKeyValueROTransaction, AsyncKeyValueRWTransaction, AsyncKeyValueStore, AsyncKeyValueFileSystem } from '../generic/key_value_filesystem'; 3 | import { ApiError, ErrorCode } from '../ApiError'; 4 | import { Buffer } from 'buffer'; 5 | import { CreateBackend, type BackendOptions } from './backend'; 6 | 7 | /** 8 | * Get the indexedDB constructor for the current browser. 9 | * @hidden 10 | */ 11 | const indexedDB: IDBFactory = (() => { 12 | try { 13 | return globalThis.indexedDB || (globalThis).mozIndexedDB || (globalThis).webkitIndexedDB || globalThis.msIndexedDB; 14 | } catch { 15 | return null; 16 | } 17 | })(); 18 | 19 | /** 20 | * Converts a DOMException or a DOMError from an IndexedDB event into a 21 | * standardized BrowserFS API error. 22 | * @hidden 23 | */ 24 | function convertError(e: { name: string }, message: string = e.toString()): ApiError { 25 | switch (e.name) { 26 | case 'NotFoundError': 27 | return new ApiError(ErrorCode.ENOENT, message); 28 | case 'QuotaExceededError': 29 | return new ApiError(ErrorCode.ENOSPC, message); 30 | default: 31 | // The rest do not seem to map cleanly to standard error codes. 32 | return new ApiError(ErrorCode.EIO, message); 33 | } 34 | } 35 | 36 | /** 37 | * Produces a new onerror handler for IDB. Our errors are always fatal, so we 38 | * handle them generically: Call the user-supplied callback with a translated 39 | * version of the error, and let the error bubble up. 40 | * @hidden 41 | */ 42 | function onErrorHandler(cb: (e: ApiError) => void, code: ErrorCode = ErrorCode.EIO, message: string | null = null): (e?: any) => void { 43 | return function (e?: any): void { 44 | // Prevent the error from canceling the transaction. 45 | e.preventDefault(); 46 | cb(new ApiError(code, message !== null ? message : undefined)); 47 | }; 48 | } 49 | 50 | /** 51 | * @hidden 52 | */ 53 | export class IndexedDBROTransaction implements AsyncKeyValueROTransaction { 54 | constructor(public tx: IDBTransaction, public store: IDBObjectStore) {} 55 | 56 | public get(key: string): Promise { 57 | return new Promise((resolve, reject) => { 58 | try { 59 | const r: IDBRequest = this.store.get(key); 60 | r.onerror = onErrorHandler(reject); 61 | r.onsuccess = event => { 62 | // IDB returns the value 'undefined' when you try to get keys that 63 | // don't exist. The caller expects this behavior. 64 | const result = (event.target).result; 65 | if (result === undefined) { 66 | resolve(result); 67 | } else { 68 | // IDB data is stored as an ArrayBuffer 69 | resolve(Buffer.from(result)); 70 | } 71 | }; 72 | } catch (e) { 73 | reject(convertError(e)); 74 | } 75 | }); 76 | } 77 | } 78 | 79 | /** 80 | * @hidden 81 | */ 82 | export class IndexedDBRWTransaction extends IndexedDBROTransaction implements AsyncKeyValueRWTransaction, AsyncKeyValueROTransaction { 83 | constructor(tx: IDBTransaction, store: IDBObjectStore) { 84 | super(tx, store); 85 | } 86 | 87 | /** 88 | * @todo return false when add has a key conflict (no error) 89 | */ 90 | public put(key: string, data: Buffer, overwrite: boolean): Promise { 91 | return new Promise((resolve, reject) => { 92 | try { 93 | const r: IDBRequest = overwrite ? this.store.put(data, key) : this.store.add(data, key); 94 | r.onerror = onErrorHandler(reject); 95 | r.onsuccess = () => { 96 | resolve(true); 97 | }; 98 | } catch (e) { 99 | reject(convertError(e)); 100 | } 101 | }); 102 | } 103 | 104 | public del(key: string): Promise { 105 | return new Promise((resolve, reject) => { 106 | try { 107 | const r: IDBRequest = this.store.delete(key); 108 | r.onerror = onErrorHandler(reject); 109 | r.onsuccess = () => { 110 | resolve(); 111 | }; 112 | } catch (e) { 113 | reject(convertError(e)); 114 | } 115 | }); 116 | } 117 | 118 | public commit(): Promise { 119 | return new Promise(resolve => { 120 | // Return to the event loop to commit the transaction. 121 | setTimeout(resolve, 0); 122 | }); 123 | } 124 | 125 | public abort(): Promise { 126 | return new Promise((resolve, reject) => { 127 | try { 128 | this.tx.abort(); 129 | resolve(); 130 | } catch (e) { 131 | reject(convertError(e)); 132 | } 133 | }); 134 | } 135 | } 136 | 137 | export class IndexedDBStore implements AsyncKeyValueStore { 138 | public static Create(storeName: string, indexedDB: IDBFactory): Promise { 139 | return new Promise((resolve, reject) => { 140 | const openReq: IDBOpenDBRequest = indexedDB.open(storeName, 1); 141 | 142 | openReq.onupgradeneeded = event => { 143 | const db: IDBDatabase = (event.target).result; 144 | // Huh. This should never happen; we're at version 1. Why does another 145 | // database exist? 146 | if (db.objectStoreNames.contains(storeName)) { 147 | db.deleteObjectStore(storeName); 148 | } 149 | db.createObjectStore(storeName); 150 | }; 151 | 152 | openReq.onsuccess = event => { 153 | resolve(new IndexedDBStore((event.target).result, storeName)); 154 | }; 155 | 156 | openReq.onerror = onErrorHandler(reject, ErrorCode.EACCES); 157 | }); 158 | } 159 | 160 | constructor(private db: IDBDatabase, private storeName: string) {} 161 | 162 | public name(): string { 163 | return IndexedDBFileSystem.Name + ' - ' + this.storeName; 164 | } 165 | 166 | public clear(): Promise { 167 | return new Promise((resolve, reject) => { 168 | try { 169 | const tx = this.db.transaction(this.storeName, 'readwrite'), 170 | objectStore = tx.objectStore(this.storeName), 171 | r: IDBRequest = objectStore.clear(); 172 | r.onsuccess = () => { 173 | // Use setTimeout to commit transaction. 174 | setTimeout(resolve, 0); 175 | }; 176 | r.onerror = onErrorHandler(reject); 177 | } catch (e) { 178 | reject(convertError(e)); 179 | } 180 | }); 181 | } 182 | 183 | public beginTransaction(type: 'readonly'): AsyncKeyValueROTransaction; 184 | public beginTransaction(type: 'readwrite'): AsyncKeyValueRWTransaction; 185 | public beginTransaction(type: 'readonly' | 'readwrite' = 'readonly'): AsyncKeyValueROTransaction { 186 | const tx = this.db.transaction(this.storeName, type), 187 | objectStore = tx.objectStore(this.storeName); 188 | if (type === 'readwrite') { 189 | return new IndexedDBRWTransaction(tx, objectStore); 190 | } else if (type === 'readonly') { 191 | return new IndexedDBROTransaction(tx, objectStore); 192 | } else { 193 | throw new ApiError(ErrorCode.EINVAL, 'Invalid transaction type.'); 194 | } 195 | } 196 | } 197 | 198 | export namespace IndexedDBFileSystem { 199 | /** 200 | * Configuration options for the IndexedDB file system. 201 | */ 202 | export interface Options { 203 | /** 204 | * The name of this file system. You can have multiple IndexedDB file systems operating at once, but each must have a different name. 205 | */ 206 | storeName?: string; 207 | 208 | /** 209 | * The size of the inode cache. Defaults to 100. A size of 0 or below disables caching. 210 | */ 211 | cacheSize?: number; 212 | 213 | /** 214 | * The IDBFactory to use. Defaults to `globalThis.indexedDB`. 215 | */ 216 | idbFactory?: IDBFactory; 217 | } 218 | } 219 | 220 | /** 221 | * A file system that uses the IndexedDB key value file system. 222 | */ 223 | export class IndexedDBFileSystem extends AsyncKeyValueFileSystem { 224 | public static readonly Name = 'IndexedDB'; 225 | 226 | public static Create = CreateBackend.bind(this); 227 | 228 | public static readonly Options: BackendOptions = { 229 | storeName: { 230 | type: 'string', 231 | optional: true, 232 | description: 'The name of this file system. You can have multiple IndexedDB file systems operating at once, but each must have a different name.', 233 | }, 234 | cacheSize: { 235 | type: 'number', 236 | optional: true, 237 | description: 'The size of the inode cache. Defaults to 100. A size of 0 or below disables caching.', 238 | }, 239 | idbFactory: { 240 | type: 'object', 241 | optional: true, 242 | description: 'The IDBFactory to use. Defaults to globalThis.indexedDB.', 243 | }, 244 | }; 245 | 246 | public static isAvailable(idbFactory: IDBFactory = globalThis.indexedDB): boolean { 247 | try { 248 | if (!(idbFactory instanceof IDBFactory)) { 249 | return false; 250 | } 251 | const req = indexedDB.open('__browserfs_test__'); 252 | if (!req) { 253 | return false; 254 | } 255 | } catch (e) { 256 | return false; 257 | } 258 | } 259 | 260 | constructor({ cacheSize = 100, storeName = 'browserfs', idbFactory = globalThis.indexedDB }: IndexedDBFileSystem.Options) { 261 | super(cacheSize); 262 | this._ready = IndexedDBStore.Create(storeName, idbFactory).then(store => { 263 | this.init(store); 264 | return this; 265 | }); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/backends/Storage.ts: -------------------------------------------------------------------------------- 1 | import { SyncKeyValueStore, SimpleSyncStore, SyncKeyValueFileSystem, SimpleSyncRWTransaction, SyncKeyValueRWTransaction } from '../generic/key_value_filesystem'; 2 | import { ApiError, ErrorCode } from '../ApiError'; 3 | import { Buffer } from 'buffer'; 4 | import { CreateBackend, type BackendOptions } from './backend'; 5 | 6 | /** 7 | * A synchronous key-value store backed by Storage. 8 | */ 9 | export class StorageStore implements SyncKeyValueStore, SimpleSyncStore { 10 | public name(): string { 11 | return StorageFileSystem.Name; 12 | } 13 | 14 | constructor(protected _storage) {} 15 | 16 | public clear(): void { 17 | this._storage.clear(); 18 | } 19 | 20 | public beginTransaction(type: string): SyncKeyValueRWTransaction { 21 | // No need to differentiate. 22 | return new SimpleSyncRWTransaction(this); 23 | } 24 | 25 | public get(key: string): Buffer | undefined { 26 | const data = this._storage.getItem(key); 27 | if (typeof data != 'string') { 28 | return; 29 | } 30 | 31 | return Buffer.from(data); 32 | } 33 | 34 | public put(key: string, data: Buffer, overwrite: boolean): boolean { 35 | try { 36 | if (!overwrite && this._storage.getItem(key) !== null) { 37 | // Don't want to overwrite the key! 38 | return false; 39 | } 40 | this._storage.setItem(key, data.toString()); 41 | return true; 42 | } catch (e) { 43 | throw new ApiError(ErrorCode.ENOSPC, 'Storage is full.'); 44 | } 45 | } 46 | 47 | public del(key: string): void { 48 | try { 49 | this._storage.removeItem(key); 50 | } catch (e) { 51 | throw new ApiError(ErrorCode.EIO, 'Unable to delete key ' + key + ': ' + e); 52 | } 53 | } 54 | } 55 | 56 | export namespace StorageFileSystem { 57 | /** 58 | * Options to pass to the StorageFileSystem 59 | */ 60 | export interface Options { 61 | /** 62 | * The Storage to use. Defaults to globalThis.localStorage. 63 | */ 64 | storage: Storage; 65 | } 66 | } 67 | 68 | /** 69 | * A synchronous file system backed by a `Storage` (e.g. localStorage). 70 | */ 71 | export class StorageFileSystem extends SyncKeyValueFileSystem { 72 | public static readonly Name = 'Storage'; 73 | 74 | public static Create = CreateBackend.bind(this); 75 | 76 | public static readonly Options: BackendOptions = { 77 | storage: { 78 | type: 'object', 79 | optional: true, 80 | description: 'The Storage to use. Defaults to globalThis.localStorage.', 81 | }, 82 | }; 83 | 84 | public static isAvailable(storage: Storage = globalThis.localStorage): boolean { 85 | return storage instanceof Storage; 86 | } 87 | /** 88 | * Creates a new Storage file system using the contents of `Storage`. 89 | */ 90 | constructor({ storage = globalThis.localStorage }: StorageFileSystem.Options) { 91 | super({ store: new StorageStore(storage) }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/backends/WorkerFS.ts: -------------------------------------------------------------------------------- 1 | import { type FileSystem, BaseFileSystem, FileContents, FileSystemMetadata } from '../filesystem'; 2 | import { ApiError, ErrorCode } from '../ApiError'; 3 | import { File, FileFlag } from '../file'; 4 | import { Stats } from '../stats'; 5 | import { Cred } from '../cred'; 6 | import { CreateBackend, type BackendOptions } from './backend'; 7 | 8 | /** 9 | * @hidden 10 | */ 11 | declare const importScripts: (...path: string[]) => unknown; 12 | 13 | /** 14 | * An RPC message 15 | */ 16 | interface RPCMessage { 17 | isBFS: true; 18 | id: number; 19 | } 20 | 21 | type _FSAsyncMethods = { 22 | [Method in keyof FileSystem]: Extract Promise>; 23 | }; 24 | 25 | type _RPCFSRequests = { 26 | [Method in keyof _FSAsyncMethods]: { method: Method; args: Parameters<_FSAsyncMethods[Method]> }; 27 | }; 28 | 29 | type _RPCFSResponses = { 30 | [Method in keyof _FSAsyncMethods]: { method: Method; value: Awaited> }; 31 | }; 32 | 33 | /** 34 | * @see https://stackoverflow.com/a/60920767/17637456 35 | */ 36 | type RPCRequest = RPCMessage & (_RPCFSRequests[keyof _FSAsyncMethods] | { method: 'metadata'; args: [] } | { method: 'syncClose'; args: [string, File] }); 37 | 38 | type RPCResponse = RPCMessage & (_RPCFSResponses[keyof _FSAsyncMethods] | { method: 'metadata'; value: FileSystemMetadata } | { method: 'syncClose'; value: null }); 39 | 40 | function isRPCMessage(arg: unknown): arg is RPCMessage { 41 | return typeof arg == 'object' && 'isBFS' in arg && !!arg.isBFS; 42 | } 43 | 44 | type _executor = Parameters[0]>; 45 | interface WorkerRequest { 46 | resolve: _executor[0]; 47 | reject: _executor[1]; 48 | } 49 | 50 | export namespace WorkerFS { 51 | export interface Options { 52 | /** 53 | * The target worker that you want to connect to, or the current worker if in a worker context. 54 | */ 55 | worker: Worker; 56 | } 57 | } 58 | 59 | type _RPCExtractReturnValue = Promise['value']>; 60 | 61 | /** 62 | * WorkerFS lets you access a BrowserFS instance that is running in a different 63 | * JavaScript context (e.g. access BrowserFS in one of your WebWorkers, or 64 | * access BrowserFS running on the main page from a WebWorker). 65 | * 66 | * For example, to have a WebWorker access files in the main browser thread, 67 | * do the following: 68 | * 69 | * MAIN BROWSER THREAD: 70 | * 71 | * ```javascript 72 | * // Listen for remote file system requests. 73 | * BrowserFS.Backend.WorkerFS.attachRemoteListener(webWorkerObject); 74 | * ``` 75 | * 76 | * WEBWORKER THREAD: 77 | * 78 | * ```javascript 79 | * // Set the remote file system as the root file system. 80 | * BrowserFS.configure({ fs: "WorkerFS", options: { worker: self }}, function(e) { 81 | * // Ready! 82 | * }); 83 | * ``` 84 | * 85 | * Note that synchronous operations are not permitted on the WorkerFS, regardless 86 | * of the configuration option of the remote FS. 87 | */ 88 | export class WorkerFS extends BaseFileSystem { 89 | public static readonly Name = 'WorkerFS'; 90 | 91 | public static Create = CreateBackend.bind(this); 92 | 93 | public static readonly Options: BackendOptions = { 94 | worker: { 95 | type: 'object', 96 | description: 'The target worker that you want to connect to, or the current worker if in a worker context.', 97 | validator: async (v: Worker): Promise => { 98 | // Check for a `postMessage` function. 99 | if (typeof v?.postMessage != 'function') { 100 | throw new ApiError(ErrorCode.EINVAL, `option must be a Web Worker instance.`); 101 | } 102 | }, 103 | }, 104 | }; 105 | 106 | public static isAvailable(): boolean { 107 | return typeof importScripts !== 'undefined' || typeof Worker !== 'undefined'; 108 | } 109 | 110 | private _worker: Worker; 111 | private _currentID: number = 0; 112 | private _requests: Map = new Map(); 113 | 114 | private _isInitialized: boolean = false; 115 | private _metadata: FileSystemMetadata; 116 | 117 | /** 118 | * Constructs a new WorkerFS instance that connects with BrowserFS running on 119 | * the specified worker. 120 | */ 121 | public constructor({ worker }: WorkerFS.Options) { 122 | super(); 123 | this._worker = worker; 124 | this._worker.onmessage = (event: MessageEvent) => { 125 | if (!isRPCMessage(event.data)) { 126 | return; 127 | } 128 | const { id, method, value } = event.data as RPCResponse; 129 | 130 | if (method === 'metadata') { 131 | this._metadata = value; 132 | this._isInitialized = true; 133 | return; 134 | } 135 | 136 | const { resolve, reject } = this._requests.get(id); 137 | this._requests.delete(id); 138 | if (value instanceof Error || value instanceof ApiError) { 139 | reject(value); 140 | return; 141 | } 142 | resolve(value); 143 | }; 144 | } 145 | 146 | public get metadata(): FileSystemMetadata { 147 | return { 148 | ...super.metadata, 149 | ...this._metadata, 150 | name: WorkerFS.Name, 151 | synchronous: false, 152 | }; 153 | } 154 | 155 | private async _rpc(method: T, ...args: Extract['args']): _RPCExtractReturnValue { 156 | return new Promise((resolve, reject) => { 157 | const id = this._currentID++; 158 | this._requests.set(id, { resolve, reject }); 159 | this._worker.postMessage({ 160 | isBFS: true, 161 | id, 162 | method, 163 | args, 164 | } as RPCRequest); 165 | }); 166 | } 167 | 168 | public rename(oldPath: string, newPath: string, cred: Cred): Promise { 169 | return this._rpc('rename', oldPath, newPath, cred); 170 | } 171 | public stat(p: string, cred: Cred): Promise { 172 | return this._rpc('stat', p, cred); 173 | } 174 | public open(p: string, flag: FileFlag, mode: number, cred: Cred): Promise { 175 | return this._rpc('open', p, flag, mode, cred); 176 | } 177 | public unlink(p: string, cred: Cred): Promise { 178 | return this._rpc('unlink', p, cred); 179 | } 180 | public rmdir(p: string, cred: Cred): Promise { 181 | return this._rpc('rmdir', p, cred); 182 | } 183 | public mkdir(p: string, mode: number, cred: Cred): Promise { 184 | return this._rpc('mkdir', p, mode, cred); 185 | } 186 | public readdir(p: string, cred: Cred): Promise { 187 | return this._rpc('readdir', p, cred); 188 | } 189 | public exists(p: string, cred: Cred): Promise { 190 | return this._rpc('exists', p, cred); 191 | } 192 | public realpath(p: string, cred: Cred): Promise { 193 | return this._rpc('realpath', p, cred); 194 | } 195 | public truncate(p: string, len: number, cred: Cred): Promise { 196 | return this._rpc('truncate', p, len, cred); 197 | } 198 | public readFile(fname: string, encoding: BufferEncoding, flag: FileFlag, cred: Cred): Promise { 199 | return this._rpc('readFile', fname, encoding, flag, cred); 200 | } 201 | public writeFile(fname: string, data: FileContents, encoding: BufferEncoding, flag: FileFlag, mode: number, cred: Cred): Promise { 202 | return this._rpc('writeFile', fname, data, encoding, flag, mode, cred); 203 | } 204 | public appendFile(fname: string, data: FileContents, encoding: BufferEncoding, flag: FileFlag, mode: number, cred: Cred): Promise { 205 | return this._rpc('appendFile', fname, data, encoding, flag, mode, cred); 206 | } 207 | public chmod(p: string, mode: number, cred: Cred): Promise { 208 | return this._rpc('chmod', p, mode, cred); 209 | } 210 | public chown(p: string, new_uid: number, new_gid: number, cred: Cred): Promise { 211 | return this._rpc('chown', p, new_uid, new_gid, cred); 212 | } 213 | public utimes(p: string, atime: Date, mtime: Date, cred: Cred): Promise { 214 | return this._rpc('utimes', p, atime, mtime, cred); 215 | } 216 | public link(srcpath: string, dstpath: string, cred: Cred): Promise { 217 | return this._rpc('link', srcpath, dstpath, cred); 218 | } 219 | public symlink(srcpath: string, dstpath: string, type: string, cred: Cred): Promise { 220 | return this._rpc('symlink', srcpath, dstpath, type, cred); 221 | } 222 | public readlink(p: string, cred: Cred): Promise { 223 | return this._rpc('readlink', p, cred); 224 | } 225 | 226 | public syncClose(method: string, fd: File): Promise { 227 | return this._rpc('syncClose', method, fd); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/backends/backend.ts: -------------------------------------------------------------------------------- 1 | import type { BFSCallback, FileSystem } from '../filesystem'; 2 | import { checkOptions } from '../utils'; 3 | 4 | /** 5 | * Describes a file system option. 6 | */ 7 | export interface BackendOption { 8 | /** 9 | * The basic JavaScript type(s) for this option. 10 | */ 11 | type: string | string[]; 12 | 13 | /** 14 | * Whether or not the option is optional (e.g., can be set to null or undefined). 15 | * Defaults to `false`. 16 | */ 17 | optional?: boolean; 18 | 19 | /** 20 | * Description of the option. Used in error messages and documentation. 21 | */ 22 | description: string; 23 | 24 | /** 25 | * A custom validation function to check if the option is valid. 26 | * Resolves if valid and rejects if not. 27 | */ 28 | validator?(opt: T): Promise; 29 | } 30 | 31 | /** 32 | * Describes all of the options available in a file system. 33 | */ 34 | export interface BackendOptions { 35 | [name: string]: BackendOption; 36 | } 37 | 38 | /** 39 | * Contains types for static functions on a backend. 40 | */ 41 | export interface BaseBackendConstructor { 42 | new (...params: ConstructorParameters): InstanceType; 43 | 44 | /** 45 | * A name to identify the backend. 46 | */ 47 | Name: string; 48 | 49 | /** 50 | * Describes all of the options available for this backend. 51 | */ 52 | Options: BackendOptions; 53 | 54 | /** 55 | * Whether the backend is available in the current environment. 56 | * It supports checking synchronously and asynchronously 57 | * Sync: 58 | * Returns 'true' if this backend is available in the current 59 | * environment. For example, a `localStorage`-backed filesystem will return 60 | * 'false' if the browser does not support that API. 61 | * 62 | * Defaults to 'false', as the FileSystem base class isn't usable alone. 63 | */ 64 | isAvailable(): boolean; 65 | } 66 | 67 | /** 68 | * Contains types for static functions on a backend. 69 | */ 70 | export interface BackendConstructor extends BaseBackendConstructor { 71 | /** 72 | * Creates backend of this given type with the given 73 | * options, and either returns the result in a promise or callback. 74 | */ 75 | Create(): Promise>; 76 | Create(options: object): Promise>; 77 | Create(cb: BFSCallback>): void; 78 | Create(options: object, cb: BFSCallback>): void; 79 | Create(options: object, cb?: BFSCallback>): Promise> | void; 80 | } 81 | 82 | export function CreateBackend(this: FS): Promise>; 83 | export function CreateBackend(this: FS, options: BackendOptions): Promise>; 84 | export function CreateBackend(this: FS, cb: BFSCallback>): void; 85 | export function CreateBackend(this: FS, options: BackendOptions, cb: BFSCallback>): void; 86 | export function CreateBackend( 87 | this: FS, 88 | options?: BackendOptions | BFSCallback>, 89 | cb?: BFSCallback> 90 | ): Promise> | void { 91 | cb = typeof options === 'function' ? options : cb; 92 | 93 | checkOptions(this, options); 94 | 95 | const fs = new this(typeof options === 'function' ? {} : options) as InstanceType; 96 | 97 | // Promise 98 | if (typeof cb != 'function') { 99 | return fs.whenReady(); 100 | } 101 | 102 | // Callback 103 | fs.whenReady() 104 | .then(fs => cb(null, fs)) 105 | .catch(err => cb(err)); 106 | } 107 | -------------------------------------------------------------------------------- /src/backends/index.ts: -------------------------------------------------------------------------------- 1 | import { AsyncMirror } from './AsyncMirror'; 2 | import { DropboxFileSystem as Dropbox } from './Dropbox'; 3 | import { EmscriptenFileSystem as Emscripten } from './Emscripten'; 4 | import { FileSystemAccessFileSystem as FileSystemAccess } from './FileSystemAccess'; 5 | import { FolderAdapter } from './FolderAdapter'; 6 | import { InMemoryFileSystem as InMemory } from './InMemory'; 7 | import { IndexedDBFileSystem as IndexedDB } from './IndexedDB'; 8 | import { StorageFileSystem as Storage } from './Storage'; 9 | import { OverlayFS } from './OverlayFS'; 10 | import { WorkerFS } from './WorkerFS'; 11 | import { HTTPRequest } from './HTTPRequest'; 12 | import { ZipFS } from './ZipFS'; 13 | import { IsoFS } from './IsoFS'; 14 | import { BackendConstructor } from './backend'; 15 | 16 | export const backends: { [backend: string]: BackendConstructor } = { 17 | AsyncMirror, 18 | Dropbox, 19 | Emscripten, 20 | FileSystemAccess, 21 | FolderAdapter, 22 | InMemory, 23 | IndexedDB, 24 | IsoFS, 25 | Storage, 26 | OverlayFS, 27 | WorkerFS, 28 | HTTPRequest, 29 | XMLHTTPRequest: HTTPRequest, 30 | ZipFS, 31 | }; 32 | 33 | export { 34 | AsyncMirror, 35 | Dropbox, 36 | Emscripten, 37 | FileSystemAccess, 38 | FolderAdapter, 39 | InMemory, 40 | IndexedDB, 41 | IsoFS, 42 | Storage, 43 | OverlayFS, 44 | WorkerFS, 45 | HTTPRequest, 46 | HTTPRequest as XMLHTTPRequest, 47 | ZipFS, 48 | }; 49 | -------------------------------------------------------------------------------- /src/cred.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Credentials used for FS ops. 3 | * Similar to Linux's cred struct. See https://github.com/torvalds/linux/blob/master/include/linux/cred.h 4 | */ 5 | export class Cred { 6 | constructor(public uid: number, public gid: number, public suid: number, public sgid: number, public euid: number, public egid: number) {} 7 | 8 | public static Root = new Cred(0, 0, 0, 0, 0, 0); 9 | } 10 | -------------------------------------------------------------------------------- /src/emulation/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | FS Constants 3 | See https://nodejs.org/api/fs.html#file-access-constants 4 | */ 5 | 6 | // File Access Constants 7 | 8 | /** Constant for fs.access(). File is visible to the calling process. */ 9 | export const F_OK = 0; 10 | 11 | /** Constant for fs.access(). File can be read by the calling process. */ 12 | export const R_OK = 4; 13 | 14 | /** Constant for fs.access(). File can be written by the calling process. */ 15 | export const W_OK = 2; 16 | 17 | /** Constant for fs.access(). File can be executed by the calling process. */ 18 | export const X_OK = 1; 19 | 20 | // File Copy Constants 21 | 22 | /** Constant for fs.copyFile. Flag indicating the destination file should not be overwritten if it already exists. */ 23 | export const COPYFILE_EXCL = 1; 24 | 25 | /** 26 | * Constant for fs.copyFile. Copy operation will attempt to create a copy-on-write reflink. 27 | * If the underlying platform does not support copy-on-write, then a fallback copy mechanism is used. 28 | */ 29 | export const COPYFILE_FICLONE = 2; 30 | 31 | /** 32 | * Constant for fs.copyFile. Copy operation will attempt to create a copy-on-write reflink. 33 | * If the underlying platform does not support copy-on-write, then the operation will fail with an error. 34 | */ 35 | export const COPYFILE_FICLONE_FORCE = 4; 36 | 37 | // File Open Constants 38 | 39 | /** Constant for fs.open(). Flag indicating to open a file for read-only access. */ 40 | export const O_RDONLY = 0; 41 | 42 | /** Constant for fs.open(). Flag indicating to open a file for write-only access. */ 43 | export const O_WRONLY = 1; 44 | 45 | /** Constant for fs.open(). Flag indicating to open a file for read-write access. */ 46 | export const O_RDWR = 2; 47 | 48 | /** Constant for fs.open(). Flag indicating to create the file if it does not already exist. */ 49 | export const O_CREAT = 0o100; // Node internal is 50 | 51 | /** Constant for fs.open(). Flag indicating that opening a file should fail if the O_CREAT flag is set and the file already exists. */ 52 | export const O_EXCL = 0o200; 53 | 54 | /** 55 | * Constant for fs.open(). Flag indicating that if path identifies a terminal device, 56 | * opening the path shall not cause that terminal to become the controlling terminal for the process 57 | * (if the process does not already have one). 58 | */ 59 | export const O_NOCTTY = 0o400; 60 | 61 | /** Constant for fs.open(). Flag indicating that if the file exists and is a regular file, and the file is opened successfully for write access, its length shall be truncated to zero. */ 62 | export const O_TRUNC = 0o1000; 63 | 64 | /** Constant for fs.open(). Flag indicating that data will be appended to the end of the file. */ 65 | export const O_APPEND = 0o2000; 66 | 67 | /** Constant for fs.open(). Flag indicating that the open should fail if the path is not a directory. */ 68 | export const O_DIRECTORY = 0o200000; 69 | 70 | /** 71 | * constant for fs.open(). 72 | * Flag indicating reading accesses to the file system will no longer result in 73 | * an update to the atime information associated with the file. 74 | * This flag is available on Linux operating systems only. 75 | */ 76 | export const O_NOATIME = 0o1000000; 77 | 78 | /** Constant for fs.open(). Flag indicating that the open should fail if the path is a symbolic link. */ 79 | export const O_NOFOLLOW = 0o400000; 80 | 81 | /** Constant for fs.open(). Flag indicating that the file is opened for synchronous I/O. */ 82 | export const O_SYNC = 0o4010000; 83 | 84 | /** Constant for fs.open(). Flag indicating that the file is opened for synchronous I/O with write operations waiting for data integrity. */ 85 | export const O_DSYNC = 0o10000; 86 | 87 | /** Constant for fs.open(). Flag indicating to open the symbolic link itself rather than the resource it is pointing to. */ 88 | export const O_SYMLINK = 0o100000; 89 | 90 | /** Constant for fs.open(). When set, an attempt will be made to minimize caching effects of file I/O. */ 91 | export const O_DIRECT = 0o40000; 92 | 93 | /** Constant for fs.open(). Flag indicating to open the file in nonblocking mode when possible. */ 94 | export const O_NONBLOCK = 0o4000; 95 | 96 | // File Type Constants 97 | 98 | /** Constant for fs.Stats mode property for determining a file's type. Bit mask used to extract the file type code. */ 99 | export const S_IFMT = 0o170000; 100 | 101 | /** Constant for fs.Stats mode property for determining a file's type. File type constant for a regular file. */ 102 | export const S_IFREG = 0o100000; 103 | 104 | /** Constant for fs.Stats mode property for determining a file's type. File type constant for a directory. */ 105 | export const S_IFDIR = 0o40000; 106 | 107 | /** Constant for fs.Stats mode property for determining a file's type. File type constant for a character-oriented device file. */ 108 | export const S_IFCHR = 0o20000; 109 | 110 | /** Constant for fs.Stats mode property for determining a file's type. File type constant for a block-oriented device file. */ 111 | export const S_IFBLK = 0o60000; 112 | 113 | /** Constant for fs.Stats mode property for determining a file's type. File type constant for a FIFO/pipe. */ 114 | export const S_IFIFO = 0o10000; 115 | 116 | /** Constant for fs.Stats mode property for determining a file's type. File type constant for a symbolic link. */ 117 | export const S_IFLNK = 0o120000; 118 | 119 | /** Constant for fs.Stats mode property for determining a file's type. File type constant for a socket. */ 120 | export const S_IFSOCK = 0o140000; 121 | 122 | // File Mode Constants 123 | 124 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable, writable and executable by owner. */ 125 | export const S_IRWXU = 0o700; 126 | 127 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable by owner. */ 128 | export const S_IRUSR = 0o400; 129 | 130 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating writable by owner. */ 131 | export const S_IWUSR = 0o200; 132 | 133 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating executable by owner. */ 134 | export const S_IXUSR = 0o100; 135 | 136 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable, writable and executable by group. */ 137 | export const S_IRWXG = 0o70; 138 | 139 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable by group. */ 140 | export const S_IRGRP = 0o40; 141 | 142 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating writable by group. */ 143 | export const S_IWGRP = 0o20; 144 | 145 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating executable by group. */ 146 | export const S_IXGRP = 0o10; 147 | 148 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable, writable and executable by others. */ 149 | export const S_IRWXO = 7; 150 | 151 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating readable by others. */ 152 | export const S_IROTH = 4; 153 | 154 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating writable by others. */ 155 | export const S_IWOTH = 2; 156 | 157 | /** Constant for fs.Stats mode property for determining access permissions for a file. File mode indicating executable by others. */ 158 | export const S_IXOTH = 1; 159 | -------------------------------------------------------------------------------- /src/emulation/fs.ts: -------------------------------------------------------------------------------- 1 | import * as fs_mock from './index'; 2 | import type * as fs_node from 'node:fs'; 3 | 4 | type BrowserFSModule = typeof fs_node & typeof fs_mock; 5 | // @ts-expect-error 2322 6 | const fs: BrowserFSModule = fs_mock; 7 | 8 | export * from './index'; 9 | export default fs; 10 | -------------------------------------------------------------------------------- /src/emulation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './callbacks'; 2 | export * from './sync'; 3 | export * as promises from './promises'; 4 | export * as constants from './constants'; 5 | export { initialize, getMount, getMounts, mount, umount, _toUnixTimestamp } from './shared'; 6 | -------------------------------------------------------------------------------- /src/emulation/shared.ts: -------------------------------------------------------------------------------- 1 | // Utilities and shared data 2 | 3 | import { posix as path } from 'path'; 4 | import { ApiError, ErrorCode } from '../ApiError'; 5 | import { Cred } from '../cred'; 6 | import { FileSystem } from '../filesystem'; 7 | import { File } from '../file'; 8 | //import { BackendConstructor } from '../backends'; 9 | import { InMemoryFileSystem } from '../backends/InMemory'; 10 | import { BackendConstructor } from '../backends/backend'; 11 | 12 | /** 13 | * converts Date or number to a fractional UNIX timestamp 14 | * Grabbed from NodeJS sources (lib/fs.js) 15 | */ 16 | export function _toUnixTimestamp(time: Date | number): number { 17 | if (typeof time === 'number') { 18 | return time; 19 | } else if (time instanceof Date) { 20 | return time.getTime() / 1000; 21 | } 22 | throw new Error('Cannot parse time: ' + time); 23 | } 24 | 25 | export function normalizeMode(mode: unknown, def: number): number { 26 | switch (typeof mode) { 27 | case 'number': 28 | // (path, flag, mode, cb?) 29 | return mode; 30 | case 'string': 31 | // (path, flag, modeString, cb?) 32 | const trueMode = parseInt(mode, 8); 33 | if (!isNaN(trueMode)) { 34 | return trueMode; 35 | } 36 | // Invalid string. 37 | return def; 38 | default: 39 | return def; 40 | } 41 | } 42 | 43 | export function normalizeTime(time: number | Date): Date { 44 | if (time instanceof Date) { 45 | return time; 46 | } 47 | 48 | if (typeof time === 'number') { 49 | return new Date(time * 1000); 50 | } 51 | 52 | throw new ApiError(ErrorCode.EINVAL, `Invalid time.`); 53 | } 54 | 55 | export function normalizePath(p: string): string { 56 | // Node doesn't allow null characters in paths. 57 | if (p.indexOf('\u0000') >= 0) { 58 | throw new ApiError(ErrorCode.EINVAL, 'Path must be a string without null bytes.'); 59 | } 60 | if (p === '') { 61 | throw new ApiError(ErrorCode.EINVAL, 'Path must not be empty.'); 62 | } 63 | p = p.replaceAll(/\/+/g, '/'); 64 | return path.resolve(p); 65 | } 66 | 67 | export function normalizeOptions(options: any, defEnc: string | null, defFlag: string, defMode: number | null): { encoding: BufferEncoding; flag: string; mode: number } { 68 | // typeof null === 'object' so special-case handing is needed. 69 | switch (options === null ? 'null' : typeof options) { 70 | case 'object': 71 | return { 72 | encoding: typeof options['encoding'] !== 'undefined' ? options['encoding'] : defEnc, 73 | flag: typeof options['flag'] !== 'undefined' ? options['flag'] : defFlag, 74 | mode: normalizeMode(options['mode'], defMode!), 75 | }; 76 | case 'string': 77 | return { 78 | encoding: options, 79 | flag: defFlag, 80 | mode: defMode!, 81 | }; 82 | case 'null': 83 | case 'undefined': 84 | case 'function': 85 | return { 86 | encoding: defEnc! as BufferEncoding, 87 | flag: defFlag, 88 | mode: defMode!, 89 | }; 90 | default: 91 | throw new TypeError(`"options" must be a string or an object, got ${typeof options} instead.`); 92 | } 93 | } 94 | 95 | export function nop() { 96 | // do nothing 97 | } 98 | 99 | // credentials 100 | export let cred: Cred; 101 | export function setCred(val: Cred): void { 102 | cred = val; 103 | } 104 | 105 | // descriptors 106 | export const fdMap: Map = new Map(); 107 | let nextFd = 100; 108 | export function getFdForFile(file: File): number { 109 | const fd = nextFd++; 110 | fdMap.set(fd, file); 111 | return fd; 112 | } 113 | export function fd2file(fd: number): File { 114 | if (!fdMap.has(fd)) { 115 | throw new ApiError(ErrorCode.EBADF, 'Invalid file descriptor.'); 116 | } 117 | return fdMap.get(fd); 118 | } 119 | 120 | // mounting 121 | export interface MountMapping { 122 | [point: string]: InstanceType; 123 | } 124 | 125 | export const mounts: Map = new Map(); 126 | 127 | /* 128 | Set a default root. 129 | There is a very small but not 0 change that initialize() will try to unmount the default before it is mounted. 130 | This can be fixed by using a top-level await, which is not done to maintain ES6 compatibility. 131 | */ 132 | InMemoryFileSystem.Create().then(fs => mount('/', fs)); 133 | 134 | /** 135 | * Gets the file system mounted at `mountPoint` 136 | */ 137 | export function getMount(mountPoint: string): FileSystem { 138 | return mounts.get(mountPoint); 139 | } 140 | 141 | /** 142 | * Gets an object of mount points (keys) and filesystems (values) 143 | */ 144 | export function getMounts(): MountMapping { 145 | return Object.fromEntries(mounts.entries()); 146 | } 147 | 148 | /** 149 | * Mounts the file system at the given mount point. 150 | */ 151 | export function mount(mountPoint: string, fs: FileSystem): void { 152 | if (mountPoint[0] !== '/') { 153 | mountPoint = '/' + mountPoint; 154 | } 155 | mountPoint = path.resolve(mountPoint); 156 | if (mounts.has(mountPoint)) { 157 | throw new ApiError(ErrorCode.EINVAL, 'Mount point ' + mountPoint + ' is already in use.'); 158 | } 159 | mounts.set(mountPoint, fs); 160 | } 161 | 162 | /** 163 | * Unmounts the file system at the given mount point. 164 | */ 165 | export function umount(mountPoint: string): void { 166 | if (mountPoint[0] !== '/') { 167 | mountPoint = `/${mountPoint}`; 168 | } 169 | mountPoint = path.resolve(mountPoint); 170 | if (!mounts.has(mountPoint)) { 171 | throw new ApiError(ErrorCode.EINVAL, 'Mount point ' + mountPoint + ' is already unmounted.'); 172 | } 173 | mounts.delete(mountPoint); 174 | } 175 | 176 | /** 177 | * Gets the internal FileSystem for the path, then returns it along with the path relative to the FS' root 178 | */ 179 | export function resolveFS(path: string): { fs: FileSystem; path: string; mountPoint: string } { 180 | const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // decending order of the string length 181 | for (const [mountPoint, fs] of sortedMounts) { 182 | // We know path is normalized, so it would be a substring of the mount point. 183 | if (mountPoint.length <= path.length && path.startsWith(mountPoint)) { 184 | path = path.slice(mountPoint.length > 1 ? mountPoint.length : 0); // Resolve the path relative to the mount point 185 | if (path === '') { 186 | path = '/'; 187 | } 188 | return { fs, path, mountPoint }; 189 | } 190 | } 191 | 192 | throw new ApiError(ErrorCode.EIO, 'BrowserFS not initialized with a file system'); 193 | } 194 | 195 | /** 196 | * Reverse maps the paths in text from the mounted FileSystem to the global path 197 | */ 198 | export function fixPaths(text: string, paths: { [from: string]: string }): string { 199 | for (const [from, to] of Object.entries(paths)) { 200 | text = text.replaceAll(from, to); 201 | } 202 | return text; 203 | } 204 | 205 | export function fixError(e: E, paths: { [from: string]: string }): E { 206 | e.stack = fixPaths(e.stack, paths); 207 | e.message = fixPaths(e.message, paths); 208 | return e; 209 | } 210 | 211 | export function initialize(mountMapping: MountMapping): void { 212 | if (mountMapping['/']) { 213 | umount('/'); 214 | } 215 | for (const [point, fs] of Object.entries(mountMapping)) { 216 | const FS = fs.constructor as unknown as BackendConstructor; 217 | if (!FS.isAvailable()) { 218 | throw new ApiError(ErrorCode.EINVAL, `Can not mount "${point}" since the filesystem is unavailable.`); 219 | } 220 | 221 | mount(point, fs); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/generic/dropbox_bridge.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module 'dropbox_bridge' { 3 | export const Dropbox: typeof DropboxTypes.Dropbox; 4 | export type Types = typeof DropboxTypes; 5 | } 6 | -------------------------------------------------------------------------------- /src/generic/fetch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains utility methods for network I/O (using fetch) 3 | */ 4 | import { Buffer } from 'buffer'; 5 | import { ApiError, ErrorCode } from '../ApiError'; 6 | 7 | export const fetchIsAvailable = typeof fetch !== 'undefined' && fetch !== null; 8 | 9 | /** 10 | * @hidden 11 | */ 12 | function convertError(e): never { 13 | throw new ApiError(ErrorCode.EIO, e.message); 14 | } 15 | 16 | /** 17 | * Asynchronously download a file as a buffer or a JSON object. 18 | * Note that the third function signature with a non-specialized type is 19 | * invalid, but TypeScript requires it when you specialize string arguments to 20 | * constants. 21 | * @hidden 22 | */ 23 | export async function fetchFile(p: string, type: 'buffer'): Promise; 24 | export async function fetchFile(p: string, type: 'json'): Promise; 25 | export async function fetchFile(p: string, type: string): Promise; 26 | export async function fetchFile(p: string, type: string): Promise { 27 | const response = await fetch(p).catch(convertError); 28 | if (!response.ok) { 29 | throw new ApiError(ErrorCode.EIO, `fetch error: response returned code ${response.status}`); 30 | } 31 | switch (type) { 32 | case 'buffer': 33 | const buf = await response.arrayBuffer().catch(convertError); 34 | return Buffer.from(buf); 35 | case 'json': 36 | const json = await response.json().catch(convertError); 37 | return json; 38 | default: 39 | throw new ApiError(ErrorCode.EINVAL, 'Invalid download type: ' + type); 40 | } 41 | } 42 | 43 | /** 44 | * Asynchronously retrieves the size of the given file in bytes. 45 | * @hidden 46 | */ 47 | export async function fetchFileSize(p: string): Promise { 48 | const response = await fetch(p, { method: 'HEAD' }).catch(convertError); 49 | if (!response.ok) { 50 | throw new ApiError(ErrorCode.EIO, `fetch HEAD error: response returned code ${response.status}`); 51 | } 52 | return parseInt(response.headers.get('Content-Length') || '-1', 10); 53 | } 54 | -------------------------------------------------------------------------------- /src/generic/mutex.ts: -------------------------------------------------------------------------------- 1 | export type MutexCallback = () => void; 2 | 3 | /** 4 | * Non-recursive mutex 5 | * @hidden 6 | */ 7 | export default class Mutex { 8 | private _locks: Map = new Map(); 9 | 10 | public lock(path: string): Promise { 11 | return new Promise(resolve => { 12 | if (this._locks.has(path)) { 13 | this._locks.get(path).push(resolve); 14 | } else { 15 | this._locks.set(path, []); 16 | } 17 | }); 18 | } 19 | 20 | public unlock(path: string): void { 21 | if (!this._locks.has(path)) { 22 | throw new Error('unlock of a non-locked mutex'); 23 | } 24 | 25 | const next = this._locks.get(path).shift(); 26 | /* 27 | don't unlock - we want to queue up next for the 28 | end of the current task execution, but we don't 29 | want it to be called inline with whatever the 30 | current stack is. This way we still get the nice 31 | behavior that an unlock immediately followed by a 32 | lock won't cause starvation. 33 | */ 34 | if (next) { 35 | setTimeout(next, 0); 36 | return; 37 | } 38 | 39 | this._locks.delete(path); 40 | } 41 | 42 | public tryLock(path: string): boolean { 43 | if (this._locks.has(path)) { 44 | return false; 45 | } 46 | 47 | this._locks.set(path, []); 48 | return true; 49 | } 50 | 51 | public isLocked(path: string): boolean { 52 | return this._locks.has(path); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * BrowserFS's main module. This is exposed in the browser via the BrowserFS global. 3 | */ 4 | 5 | import fs from './emulation/fs'; 6 | import { FileSystem, type BFSOneArgCallback, type BFSCallback } from './filesystem'; 7 | import EmscriptenFS from './generic/emscripten_fs'; 8 | import { backends } from './backends'; 9 | import { ErrorCode, ApiError } from './ApiError'; 10 | import { Cred } from './cred'; 11 | import * as process from 'process'; 12 | import type { BackendConstructor } from './backends/backend'; 13 | import { type MountMapping, setCred } from './emulation/shared'; 14 | 15 | if (process && (process)['initializeTTYs']) { 16 | (process)['initializeTTYs'](); 17 | } 18 | 19 | /** 20 | * @hidden 21 | */ 22 | export function registerBackend(name: string, fs: BackendConstructor) { 23 | backends[name] = fs; 24 | } 25 | 26 | /** 27 | * Initializes BrowserFS with the given file systems. 28 | */ 29 | export function initialize(mounts: { [point: string]: FileSystem }, uid: number = 0, gid: number = 0) { 30 | setCred(new Cred(uid, gid, uid, gid, uid, gid)); 31 | return fs.initialize(mounts); 32 | } 33 | 34 | /** 35 | * Defines a mapping of mount points to their configurations 36 | */ 37 | export interface ConfigMapping { 38 | [mountPoint: string]: FileSystem | FileSystemConfiguration | keyof typeof backends; 39 | } 40 | 41 | /** 42 | * A configuration for BrowserFS 43 | */ 44 | export type Configuration = FileSystem | FileSystemConfiguration | ConfigMapping; 45 | 46 | async function _configure(config: Configuration): Promise { 47 | if ('fs' in config || config instanceof FileSystem) { 48 | // single FS 49 | config = { '/': config } as ConfigMapping; 50 | } 51 | for (let [point, value] of Object.entries(config)) { 52 | if (typeof value == 'number') { 53 | //should never happen 54 | continue; 55 | } 56 | point = point.toString(); // so linting stops complaining that point should be declared with const, which can't be done since value is assigned to 57 | 58 | if (value instanceof FileSystem) { 59 | continue; 60 | } 61 | 62 | if (typeof value == 'string') { 63 | value = { fs: value }; 64 | } 65 | 66 | config[point] = await getFileSystem(value); 67 | } 68 | return initialize(config as MountMapping); 69 | } 70 | 71 | /** 72 | * Creates a file system with the given configuration, and initializes BrowserFS with it. 73 | * See the FileSystemConfiguration type for more info on the configuration object. 74 | */ 75 | export function configure(config: Configuration): Promise; 76 | export function configure(config: Configuration, cb: BFSOneArgCallback): void; 77 | export function configure(config: Configuration, cb?: BFSOneArgCallback): Promise | void { 78 | // Promise version 79 | if (typeof cb != 'function') { 80 | return _configure(config); 81 | } 82 | 83 | // Callback version 84 | _configure(config) 85 | .then(() => cb()) 86 | .catch(err => cb(err)); 87 | return; 88 | } 89 | 90 | /** 91 | * Asynchronously creates a file system with the given configuration, and initializes BrowserFS with it. 92 | * See the FileSystemConfiguration type for more info on the configuration object. 93 | * Note: unlike configure, the .then is provided with the file system 94 | */ 95 | 96 | /** 97 | * Specifies a file system backend type and its options. 98 | * 99 | * Individual options can recursively contain FileSystemConfiguration objects for 100 | * option values that require file systems. 101 | * 102 | * For example, to mirror Dropbox to Storage with AsyncMirror, use the following 103 | * object: 104 | * 105 | * ```javascript 106 | * var config = { 107 | * fs: "AsyncMirror", 108 | * options: { 109 | * sync: {fs: "Storage"}, 110 | * async: {fs: "Dropbox", options: {client: anAuthenticatedDropboxSDKClient }} 111 | * } 112 | * }; 113 | * ``` 114 | * 115 | * The option object for each file system corresponds to that file system's option object passed to its `Create()` method. 116 | */ 117 | export interface FileSystemConfiguration { 118 | fs: string; 119 | options?: object; 120 | } 121 | 122 | async function _getFileSystem({ fs: fsName, options = {} }: FileSystemConfiguration): Promise { 123 | if (!fsName) { 124 | throw new ApiError(ErrorCode.EPERM, 'Missing "fs" property on configuration object.'); 125 | } 126 | 127 | if (typeof options !== 'object' || options === null) { 128 | throw new ApiError(ErrorCode.EINVAL, 'Invalid "options" property on configuration object.'); 129 | } 130 | 131 | const props = Object.keys(options).filter(k => k != 'fs'); 132 | 133 | for (const prop of props) { 134 | const opt = options[prop]; 135 | if (opt === null || typeof opt !== 'object' || !('fs' in opt)) { 136 | continue; 137 | } 138 | 139 | const fs = await _getFileSystem(opt); 140 | options[prop] = fs; 141 | } 142 | 143 | const fsc = (backends)[fsName]; 144 | if (!fsc) { 145 | throw new ApiError(ErrorCode.EPERM, `File system ${fsName} is not available in BrowserFS.`); 146 | } else { 147 | return fsc.Create(options); 148 | } 149 | } 150 | 151 | /** 152 | * Retrieve a file system with the given configuration. Will return a promise if invoked without a callback 153 | * @param config A FileSystemConfiguration object. See FileSystemConfiguration for details. 154 | * @param cb Called when the file system is constructed, or when an error occurs. 155 | */ 156 | export function getFileSystem(config: FileSystemConfiguration): Promise; 157 | export function getFileSystem(config: FileSystemConfiguration, cb: BFSCallback): void; 158 | export function getFileSystem(config: FileSystemConfiguration, cb?: BFSCallback): Promise | void { 159 | // Promise version 160 | if (typeof cb != 'function') { 161 | return _getFileSystem(config); 162 | } 163 | 164 | // Callback version 165 | _getFileSystem(config) 166 | .then(fs => cb(null, fs)) 167 | .catch(err => cb(err)); 168 | return; 169 | } 170 | 171 | export * from './cred'; 172 | export * from './inode'; 173 | export * from './stats'; 174 | export * from './file'; 175 | export * from './filesystem'; 176 | export * from './backends'; 177 | export * from './ApiError'; 178 | export * from './generic/key_value_filesystem'; 179 | export { fs, EmscriptenFS }; 180 | export default fs; 181 | -------------------------------------------------------------------------------- /src/inode.ts: -------------------------------------------------------------------------------- 1 | import { Stats, FileType } from './stats'; 2 | import { Buffer } from 'buffer'; 3 | 4 | /** 5 | * Generic inode definition that can easily be serialized. 6 | */ 7 | export default class Inode { 8 | /** 9 | * Converts the buffer into an Inode. 10 | */ 11 | public static fromBuffer(buffer: Buffer): Inode { 12 | if (buffer === undefined) { 13 | throw new Error('NO'); 14 | } 15 | return new Inode( 16 | buffer.toString('ascii', 38), 17 | buffer.readUInt32LE(0), 18 | buffer.readUInt16LE(4), 19 | buffer.readDoubleLE(6), 20 | buffer.readDoubleLE(14), 21 | buffer.readDoubleLE(22), 22 | buffer.readUInt32LE(30), 23 | buffer.readUInt32LE(34) 24 | ); 25 | } 26 | 27 | constructor( 28 | public id: string, 29 | public size: number, 30 | public mode: number, 31 | public atime: number, 32 | public mtime: number, 33 | public ctime: number, 34 | public uid: number, 35 | public gid: number 36 | ) {} 37 | 38 | /** 39 | * Handy function that converts the Inode to a Node Stats object. 40 | */ 41 | public toStats(): Stats { 42 | return new Stats( 43 | (this.mode & 0xf000) === FileType.DIRECTORY ? FileType.DIRECTORY : FileType.FILE, 44 | this.size, 45 | this.mode, 46 | this.atime, 47 | this.mtime, 48 | this.ctime, 49 | this.uid, 50 | this.gid 51 | ); 52 | } 53 | 54 | /** 55 | * Get the size of this Inode, in bytes. 56 | */ 57 | public getSize(): number { 58 | // ASSUMPTION: ID is ASCII (1 byte per char). 59 | return 38 + this.id.length; 60 | } 61 | 62 | /** 63 | * Writes the inode into the start of the buffer. 64 | */ 65 | public toBuffer(buff: Buffer = Buffer.alloc(this.getSize())): Buffer { 66 | buff.writeUInt32LE(this.size, 0); 67 | buff.writeUInt16LE(this.mode, 4); 68 | buff.writeDoubleLE(this.atime, 6); 69 | buff.writeDoubleLE(this.mtime, 14); 70 | buff.writeDoubleLE(this.ctime, 22); 71 | buff.writeUInt32LE(this.uid, 30); 72 | buff.writeUInt32LE(this.gid, 34); 73 | buff.write(this.id, 38, this.id.length, 'ascii'); 74 | return buff; 75 | } 76 | 77 | /** 78 | * Updates the Inode using information from the stats object. Used by file 79 | * systems at sync time, e.g.: 80 | * - Program opens file and gets a File object. 81 | * - Program mutates file. File object is responsible for maintaining 82 | * metadata changes locally -- typically in a Stats object. 83 | * - Program closes file. File object's metadata changes are synced with the 84 | * file system. 85 | * @return True if any changes have occurred. 86 | */ 87 | public update(stats: Stats): boolean { 88 | let hasChanged = false; 89 | if (this.size !== stats.size) { 90 | this.size = stats.size; 91 | hasChanged = true; 92 | } 93 | 94 | if (this.mode !== stats.mode) { 95 | this.mode = stats.mode; 96 | hasChanged = true; 97 | } 98 | 99 | const atimeMs = stats.atime.getTime(); 100 | if (this.atime !== atimeMs) { 101 | this.atime = atimeMs; 102 | hasChanged = true; 103 | } 104 | 105 | const mtimeMs = stats.mtime.getTime(); 106 | if (this.mtime !== mtimeMs) { 107 | this.mtime = mtimeMs; 108 | hasChanged = true; 109 | } 110 | 111 | const ctimeMs = stats.ctime.getTime(); 112 | if (this.ctime !== ctimeMs) { 113 | this.ctime = ctimeMs; 114 | hasChanged = true; 115 | } 116 | 117 | if (this.uid !== stats.uid) { 118 | this.uid = stats.uid; 119 | hasChanged = true; 120 | } 121 | 122 | if (this.uid !== stats.uid) { 123 | this.uid = stats.uid; 124 | hasChanged = true; 125 | } 126 | 127 | return hasChanged; 128 | } 129 | 130 | // XXX: Copied from Stats. Should reconcile these two into something more 131 | // compact. 132 | 133 | /** 134 | * @return [Boolean] True if this item is a file. 135 | */ 136 | public isFile(): boolean { 137 | return (this.mode & 0xf000) === FileType.FILE; 138 | } 139 | 140 | /** 141 | * @return [Boolean] True if this item is a directory. 142 | */ 143 | public isDirectory(): boolean { 144 | return (this.mode & 0xf000) === FileType.DIRECTORY; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/stats.ts: -------------------------------------------------------------------------------- 1 | import type { StatsBase } from 'fs'; 2 | import { Cred } from './cred'; 3 | import { Buffer } from 'buffer'; 4 | import { S_IFDIR, S_IFLNK, S_IFMT, S_IFREG } from './emulation/constants'; 5 | 6 | /** 7 | * Indicates the type of the given file. Applied to 'mode'. 8 | */ 9 | export enum FileType { 10 | FILE = S_IFREG, 11 | DIRECTORY = S_IFDIR, 12 | SYMLINK = S_IFLNK, 13 | } 14 | 15 | /** 16 | * Implementation of Node's `Stats`. 17 | * 18 | * Attribute descriptions are from `man 2 stat' 19 | * @see http://nodejs.org/api/fs.html#fs_class_fs_stats 20 | * @see http://man7.org/linux/man-pages/man2/stat.2.html 21 | */ 22 | export class Stats implements StatsBase { 23 | public static fromBuffer(buffer: Buffer): Stats { 24 | const size = buffer.readUInt32LE(0), 25 | mode = buffer.readUInt32LE(4), 26 | atime = buffer.readDoubleLE(8), 27 | mtime = buffer.readDoubleLE(16), 28 | ctime = buffer.readDoubleLE(24), 29 | uid = buffer.readUInt32LE(32), 30 | gid = buffer.readUInt32LE(36); 31 | 32 | return new Stats(mode & S_IFMT, size, mode & ~S_IFMT, atime, mtime, ctime, uid, gid); 33 | } 34 | 35 | /** 36 | * Clones the stats object. 37 | */ 38 | public static clone(s: Stats): Stats { 39 | return new Stats(s.mode & S_IFMT, s.size, s.mode & ~S_IFMT, s.atimeMs, s.mtimeMs, s.ctimeMs, s.uid, s.gid, s.birthtimeMs); 40 | } 41 | 42 | public blocks: number; 43 | public mode: number; 44 | // ID of device containing file 45 | public dev: number = 0; 46 | // inode number 47 | public ino: number = 0; 48 | // device ID (if special file) 49 | public rdev: number = 0; 50 | // number of hard links 51 | public nlink: number = 1; 52 | // blocksize for file system I/O 53 | public blksize: number = 4096; 54 | // user ID of owner 55 | public uid: number = 0; 56 | // group ID of owner 57 | public gid: number = 0; 58 | // Some file systems stash data on stats objects. 59 | public fileData: Buffer | null = null; 60 | public atimeMs: number; 61 | public mtimeMs: number; 62 | public ctimeMs: number; 63 | public birthtimeMs: number; 64 | public size: number; 65 | 66 | public get atime(): Date { 67 | return new Date(this.atimeMs); 68 | } 69 | 70 | public get mtime(): Date { 71 | return new Date(this.mtimeMs); 72 | } 73 | 74 | public get ctime(): Date { 75 | return new Date(this.ctimeMs); 76 | } 77 | 78 | public get birthtime(): Date { 79 | return new Date(this.birthtimeMs); 80 | } 81 | 82 | /** 83 | * Provides information about a particular entry in the file system. 84 | * @param itemType Type of the item (FILE, DIRECTORY, SYMLINK, or SOCKET) 85 | * @param size Size of the item in bytes. For directories/symlinks, 86 | * this is normally the size of the struct that represents the item. 87 | * @param mode Unix-style file mode (e.g. 0o644) 88 | * @param atimeMs time of last access, in milliseconds since epoch 89 | * @param mtimeMs time of last modification, in milliseconds since epoch 90 | * @param ctimeMs time of last time file status was changed, in milliseconds since epoch 91 | * @param uid the id of the user that owns the file 92 | * @param gid the id of the group that owns the file 93 | * @param birthtimeMs time of file creation, in milliseconds since epoch 94 | */ 95 | constructor(itemType: FileType, size: number, mode?: number, atimeMs?: number, mtimeMs?: number, ctimeMs?: number, uid?: number, gid?: number, birthtimeMs?: number) { 96 | this.size = size; 97 | let currentTime = 0; 98 | if (typeof atimeMs !== 'number') { 99 | currentTime = Date.now(); 100 | atimeMs = currentTime; 101 | } 102 | if (typeof mtimeMs !== 'number') { 103 | if (!currentTime) { 104 | currentTime = Date.now(); 105 | } 106 | mtimeMs = currentTime; 107 | } 108 | if (typeof ctimeMs !== 'number') { 109 | if (!currentTime) { 110 | currentTime = Date.now(); 111 | } 112 | ctimeMs = currentTime; 113 | } 114 | if (typeof birthtimeMs !== 'number') { 115 | if (!currentTime) { 116 | currentTime = Date.now(); 117 | } 118 | birthtimeMs = currentTime; 119 | } 120 | if (typeof uid !== 'number') { 121 | uid = 0; 122 | } 123 | if (typeof gid !== 'number') { 124 | gid = 0; 125 | } 126 | this.atimeMs = atimeMs; 127 | this.ctimeMs = ctimeMs; 128 | this.mtimeMs = mtimeMs; 129 | this.birthtimeMs = birthtimeMs; 130 | 131 | if (!mode) { 132 | switch (itemType) { 133 | case FileType.FILE: 134 | this.mode = 0o644; 135 | break; 136 | case FileType.DIRECTORY: 137 | default: 138 | this.mode = 0o777; 139 | } 140 | } else { 141 | this.mode = mode; 142 | } 143 | // number of 512B blocks allocated 144 | this.blocks = Math.ceil(size / 512); 145 | // Check if mode also includes top-most bits, which indicate the file's 146 | // type. 147 | if ((this.mode & S_IFMT) == 0) { 148 | this.mode |= itemType; 149 | } 150 | } 151 | 152 | public toBuffer(): Buffer { 153 | const buffer = Buffer.alloc(32); 154 | buffer.writeUInt32LE(this.size, 0); 155 | buffer.writeUInt32LE(this.mode, 4); 156 | buffer.writeDoubleLE(this.atime.getTime(), 8); 157 | buffer.writeDoubleLE(this.mtime.getTime(), 16); 158 | buffer.writeDoubleLE(this.ctime.getTime(), 24); 159 | buffer.writeUInt32LE(this.uid, 32); 160 | buffer.writeUInt32LE(this.gid, 36); 161 | return buffer; 162 | } 163 | 164 | /** 165 | * @return [Boolean] True if this item is a file. 166 | */ 167 | public isFile(): boolean { 168 | return (this.mode & S_IFMT) === S_IFREG; 169 | } 170 | 171 | /** 172 | * @return [Boolean] True if this item is a directory. 173 | */ 174 | public isDirectory(): boolean { 175 | return (this.mode & S_IFMT) === S_IFDIR; 176 | } 177 | 178 | /** 179 | * @return [Boolean] True if this item is a symbolic link (only valid through lstat) 180 | */ 181 | public isSymbolicLink(): boolean { 182 | return (this.mode & S_IFMT) === S_IFLNK; 183 | } 184 | 185 | /** 186 | * Checks if a given user/group has access to this item 187 | * @param mode The request access as 4 bits (unused, read, write, execute) 188 | * @param uid The requesting UID 189 | * @param gid The requesting GID 190 | * @returns [Boolean] True if the request has access, false if the request does not 191 | */ 192 | public hasAccess(mode: number, cred: Cred): boolean { 193 | if (cred.euid === 0 || cred.egid === 0) { 194 | //Running as root 195 | return true; 196 | } 197 | const perms = this.mode & ~S_IFMT; 198 | let uMode = 0xf, 199 | gMode = 0xf, 200 | wMode = 0xf; 201 | 202 | if (cred.euid == this.uid) { 203 | const uPerms = (0xf00 & perms) >> 8; 204 | uMode = (mode ^ uPerms) & mode; 205 | } 206 | if (cred.egid == this.gid) { 207 | const gPerms = (0xf0 & perms) >> 4; 208 | gMode = (mode ^ gPerms) & mode; 209 | } 210 | const wPerms = 0xf & perms; 211 | wMode = (mode ^ wPerms) & mode; 212 | /* 213 | Result = 0b0xxx (read, write, execute) 214 | If any bits are set that means the request does not have that permission. 215 | */ 216 | const result = uMode & gMode & wMode; 217 | return !result; 218 | } 219 | 220 | /** 221 | * Convert the current stats object into a cred object 222 | */ 223 | public getCred(uid: number = this.uid, gid: number = this.gid): Cred { 224 | return new Cred(uid, gid, this.uid, this.gid, uid, gid); 225 | } 226 | 227 | /** 228 | * Change the mode of the file. We use this helper function to prevent messing 229 | * up the type of the file, which is encoded in mode. 230 | */ 231 | public chmod(mode: number): void { 232 | this.mode = (this.mode & S_IFMT) | mode; 233 | } 234 | 235 | /** 236 | * Change the owner user/group of the file. 237 | * This function makes sure it is a valid UID/GID (that is, a 32 unsigned int) 238 | */ 239 | public chown(uid: number, gid: number): void { 240 | if (!isNaN(+uid) && 0 <= +uid && +uid < 2 ** 32) { 241 | this.uid = uid; 242 | } 243 | if (!isNaN(+gid) && 0 <= +gid && +gid < 2 ** 32) { 244 | this.gid = gid; 245 | } 246 | } 247 | 248 | // We don't support the following types of files. 249 | 250 | public isSocket(): boolean { 251 | return false; 252 | } 253 | 254 | public isBlockDevice(): boolean { 255 | return false; 256 | } 257 | 258 | public isCharacterDevice(): boolean { 259 | return false; 260 | } 261 | 262 | public isFIFO(): boolean { 263 | return false; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Grab bag of utility functions used across the code. 3 | */ 4 | import { FileSystem } from './filesystem'; 5 | import { ErrorCode, ApiError } from './ApiError'; 6 | import * as path from 'path'; 7 | import { Cred } from './cred'; 8 | import { Buffer } from 'buffer'; 9 | import type { BackendConstructor, BackendOptions, BaseBackendConstructor } from './backends/backend'; 10 | 11 | /** 12 | * Synchronous recursive makedir. 13 | * @hidden 14 | */ 15 | export function mkdirpSync(p: string, mode: number, cred: Cred, fs: FileSystem): void { 16 | if (!fs.existsSync(p, cred)) { 17 | mkdirpSync(path.dirname(p), mode, cred, fs); 18 | fs.mkdirSync(p, mode, cred); 19 | } 20 | } 21 | 22 | /** 23 | * Copies a slice of the given buffer 24 | * @hidden 25 | */ 26 | export function copyingSlice(buff: Buffer, start: number = 0, end = buff.length): Buffer { 27 | if (start < 0 || end < 0 || end > buff.length || start > end) { 28 | throw new TypeError(`Invalid slice bounds on buffer of length ${buff.length}: [${start}, ${end}]`); 29 | } 30 | if (buff.length === 0) { 31 | // Avoid s0 corner case in ArrayBuffer case. 32 | return Buffer.alloc(0); 33 | } else { 34 | return buff.subarray(start, end); 35 | } 36 | } 37 | 38 | /** 39 | * Option validator for a Buffer file system option. 40 | * @hidden 41 | */ 42 | export async function bufferValidator(v: object): Promise { 43 | if (!Buffer.isBuffer(v)) { 44 | throw new ApiError(ErrorCode.EINVAL, 'option must be a Buffer.'); 45 | } 46 | } 47 | 48 | /* 49 | * Levenshtein distance, from the `js-levenshtein` NPM module. 50 | * Copied here to avoid complexity of adding another CommonJS module dependency. 51 | */ 52 | 53 | function _min(d0: number, d1: number, d2: number, bx: number, ay: number): number { 54 | return Math.min(d0 + 1, d1 + 1, d2 + 1, bx === ay ? d1 : d1 + 1); 55 | } 56 | 57 | /** 58 | * Calculates levenshtein distance. 59 | * @param a 60 | * @param b 61 | */ 62 | function levenshtein(a: string, b: string): number { 63 | if (a === b) { 64 | return 0; 65 | } 66 | 67 | if (a.length > b.length) { 68 | [a, b] = [b, a]; // Swap a and b 69 | } 70 | 71 | let la = a.length; 72 | let lb = b.length; 73 | 74 | // Trim common suffix 75 | while (la > 0 && a.charCodeAt(la - 1) === b.charCodeAt(lb - 1)) { 76 | la--; 77 | lb--; 78 | } 79 | 80 | let offset = 0; 81 | 82 | // Trim common prefix 83 | while (offset < la && a.charCodeAt(offset) === b.charCodeAt(offset)) { 84 | offset++; 85 | } 86 | 87 | la -= offset; 88 | lb -= offset; 89 | 90 | if (la === 0 || lb === 1) { 91 | return lb; 92 | } 93 | 94 | const vector = new Array(la << 1); 95 | 96 | for (let y = 0; y < la; ) { 97 | vector[la + y] = a.charCodeAt(offset + y); 98 | vector[y] = ++y; 99 | } 100 | 101 | let x: number; 102 | let d0: number; 103 | let d1: number; 104 | let d2: number; 105 | let d3: number; 106 | for (x = 0; x + 3 < lb; ) { 107 | const bx0 = b.charCodeAt(offset + (d0 = x)); 108 | const bx1 = b.charCodeAt(offset + (d1 = x + 1)); 109 | const bx2 = b.charCodeAt(offset + (d2 = x + 2)); 110 | const bx3 = b.charCodeAt(offset + (d3 = x + 3)); 111 | let dd = (x += 4); 112 | for (let y = 0; y < la; ) { 113 | const ay = vector[la + y]; 114 | const dy = vector[y]; 115 | d0 = _min(dy, d0, d1, bx0, ay); 116 | d1 = _min(d0, d1, d2, bx1, ay); 117 | d2 = _min(d1, d2, d3, bx2, ay); 118 | dd = _min(d2, d3, dd, bx3, ay); 119 | vector[y++] = dd; 120 | d3 = d2; 121 | d2 = d1; 122 | d1 = d0; 123 | d0 = dy; 124 | } 125 | } 126 | 127 | let dd: number = 0; 128 | for (; x < lb; ) { 129 | const bx0 = b.charCodeAt(offset + (d0 = x)); 130 | dd = ++x; 131 | for (let y = 0; y < la; y++) { 132 | const dy = vector[y]; 133 | vector[y] = dd = dy < d0 || dd < d0 ? (dy > dd ? dd + 1 : dy + 1) : bx0 === vector[la + y] ? d0 : d0 + 1; 134 | d0 = dy; 135 | } 136 | } 137 | 138 | return dd; 139 | } 140 | 141 | /** 142 | * Checks that the given options object is valid for the file system options. 143 | * @hidden 144 | */ 145 | export async function checkOptions(backend: BaseBackendConstructor, opts: object): Promise { 146 | const optsInfo = backend.Options; 147 | const fsName = backend.Name; 148 | 149 | let pendingValidators = 0; 150 | let callbackCalled = false; 151 | let loopEnded = false; 152 | 153 | // Check for required options. 154 | for (const optName in optsInfo) { 155 | if (Object.prototype.hasOwnProperty.call(optsInfo, optName)) { 156 | const opt = optsInfo[optName]; 157 | const providedValue = opts && opts[optName]; 158 | 159 | if (providedValue === undefined || providedValue === null) { 160 | if (!opt.optional) { 161 | // Required option, not provided. 162 | // Any incorrect options provided? Which ones are close to the provided one? 163 | // (edit distance 5 === close) 164 | const incorrectOptions = Object.keys(opts) 165 | .filter(o => !(o in optsInfo)) 166 | .map((a: string) => { 167 | return { str: a, distance: levenshtein(optName, a) }; 168 | }) 169 | .filter(o => o.distance < 5) 170 | .sort((a, b) => a.distance - b.distance); 171 | // Validators may be synchronous. 172 | if (callbackCalled) { 173 | return; 174 | } 175 | callbackCalled = true; 176 | throw new ApiError( 177 | ErrorCode.EINVAL, 178 | `[${fsName}] Required option '${optName}' not provided.${ 179 | incorrectOptions.length > 0 ? ` You provided unrecognized option '${incorrectOptions[0].str}'; perhaps you meant to type '${optName}'.` : '' 180 | }\nOption description: ${opt.description}` 181 | ); 182 | } 183 | // Else: Optional option, not provided. That is OK. 184 | } else { 185 | // Option provided! Check type. 186 | let typeMatches = false; 187 | if (Array.isArray(opt.type)) { 188 | typeMatches = opt.type.indexOf(typeof providedValue) !== -1; 189 | } else { 190 | typeMatches = typeof providedValue === opt.type; 191 | } 192 | if (!typeMatches) { 193 | // Validators may be synchronous. 194 | if (callbackCalled) { 195 | return; 196 | } 197 | callbackCalled = true; 198 | throw new ApiError( 199 | ErrorCode.EINVAL, 200 | `[${fsName}] Value provided for option ${optName} is not the proper type. Expected ${ 201 | Array.isArray(opt.type) ? `one of {${opt.type.join(', ')}}` : opt.type 202 | }, but received ${typeof providedValue}\nOption description: ${opt.description}` 203 | ); 204 | } else if (opt.validator) { 205 | pendingValidators++; 206 | try { 207 | await opt.validator(providedValue); 208 | } catch (e) { 209 | if (!callbackCalled) { 210 | if (e) { 211 | callbackCalled = true; 212 | throw e; 213 | } 214 | pendingValidators--; 215 | if (pendingValidators === 0 && loopEnded) { 216 | return; 217 | } 218 | } 219 | } 220 | } 221 | // Otherwise: All good! 222 | } 223 | } 224 | } 225 | loopEnded = true; 226 | if (pendingValidators === 0 && !callbackCalled) { 227 | return; 228 | } 229 | } 230 | 231 | /** Waits n ms. */ 232 | export function wait(ms: number): Promise { 233 | return new Promise(resolve => { 234 | setTimeout(resolve, ms); 235 | }); 236 | } 237 | 238 | /** 239 | * Converts a callback into a promise. Assumes last parameter is the callback 240 | * @todo Look at changing resolve value from cbArgs[0] to include other callback arguments? 241 | */ 242 | export function toPromise(fn: (...fnArgs: unknown[]) => unknown) { 243 | return function (...args: unknown[]): Promise { 244 | return new Promise((resolve, reject) => { 245 | args.push((e: ApiError, ...cbArgs: unknown[]) => { 246 | if (e) { 247 | reject(e); 248 | } else { 249 | resolve(cbArgs[0]); 250 | } 251 | }); 252 | fn(...args); 253 | }); 254 | }; 255 | } 256 | 257 | /** 258 | * @hidden 259 | */ 260 | export const setImmediate = typeof globalThis.setImmediate == 'function' ? globalThis.setImmediate : cb => setTimeout(cb, 0); 261 | -------------------------------------------------------------------------------- /test/common.ts: -------------------------------------------------------------------------------- 1 | import { Stats, FileType } from '../src/stats'; 2 | import { configure as _configure, fs } from '../src/index'; 3 | import * as path from 'node:path'; 4 | import * as _fs from 'node:fs'; 5 | 6 | export const tmpDir = 'tmp/'; 7 | export const fixturesDir = 'test/fixtures/files/node'; 8 | 9 | function copy(srcFS: typeof _fs, dstFS: typeof fs, _p: string) { 10 | const p = path.posix.resolve(_p); 11 | const stats = srcFS.statSync(p); 12 | 13 | if (!stats.isDirectory()) { 14 | dstFS.writeFileSync(p, srcFS.readFileSync(_p)); 15 | return; 16 | } 17 | 18 | dstFS.mkdirSync(p); 19 | for (const file of srcFS.readdirSync(_p)) { 20 | copy(srcFS, dstFS, path.posix.join(p, file)); 21 | } 22 | } 23 | 24 | export async function configure(config) { 25 | const result = await _configure(config); 26 | copy(_fs, fs, fixturesDir); 27 | return result; 28 | } 29 | 30 | export { fs }; 31 | 32 | export function createMockStats(mode): Stats { 33 | return new Stats(FileType.FILE, -1, mode); 34 | } 35 | 36 | const tests /*: { [B in keyof typeof Backends]: Parameters[0] }*/ = { 37 | AsyncMirror: { sync: { fs: 'InMemory' }, async: { fs: 'InMemory' } }, 38 | //Dropbox: {}, 39 | //Emscripten: {}, 40 | //FileSystemAccess: {}, 41 | FolderAdapter: { wrapped: { fs: 'InMemory' }, folder: '/example' }, 42 | InMemory: {}, 43 | //IndexedDB: {}, 44 | //IsoFS: {}, 45 | //Storage: {}, 46 | OverlayFS: { readable: { fs: 'InMemory' }, writable: { fs: 'InMemory' } }, 47 | //WorkerFS: {}, 48 | //HTTPRequest: {}, 49 | //ZipFS: {}, 50 | }; 51 | 52 | export const backends = Object.entries(tests); 53 | -------------------------------------------------------------------------------- /test/fixtures/README.md: -------------------------------------------------------------------------------- 1 | This directory contains data files used during testing. 2 | 3 | - `files`: Generic test files shared by all backends 4 | - `dropbox` (dynamically generated): Certification information used by `dropbox-js`. 5 | - `zipfs` (dynamically generated): Multiple zipped versions of `files`. 6 | -------------------------------------------------------------------------------- /test/fixtures/files/emscripten/files.err: -------------------------------------------------------------------------------- 1 | texte 2 | -------------------------------------------------------------------------------- /test/fixtures/files/emscripten/files.out: -------------------------------------------------------------------------------- 1 | size: 7 2 | data: 100,-56,50,25,10,77,123 3 | loop: 100 -56 50 25 10 77 123 4 | texto 5 | $ 6 | 5 : 10,30,20,11,88 7 | fscanfed: 10 - hello 8 | 5 bytes to dev/null: 5 9 | ok. 10 | -------------------------------------------------------------------------------- /test/fixtures/files/emscripten/somefile.binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvilk/BrowserFS/76fd5122fcf3ad6bff3315550aafb041cfb6a72e/test/fixtures/files/emscripten/somefile.binary -------------------------------------------------------------------------------- /test/fixtures/files/isofs/1/2/3/4/5/6/7/8/test_file.txt: -------------------------------------------------------------------------------- 1 | ISO9660 has a limit of 7 nested directories. 2 | RockRidge has a hacky way of getting around it. 3 | This file is to test that this functionality does not break. 4 | -------------------------------------------------------------------------------- /test/fixtures/files/node/a.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // 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 permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | var c = require('./b/c'); 23 | 24 | console.error('load fixtures/a.js'); 25 | 26 | var string = 'A'; 27 | 28 | exports.SomeClass = c.SomeClass; 29 | 30 | exports.A = function () { 31 | return string; 32 | }; 33 | 34 | exports.C = function () { 35 | return c.C(); 36 | }; 37 | 38 | exports.D = function () { 39 | return c.D(); 40 | }; 41 | 42 | exports.number = 42; 43 | 44 | process.on('exit', function () { 45 | string = 'A done'; 46 | }); 47 | -------------------------------------------------------------------------------- /test/fixtures/files/node/a1.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // 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 permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | var c = require('./b/c'); 23 | 24 | console.error('load fixtures/a.js'); 25 | 26 | var string = 'A'; 27 | 28 | exports.SomeClass = c.SomeClass; 29 | 30 | exports.A = function () { 31 | return string; 32 | }; 33 | 34 | exports.C = function () { 35 | return c.C(); 36 | }; 37 | 38 | exports.D = function () { 39 | return c.D(); 40 | }; 41 | 42 | exports.number = 42; 43 | 44 | process.on('exit', function () { 45 | string = 'A done'; 46 | }); 47 | -------------------------------------------------------------------------------- /test/fixtures/files/node/elipses.txt: -------------------------------------------------------------------------------- 1 | ………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………… -------------------------------------------------------------------------------- /test/fixtures/files/node/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvilk/BrowserFS/76fd5122fcf3ad6bff3315550aafb041cfb6a72e/test/fixtures/files/node/empty.txt -------------------------------------------------------------------------------- /test/fixtures/files/node/exit.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // 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 permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | process.exit(process.argv[2] || 1); 23 | -------------------------------------------------------------------------------- /test/fixtures/files/node/x.txt: -------------------------------------------------------------------------------- 1 | xyz 2 | -------------------------------------------------------------------------------- /test/fixtures/isofs/test_joliet.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvilk/BrowserFS/76fd5122fcf3ad6bff3315550aafb041cfb6a72e/test/fixtures/isofs/test_joliet.iso -------------------------------------------------------------------------------- /test/fixtures/isofs/test_rock_ridge.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvilk/BrowserFS/76fd5122fcf3ad6bff3315550aafb041cfb6a72e/test/fixtures/isofs/test_rock_ridge.iso -------------------------------------------------------------------------------- /test/fixtures/static/49chars.txt: -------------------------------------------------------------------------------- 1 | 0123456789abcdef0123456789abcdef0123456789abcdef 2 | -------------------------------------------------------------------------------- /test/tests/HTTPRequest/listing.ts: -------------------------------------------------------------------------------- 1 | import { fs } from '../../common'; 2 | import * as BrowserFS from '../../../src'; 3 | import { promisify } from 'node:util'; 4 | 5 | type Listing = { [name: string]: Listing | null }; 6 | 7 | describe('HTTPDownloadFS', () => { 8 | let oldRootFS: BrowserFS.FileSystem; 9 | 10 | beforeAll(() => { 11 | oldRootFS = fs.getMount('/'); 12 | }); 13 | 14 | afterAll(() => { 15 | BrowserFS.initialize({ '/': oldRootFS }); 16 | }); 17 | 18 | it('File System Operations', async () => { 19 | const listing: Listing = { 20 | 'README.md': null, 21 | test: { 22 | fixtures: { 23 | static: { 24 | '49chars.txt': null, 25 | }, 26 | }, 27 | }, 28 | src: { 29 | 'README.md': null, 30 | backend: { 'AsyncMirror.ts': null, 'XmlHttpRequest.ts': null, 'ZipFS.ts': null }, 31 | 'main.ts': null, 32 | }, 33 | }; 34 | 35 | const newXFS = await BrowserFS.backends.HTTPRequest.Create({ 36 | index: listing, 37 | baseUrl: '/', 38 | }); 39 | 40 | BrowserFS.initialize({ '/': newXFS }); 41 | 42 | const expectedTestListing = ['README.md', 'src', 'test']; 43 | const testListing = fs.readdirSync('/').sort(); 44 | expect(testListing).toEqual(expectedTestListing); 45 | 46 | const readdirAsync = promisify(fs.readdir); 47 | const files = await readdirAsync('/'); 48 | expect(files.sort()).toEqual(expectedTestListing); 49 | 50 | const statAsync = promisify(fs.stat); 51 | const stats = await statAsync('/test/fixtures/static/49chars.txt'); 52 | expect(stats.isFile()).toBe(true); 53 | expect(stats.isDirectory()).toBe(false); 54 | expect(stats.size).toBeGreaterThanOrEqual(49); 55 | expect(stats.size).toBeLessThanOrEqual(50); 56 | 57 | const backendStats = await statAsync('/src/backend'); 58 | expect(backendStats.isDirectory()).toBe(true); 59 | expect(backendStats.isFile()).toBe(false); 60 | 61 | let statError = null; 62 | try { 63 | await statAsync('/src/not-existing-name'); 64 | } catch (error) { 65 | statError = error; 66 | } 67 | expect(statError).toBeTruthy(); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/tests/OverlayFS/delete-log-test.ts: -------------------------------------------------------------------------------- 1 | import { fs } from '../../common'; 2 | import { type FileSystem, OverlayFS } from '../../../src'; 3 | import { Cred } from '../../../src/cred'; 4 | 5 | let __numWaiting: number; 6 | 7 | describe('Deletion Log', () => { 8 | let rootFS: InstanceType; 9 | let readable: FileSystem; 10 | let writable: FileSystem; 11 | const logPath = '/.deletedFiles.log'; 12 | 13 | beforeAll(() => { 14 | rootFS = fs.getMount('/') as InstanceType; 15 | const fses = rootFS.getOverlayedFileSystems(); 16 | readable = fses.readable; 17 | writable = fses.writable; 18 | }); 19 | 20 | test('Deletion Log Functionality', async () => { 21 | if (__numWaiting) { 22 | } 23 | 24 | // Back up the current log. 25 | const deletionLog = rootFS.fs.getDeletionLog(); 26 | 27 | // Delete a file in the underlay. 28 | fs.unlinkSync('/test/fixtures/files/node/a.js'); 29 | expect(fs.existsSync('/test/fixtures/files/node/a.js')).toBe(false); 30 | 31 | // Try to move the deletion log. 32 | expect(() => { 33 | fs.renameSync(logPath, logPath + '2'); 34 | }).toThrow('Should not be able to rename the deletion log.'); 35 | 36 | // Move another file over the deletion log. 37 | expect(() => { 38 | fs.renameSync('/test/fixtures/files/node/a1.js', logPath); 39 | }).toThrow('Should not be able to rename a file over the deletion log.'); 40 | 41 | // Remove the deletion log. 42 | expect(() => { 43 | fs.unlinkSync(logPath); 44 | }).toThrow('Should not be able to delete the deletion log.'); 45 | 46 | // Open the deletion log. 47 | expect(() => { 48 | fs.openSync(logPath, 'r'); 49 | }).toThrow('Should not be able to open the deletion log.'); 50 | 51 | // Re-write a.js. 52 | fs.writeFileSync('/test/fixtures/files/node/a.js', Buffer.from('hi', 'utf8')); 53 | expect(fs.existsSync('/test/fixtures/files/node/a.js')).toBe(true); 54 | 55 | // Remove something else. 56 | fs.unlinkSync('/test/fixtures/files/node/a1.js'); 57 | expect(fs.existsSync('/test/fixtures/files/node/a1.js')).toBe(false); 58 | 59 | // Wait for OverlayFS to persist delete log changes. 60 | __numWaiting++; 61 | await new Promise(resolve => { 62 | const interval = setInterval(() => { 63 | if (!(rootFS as any)._deleteLogUpdatePending) { 64 | clearInterval(interval); 65 | resolve(); 66 | } 67 | }, 4); 68 | }); 69 | 70 | // Re-mount OverlayFS. 71 | const overlayFs = await OverlayFS.Create({ writable, readable }); 72 | 73 | rootFS = overlayFs as InstanceType; 74 | fs.initialize({ '/': rootFS }); 75 | const newRoot = (rootFS as InstanceType).unwrap(); 76 | expect(fs.existsSync('/test/fixtures/files/node/a.js')).toBe(true); 77 | rootFS.fs.restoreDeletionLog('', Cred.Root); 78 | expect(fs.existsSync('/test/fixtures/files/node/a1.js')).toBe(true); 79 | // Manually restore original deletion log. 80 | rootFS.fs.restoreDeletionLog(deletionLog, Cred.Root); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/tests/all/appendFile.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import type { FileContents } from '../../../src/filesystem'; 5 | import { jest } from '@jest/globals'; 6 | import { promisify } from 'node:util'; 7 | 8 | describe.each(backends)('%s appendFile tests', (name, options) => { 9 | const configured = configure({ fs: name, options }); 10 | let tmpFile: string = path.join(tmpDir, 'append.txt'); 11 | 12 | afterEach(() => { 13 | jest.restoreAllMocks(); 14 | }); 15 | 16 | it('should create an empty file and add content', async () => { 17 | await configured; 18 | const filename = path.join(tmpFile, 'append.txt'); 19 | const content = 'Sample content'; 20 | 21 | jest.spyOn(fs, 'appendFile').mockImplementation(async (file, data, mode) => { 22 | expect(file).toBe(filename); 23 | expect(data).toBe(content); 24 | }); 25 | 26 | jest.spyOn(fs, 'readFile').mockImplementation(async (file, options) => { 27 | expect(file).toBe(filename); 28 | expect(options).toBe('utf8'); 29 | return content; 30 | }); 31 | 32 | await appendFileAndVerify(filename, content); 33 | }); 34 | 35 | it('should append data to a non-empty file', async () => { 36 | await configured; 37 | const filename = path.join(tmpFile, 'append2.txt'); 38 | const currentFileData = 'ABCD'; 39 | const content = 'Sample content'; 40 | 41 | await promisify(fs.writeFile)(filename, currentFileData); 42 | 43 | jest.spyOn(fs, 'appendFile').mockImplementation(async (file, data, mode) => { 44 | expect(file).toBe(filename); 45 | expect(data).toBe(content); 46 | }); 47 | 48 | jest.spyOn(fs, 'readFile').mockImplementation(async (file, options) => { 49 | expect(file).toBe(filename); 50 | expect(options).toBe('utf8'); 51 | return currentFileData + content; 52 | }); 53 | 54 | await appendFileAndVerify(filename, content); 55 | }); 56 | 57 | it('should append a buffer to the file', async () => { 58 | await configured; 59 | const filename = path.join(tmpFile, 'append3.txt'); 60 | const currentFileData = 'ABCD'; 61 | const content = Buffer.from('Sample content', 'utf8'); 62 | 63 | await promisify(fs.writeFile)(filename, currentFileData); 64 | 65 | jest.spyOn(fs, 'appendFile').mockImplementation(async (file, data, mode) => { 66 | expect(file).toBe(filename); 67 | expect(data).toBe(content); 68 | }); 69 | 70 | jest.spyOn(fs, 'readFile').mockImplementation(async (file, options) => { 71 | expect(file).toBe(filename); 72 | expect(options).toBe('utf8'); 73 | return currentFileData + content; 74 | }); 75 | 76 | await appendFileAndVerify(filename, content); 77 | }); 78 | 79 | // Additional tests can be added here 80 | 81 | async function appendFileAndVerify(filename: string, content: FileContents): Promise { 82 | await promisify(fs.appendFile)(filename, content); 83 | 84 | const data = await promisify(fs.readFile)(filename, 'utf8'); 85 | expect(data).toEqual(content.toString()); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /test/tests/all/chmod.test.ts: -------------------------------------------------------------------------------- 1 | import { fs, createMockStats, backends, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { jest } from '@jest/globals'; 5 | import { promisify } from 'node:util'; 6 | 7 | const isWindows = process.platform === 'win32'; 8 | 9 | describe.each(backends)('%s chmod tests', (name, options) => { 10 | const configured = configure({ fs: name, options }); 11 | 12 | afterEach(() => { 13 | jest.restoreAllMocks(); 14 | }); 15 | 16 | it('should change file mode using chmod', async () => { 17 | await configured; 18 | const file1 = path.join(fixturesDir, 'a.js'); 19 | const modeAsync = 0o777; 20 | const modeSync = 0o644; 21 | 22 | jest.spyOn(fs, 'chmod').mockImplementation(async (path, mode) => { 23 | expect(path).toBe(file1); 24 | expect(mode).toBe(modeAsync.toString(8)); 25 | }); 26 | 27 | jest.spyOn(fs, 'chmodSync').mockImplementation((path, mode) => { 28 | expect(path).toBe(file1); 29 | expect(mode).toBe(modeSync); 30 | }); 31 | 32 | jest.spyOn(fs, 'statSync').mockReturnValue(createMockStats(isWindows ? modeAsync & 0o777 : modeAsync)); 33 | 34 | await changeFileMode(file1, modeAsync, modeSync); 35 | }); 36 | 37 | it('should change file mode using fchmod', async () => { 38 | await configured; 39 | const file2 = path.join(fixturesDir, 'a1.js'); 40 | const modeAsync = 0o777; 41 | const modeSync = 0o644; 42 | 43 | jest.spyOn(fs, 'open').mockImplementation(async (path, flags, mode) => { 44 | expect(path).toBe(file2); 45 | expect(flags).toBe('a'); 46 | return 123; 47 | }); 48 | 49 | jest.spyOn(fs, 'fchmod').mockImplementation(async (fd, mode) => { 50 | expect(fd).toBe(123); 51 | expect(mode).toBe(modeAsync.toString(8)); 52 | }); 53 | 54 | jest.spyOn(fs, 'fchmodSync').mockImplementation((fd, mode) => { 55 | expect(fd).toBe(123); 56 | expect(mode).toBe(modeSync); 57 | }); 58 | 59 | jest.spyOn(fs, 'fstatSync').mockReturnValue(createMockStats(isWindows ? modeAsync & 0o777 : modeAsync)); 60 | 61 | await changeFileMode(file2, modeAsync, modeSync); 62 | }); 63 | 64 | it('should change symbolic link mode using lchmod', async () => { 65 | await configured; 66 | const link = path.join(tmpDir, 'symbolic-link'); 67 | const file2 = path.join(fixturesDir, 'a1.js'); 68 | const modeAsync = 0o777; 69 | const modeSync = 0o644; 70 | 71 | jest.spyOn(fs, 'unlinkSync').mockImplementation(path => { 72 | expect(path).toBe(link); 73 | }); 74 | 75 | jest.spyOn(fs, 'symlinkSync').mockImplementation((target, path) => { 76 | expect(target).toBe(file2); 77 | expect(path).toBe(link); 78 | }); 79 | 80 | jest.spyOn(fs, 'lchmod').mockImplementation(async (path, mode) => { 81 | expect(path).toBe(link); 82 | expect(mode).toBe(modeAsync); 83 | }); 84 | 85 | jest.spyOn(fs, 'lchmodSync').mockImplementation((path, mode) => { 86 | expect(path).toBe(link); 87 | expect(mode).toBe(modeSync); 88 | }); 89 | 90 | jest.spyOn(fs, 'lstatSync').mockReturnValue(createMockStats(isWindows ? modeAsync & 0o777 : modeAsync)); 91 | 92 | await changeSymbolicLinkMode(link, file2, modeAsync, modeSync); 93 | }); 94 | }); 95 | 96 | async function changeFileMode(file: string, modeAsync: number, modeSync: number): Promise { 97 | await promisify(fs.chmod)(file, modeAsync.toString(8)); 98 | 99 | const statAsync = promisify(fs.stat); 100 | const statResult = await statAsync(file); 101 | expect(statResult.mode & 0o777).toBe(isWindows ? modeAsync & 0o777 : modeAsync); 102 | 103 | fs.chmodSync(file, modeSync); 104 | const statSyncResult = fs.statSync(file); 105 | expect(statSyncResult.mode & 0o777).toBe(isWindows ? modeSync & 0o777 : modeSync); 106 | } 107 | 108 | async function changeSymbolicLinkMode(link: string, target: string, modeAsync: number, modeSync: number): Promise { 109 | await promisify(fs.unlink)(link); 110 | await promisify(fs.symlink)(target, link); 111 | 112 | await promisify(fs.lchmod)(link, modeAsync); 113 | const lstatAsync = promisify(fs.lstat); 114 | const lstatResult = await lstatAsync(link); 115 | expect(lstatResult.mode & 0o777).toBe(isWindows ? modeAsync & 0o777 : modeAsync); 116 | 117 | fs.lchmodSync(link, modeSync); 118 | const lstatSyncResult = fs.lstatSync(link); 119 | expect(lstatSyncResult.mode & 0o777).toBe(isWindows ? modeSync & 0o777 : modeSync); 120 | } 121 | -------------------------------------------------------------------------------- /test/tests/all/error-messages.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'util'; 5 | import type { ApiError } from '../../../src/ApiError'; 6 | 7 | const existingFile = path.join(fixturesDir, 'exit.js'); 8 | 9 | const expectAsyncError = async (fn, p: string, ...args) => { 10 | let error: ApiError; 11 | try { 12 | await promisify(fn)(p, ...args); 13 | } catch (err) { 14 | error = err; 15 | } 16 | expect(error).toBeDefined(); 17 | expect(error.path).toBe(p); 18 | expect(error.message).toContain(p); 19 | return error; 20 | }; 21 | 22 | const expectSyncError = (fn, p: string, ...args) => { 23 | let error: ApiError; 24 | try { 25 | fn(p, ...args); 26 | } catch (err) { 27 | error = err; 28 | } 29 | expect(error).toBeDefined(); 30 | expect(error.path).toBe(p); 31 | expect(error.message).toContain(p); 32 | return error; 33 | }; 34 | 35 | describe.each(backends)('%s File System Tests', (name, options) => { 36 | const configured = configure({ fs: name, options }); 37 | 38 | it('should handle async operations with error', async () => { 39 | await configured; 40 | const fn = path.join(fixturesDir, 'non-existent'); 41 | 42 | await expectAsyncError(fs.stat, fn); 43 | 44 | if (!fs.getMount('/').metadata.readonly) { 45 | await expectAsyncError(fs.mkdir, existingFile, 0o666); 46 | await expectAsyncError(fs.rmdir, fn); 47 | await expectAsyncError(fs.rmdir, existingFile); 48 | await expectAsyncError(fs.rename, fn, 'foo'); 49 | await expectAsyncError(fs.open, fn, 'r'); 50 | await expectAsyncError(fs.readdir, fn); 51 | await expectAsyncError(fs.unlink, fn); 52 | 53 | if (fs.getMount('/').metadata.supportsLinks) { 54 | await expectAsyncError(fs.link, fn, 'foo'); 55 | } 56 | 57 | if (fs.getMount('/').metadata.supportsProperties) { 58 | await expectAsyncError(fs.chmod, fn, 0o666); 59 | } 60 | } 61 | 62 | if (fs.getMount('/').metadata.supportsLinks) { 63 | await expectAsyncError(fs.lstat, fn); 64 | await expectAsyncError(fs.readlink, fn); 65 | } 66 | }); 67 | 68 | // Sync operations 69 | if (fs.getMount('/').metadata.synchronous) { 70 | it('should handle sync operations with error', async () => { 71 | await configured; 72 | const fn = path.join(fixturesDir, 'non-existent'); 73 | const existingFile = path.join(fixturesDir, 'exit.js'); 74 | 75 | expectSyncError(fs.statSync, fn); 76 | 77 | if (!fs.getMount('/').metadata.readonly) { 78 | expectSyncError(fs.mkdirSync, existingFile, 0o666); 79 | expectSyncError(fs.rmdirSync, fn); 80 | expectSyncError(fs.rmdirSync, existingFile); 81 | expectSyncError(fs.renameSync, fn, 'foo'); 82 | expectSyncError(fs.openSync, fn, 'r'); 83 | expectSyncError(fs.readdirSync, fn); 84 | expectSyncError(fs.unlinkSync, fn); 85 | 86 | if (fs.getMount('/').metadata.supportsProperties) { 87 | expectSyncError(fs.chmodSync, fn, 0o666); 88 | } 89 | 90 | if (fs.getMount('/').metadata.supportsLinks) { 91 | expectSyncError(fs.linkSync, fn, 'foo'); 92 | } 93 | } 94 | 95 | if (fs.getMount('/').metadata.supportsLinks) { 96 | expectSyncError(fs.lstatSync, fn); 97 | expectSyncError(fs.readlinkSync, fn); 98 | } 99 | }); 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /test/tests/all/exists.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | describe.each(backends)('%s fs.exists', (name, options) => { 5 | const configured = configure({ fs: name, options }); 6 | let exists: boolean; 7 | let doesNotExist: boolean; 8 | const f = path.join(fixturesDir, 'x.txt'); 9 | 10 | beforeAll(() => { 11 | return new Promise(resolve => { 12 | fs.exists(f, y => { 13 | exists = y; 14 | resolve(); 15 | }); 16 | }); 17 | }); 18 | 19 | beforeAll(() => { 20 | return new Promise(resolve => { 21 | fs.exists(f + '-NO', y => { 22 | doesNotExist = y; 23 | resolve(); 24 | }); 25 | }); 26 | }); 27 | 28 | it('should return true for an existing file', async () => { 29 | await configured; 30 | expect(exists).toBe(true); 31 | }); 32 | 33 | it('should return false for a non-existent file', async () => { 34 | await configured; 35 | expect(doesNotExist).toBe(false); 36 | }); 37 | 38 | it('should have sync methods that behave the same', async () => { 39 | await configured; 40 | if (fs.getMount('/').metadata.synchronous) { 41 | expect(fs.existsSync(f)).toBe(true); 42 | expect(fs.existsSync(f + '-NO')).toBe(false); 43 | } 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/tests/all/fsync.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure } from '../../common'; 2 | import * as path from 'path'; 3 | import { promisify } from 'node:util'; // Import promisify 4 | import { tmpDir, fixturesDir } from '../../common'; 5 | 6 | describe.each(backends)('%s fs.fileSync', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | const file = path.join(fixturesDir, 'a.js'); 9 | const rootFS = fs.getMount('/'); 10 | 11 | if (!fs.getMount('/').metadata.readonly) { 12 | let fd: number; 13 | let successes = 0; 14 | 15 | beforeAll(async () => { 16 | // Promisify the fs.open function 17 | fd = await promisify(fs.open)(file, 'a', 0o777); 18 | }); 19 | 20 | if (fs.getMount('/').metadata.synchronous) { 21 | it('should synchronize file data changes (sync)', async () => { 22 | await configured; 23 | fs.fdatasyncSync(fd); 24 | successes++; 25 | fs.fsyncSync(fd); 26 | successes++; 27 | }); 28 | } 29 | 30 | it('should synchronize file data changes (async)', async () => { 31 | await configured; 32 | // Promisify the fs.fdatasync and fs.fsync functions 33 | const fdatasyncAsync = promisify(fs.fdatasync); 34 | const fsyncAsync = promisify(fs.fsync); 35 | 36 | await fdatasyncAsync(fd); 37 | successes++; 38 | await fsyncAsync(fd); 39 | successes++; 40 | }); 41 | 42 | afterAll(async () => { 43 | // Promisify the fs.close function 44 | const closeAsync = promisify(fs.close); 45 | await closeAsync(fd); 46 | }); 47 | 48 | it('should have correct number of successes', async () => { 49 | await configured; 50 | if (fs.getMount('/').metadata.synchronous) { 51 | expect(successes).toBe(4); 52 | } else { 53 | expect(successes).toBe(2); 54 | } 55 | }); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /test/tests/all/long-path.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | 6 | describe.each(backends)('%s fs.writeFile', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | if (!fs.getMount('/').metadata.readonly) { 9 | const fileNameLen = Math.max(260 - tmpDir.length - 1, 1); 10 | const fileName = path.join(tmpDir, new Array(fileNameLen + 1).join('x')); 11 | const fullPath = path.resolve(fileName); 12 | 13 | it('should write file and verify its size', async () => { 14 | await configured; 15 | await promisify(fs.writeFile)(fullPath, 'ok'); 16 | const stats = await promisify(fs.stat)(fullPath); 17 | expect(stats.size).toBe(2); 18 | }); 19 | 20 | afterAll(async () => { 21 | await promisify(fs.unlink)(fullPath); 22 | }); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /test/tests/all/mkdir.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure } from '../../common'; 2 | import { tmpDir, fixturesDir } from '../../common'; 3 | import { promisify } from 'node:util'; 4 | 5 | describe.each(backends)('%s fs.mkdir', (name, options) => { 6 | const configured = configure({ fs: name, options }); 7 | 8 | if (!fs.getMount('/').metadata.readonly) { 9 | const pathname1 = tmpDir + '/mkdir-test1'; 10 | 11 | it('should create a directory and verify its existence', async () => { 12 | await configured; 13 | 14 | await promisify(fs.mkdir)(pathname1); 15 | const exists = await promisify(fs.exists)(pathname1); 16 | expect(exists).toBe(true); 17 | }); 18 | 19 | const pathname2 = tmpDir + '/mkdir-test2'; 20 | 21 | it('should create a directory with custom permissions and verify its existence', async () => { 22 | await configured; 23 | 24 | await promisify(fs.mkdir)(pathname2, 0o777); 25 | const exists = await promisify(fs.exists)(pathname2); 26 | expect(exists).toBe(true); 27 | }); 28 | 29 | const pathname3 = tmpDir + '/mkdir-test3/again'; 30 | 31 | it('should not be able to create multi-level directories', async () => { 32 | await configured; 33 | 34 | try { 35 | await promisify(fs.mkdir)(pathname3, 0o777); 36 | } catch (err) { 37 | expect(err).not.toBeNull(); 38 | } 39 | }); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /test/tests/all/mode.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure } from '../../common'; 2 | import * as path from 'path'; 3 | import { promisify } from 'node:util'; 4 | 5 | describe.each(backends)('%s PermissionsTest', (name, options) => { 6 | const configured = configure({ fs: name, options }); 7 | const testFileContents = Buffer.from('this is a test file, plz ignore.'); 8 | 9 | function is_writable(mode: number) { 10 | return (mode & 146) > 0; 11 | } 12 | 13 | function is_readable(mode: number) { 14 | return (mode & 0x124) > 0; 15 | } 16 | 17 | function is_executable(mode: number) { 18 | return (mode & 0x49) > 0; 19 | } 20 | 21 | async function process_file(p: string, fileMode: number): Promise { 22 | const readFileAsync = promisify(fs.readFile); 23 | const openAsync = promisify(fs.open); 24 | const closeAsync = promisify(fs.close); 25 | 26 | try { 27 | const data = await readFileAsync(p); 28 | // Invariant 2: We can only read a file if we have read permissions on the file. 29 | expect(is_readable(fileMode)).toBe(true); 30 | } catch (err) { 31 | if (err.code === 'EPERM') { 32 | // Invariant 2: We can only read a file if we have read permissions on the file. 33 | expect(is_readable(fileMode)).toBe(false); 34 | } else { 35 | throw err; 36 | } 37 | } 38 | 39 | try { 40 | const fd = await openAsync(p, 'a'); 41 | // Invariant 3: We can only write to a file if we have write permissions on the file. 42 | expect(is_writable(fileMode)).toBe(true); 43 | await closeAsync(fd); 44 | } catch (err) { 45 | if (err.code === 'EPERM') { 46 | // Invariant 3: We can only write to a file if we have write permissions on the file. 47 | expect(is_writable(fileMode)).toBe(false); 48 | } else { 49 | throw err; 50 | } 51 | } 52 | } 53 | 54 | async function process_directory(p: string, dirMode: number): Promise { 55 | const readdirAsync = promisify(fs.readdir); 56 | const writeFileAsync = promisify(fs.writeFile); 57 | const unlinkAsync = promisify(fs.unlink); 58 | 59 | try { 60 | const dirs = await readdirAsync(p); 61 | // Invariant 2: We can only readdir if we have read permissions on the directory. 62 | expect(is_readable(dirMode)).toBe(true); 63 | 64 | const promises = dirs.map(async dir => { 65 | const itemPath = path.resolve(p, dir); 66 | await process_item(itemPath, dirMode); 67 | }); 68 | 69 | await Promise.all(promises); 70 | 71 | // Try to write a file into the directory. 72 | const testFile = path.resolve(p, '__test_file_plz_ignore.txt'); 73 | await writeFileAsync(testFile, testFileContents); 74 | // Clean up. 75 | await unlinkAsync(testFile); 76 | } catch (err) { 77 | if (err.code === 'EPERM') { 78 | // Invariant 2: We can only readdir if we have read permissions on the directory. 79 | expect(is_readable(dirMode)).toBe(false); 80 | // Invariant 3: We can only write to a new file if we have write permissions in the directory. 81 | expect(is_writable(dirMode)).toBe(false); 82 | } else { 83 | throw err; 84 | } 85 | } 86 | } 87 | 88 | async function process_item(p: string, parentMode: number): Promise { 89 | const statAsync = promisify(fs.stat); 90 | 91 | const isReadOnly = fs.getMount('/').metadata.readonly; 92 | 93 | try { 94 | const stat = await statAsync(p); 95 | // Invariant 4: Ensure we have execute permissions on parent directory. 96 | expect(is_executable(parentMode)).toBe(true); 97 | 98 | if (isReadOnly) { 99 | // Invariant 1: RO FS do not support write permissions. 100 | expect(is_writable(stat.mode)).toBe(false); 101 | } 102 | 103 | // Invariant 4: Ensure we have execute permissions on parent directory. 104 | expect(is_executable(parentMode)).toBe(true); 105 | 106 | if (stat.isDirectory()) { 107 | await process_directory(p, stat.mode); 108 | } else { 109 | await process_file(p, stat.mode); 110 | } 111 | } catch (err) { 112 | if (err.code === 'EPERM') { 113 | // Invariant 4: Ensure we do not have execute permissions on parent directory. 114 | expect(is_executable(parentMode)).toBe(false); 115 | } else { 116 | throw err; 117 | } 118 | } 119 | } 120 | 121 | it('should satisfy the permissions invariants', async () => { 122 | await configured; 123 | await process_item('/', 0o777); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/tests/all/null-bytes.test.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'node:util'; 2 | import { backends, fs, configure } from '../../common'; 3 | 4 | describe.each(backends)('%s fs path validation', (name, options) => { 5 | const configured = configure({ fs: name, options }); 6 | 7 | function check(asyncFn: Function, syncFn: Function, ...args: any[]): void { 8 | const expected = /Path must be a string without null bytes./; 9 | 10 | if (fs.getMount('/').metadata.synchronous && syncFn) { 11 | it(`${asyncFn.name} should throw an error for invalid path`, async () => { 12 | await configured; 13 | expect(() => { 14 | syncFn(...args); 15 | }).toThrow(expected); 16 | }); 17 | } 18 | 19 | if (asyncFn) { 20 | it(`${syncFn.name} should throw an error for invalid path`, async () => { 21 | await configured; 22 | expect(await promisify(asyncFn)(...args)).toThrow(expected); 23 | }); 24 | } 25 | } 26 | 27 | check(fs.appendFile, fs.appendFileSync, 'foo\u0000bar'); 28 | check(fs.lstat, fs.lstatSync, 'foo\u0000bar'); 29 | check(fs.mkdir, fs.mkdirSync, 'foo\u0000bar', '0755'); 30 | check(fs.open, fs.openSync, 'foo\u0000bar', 'r'); 31 | check(fs.readFile, fs.readFileSync, 'foo\u0000bar'); 32 | check(fs.readdir, fs.readdirSync, 'foo\u0000bar'); 33 | check(fs.realpath, fs.realpathSync, 'foo\u0000bar'); 34 | check(fs.rename, fs.renameSync, 'foo\u0000bar', 'foobar'); 35 | check(fs.rename, fs.renameSync, 'foobar', 'foo\u0000bar'); 36 | check(fs.rmdir, fs.rmdirSync, 'foo\u0000bar'); 37 | check(fs.stat, fs.statSync, 'foo\u0000bar'); 38 | check(fs.truncate, fs.truncateSync, 'foo\u0000bar'); 39 | check(fs.unlink, fs.unlinkSync, 'foo\u0000bar'); 40 | check(fs.writeFile, fs.writeFileSync, 'foo\u0000bar'); 41 | 42 | if (fs.getMount('/').metadata.supportsLinks) { 43 | check(fs.link, fs.linkSync, 'foo\u0000bar', 'foobar'); 44 | check(fs.link, fs.linkSync, 'foobar', 'foo\u0000bar'); 45 | check(fs.readlink, fs.readlinkSync, 'foo\u0000bar'); 46 | check(fs.symlink, fs.symlinkSync, 'foo\u0000bar', 'foobar'); 47 | check(fs.symlink, fs.symlinkSync, 'foobar', 'foo\u0000bar'); 48 | } 49 | 50 | if (fs.getMount('/').metadata.supportsProperties) { 51 | check(fs.chmod, fs.chmodSync, 'foo\u0000bar', '0644'); 52 | check(fs.chown, fs.chownSync, 'foo\u0000bar', 12, 34); 53 | check(fs.utimes, fs.utimesSync, 'foo\u0000bar', 0, 0); 54 | } 55 | 56 | it('should return false for non-existing path', async () => { 57 | await configured; 58 | await expect(await promisify(fs.exists)('foo\u0000bar')).toEqual(false); 59 | }); 60 | 61 | it('should return false for non-existing path (sync)', async () => { 62 | await configured; 63 | if (!fs.getMount('/').metadata.synchronous) { 64 | return; 65 | } 66 | expect(fs.existsSync('foo\u0000bar')).toBeFalsy(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/tests/all/open.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | 6 | describe.each(backends)('%s fs file opening', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | const filename = path.join(fixturesDir, 'a.js'); 9 | 10 | it('should throw ENOENT when opening non-existent file (sync)', async () => { 11 | await configured; 12 | 13 | if (fs.getMount('/').metadata.synchronous) { 14 | let caughtException = false; 15 | try { 16 | fs.openSync('/path/to/file/that/does/not/exist', 'r'); 17 | } catch (e) { 18 | expect(e?.code).toBe('ENOENT'); 19 | caughtException = true; 20 | } 21 | expect(caughtException).toBeTruthy(); 22 | } 23 | }); 24 | 25 | it('should throw ENOENT when opening non-existent file (async)', async () => { 26 | await configured; 27 | try { 28 | await promisify(fs.open)('/path/to/file/that/does/not/exist', 'r'); 29 | } catch (e) { 30 | expect(e?.code).toBe('ENOENT'); 31 | } 32 | }); 33 | 34 | it('should open file with mode "r"', async () => { 35 | await configured; 36 | const fd = await promisify(fs.open)(filename, 'r'); 37 | expect(fd).toBeGreaterThanOrEqual(-Infinity); 38 | }); 39 | 40 | it('should open file with mode "rs"', async () => { 41 | await configured; 42 | const fd = await promisify(fs.open)(filename, 'rs'); 43 | expect(fd).toBeGreaterThanOrEqual(-Infinity); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/tests/all/read.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | 6 | describe.each(backends)('%s read', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | let filepath: string; 9 | let expected: string; 10 | let rootFS: any; 11 | 12 | beforeEach(() => { 13 | filepath = path.join(fixturesDir, 'x.txt'); 14 | expected = 'xyz\n'; 15 | rootFS = fs.getMount('/'); 16 | }); 17 | 18 | it('should read file asynchronously', async () => { 19 | await configured; 20 | const fd = await promisify(fs.open)(filepath, 'r'); 21 | const buffer = Buffer.alloc(expected.length); 22 | const bytesRead = await promisify(fs.read)(fd, buffer, 0, expected.length, 0); 23 | 24 | expect(buffer.toString()).toEqual(expected); 25 | expect(bytesRead).toEqual(expected.length); 26 | }); 27 | 28 | if (fs.getMount('/').metadata.synchronous) { 29 | it('should read file synchronously', async () => { 30 | await configured; 31 | const fd = fs.openSync(filepath, 'r'); 32 | const buffer = Buffer.alloc(expected.length); 33 | const bytesRead = await promisify(fs.readSync)(fd, buffer, 0, expected.length, 0); 34 | 35 | expect(buffer.toString()).toEqual(expected); 36 | expect(bytesRead).toEqual(expected.length); 37 | }); 38 | } 39 | }); 40 | 41 | describe.each(backends)('%s read binary', (name, options) => { 42 | const configured = configure({ fs: name, options }); 43 | 44 | it('Read a file and check its binary bytes (asynchronous)', async () => { 45 | await configured; 46 | const buff = await promisify(fs.readFile)(path.join(fixturesDir, 'elipses.txt')); 47 | expect(buff.readUInt16LE(0)).toBe(32994); 48 | }); 49 | 50 | it('Read a file and check its binary bytes (synchronous)', () => { 51 | if (fs.getMount('/').metadata.synchronous) { 52 | const buff = fs.readFileSync(path.join(fixturesDir, 'elipses.txt')); 53 | expect(buff.readUInt16LE(0)).toBe(32994); 54 | } 55 | }); 56 | }); 57 | 58 | describe.each(backends)('%s read buffer', (name, options) => { 59 | const configured = configure({ fs: name, options }); 60 | const filepath = path.join(fixturesDir, 'x.txt'); 61 | const expected = 'xyz\n'; 62 | const bufferAsync = Buffer.alloc(expected.length); 63 | const bufferSync = Buffer.alloc(expected.length); 64 | 65 | it('should read file asynchronously', async () => { 66 | await configured; 67 | const fd = await promisify(fs.open)(filepath, 'r'); 68 | const bytesRead = await promisify(fs.read)(fd, bufferAsync, 0, expected.length, 0); 69 | 70 | expect(bytesRead).toBe(expected.length); 71 | expect(bufferAsync.toString()).toBe(expected); 72 | }); 73 | 74 | it('should read file synchronously', async () => { 75 | await configured; 76 | if (fs.getMount('/').metadata.synchronous) { 77 | const fd = fs.openSync(filepath, 'r'); 78 | const bytesRead = fs.readSync(fd, bufferSync, 0, expected.length, 0); 79 | 80 | expect(bufferSync.toString()).toBe(expected); 81 | expect(bytesRead).toBe(expected.length); 82 | } 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/tests/all/readFile.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | import { promisify } from 'node:util'; 4 | 5 | describe.each(backends)('%s File Reading', (name, options) => { 6 | const configured = configure({ fs: name, options }); 7 | it('Cannot read a file with an invalid encoding (synchronous)', async () => { 8 | await configured; 9 | 10 | let wasThrown = false; 11 | if (!fs.getMount('/').metadata.synchronous) { 12 | return; 13 | } 14 | 15 | try { 16 | fs.readFileSync(path.join(fixturesDir, 'a.js'), 'wrongencoding'); 17 | } catch (e) { 18 | wasThrown = true; 19 | } 20 | expect(wasThrown).toBeTruthy(); 21 | }); 22 | 23 | it('Cannot read a file with an invalid encoding (asynchronous)', async () => { 24 | await configured; 25 | expect(await promisify(fs.readFile)(path.join(fixturesDir, 'a.js'), 'wrongencoding')).toThrow(); 26 | }); 27 | 28 | it('Reading past the end of a file should not be an error', async () => { 29 | await configured; 30 | const fd = await promisify(fs.open)(path.join(fixturesDir, 'a.js'), 'r'); 31 | const buffData = Buffer.alloc(10); 32 | const bytesRead = await promisify(fs.read)(fd, buffData, 0, 10, 10000); 33 | expect(bytesRead).toBe(0); 34 | }); 35 | }); 36 | 37 | describe.each(backends)('%s Read and Unlink File Test', (name, options) => { 38 | const configured = configure({ fs: name, options }); 39 | const dirName = path.resolve(fixturesDir, 'test-readfile-unlink'); 40 | const fileName = path.resolve(dirName, 'test.bin'); 41 | 42 | const buf = Buffer.alloc(512); 43 | buf.fill(42); 44 | 45 | beforeAll(async () => { 46 | await configured; 47 | await promisify(fs.mkdir)(dirName); 48 | await promisify(fs.writeFile)(fileName, buf); 49 | }); 50 | 51 | it('should read file and verify its content', async () => { 52 | await configured; 53 | if (fs.getMount('/').metadata.readonly) { 54 | return; 55 | } 56 | const data: Buffer = await promisify(fs.readFile)(fileName); 57 | expect(data.length).toBe(buf.length); 58 | expect(data[0]).toBe(42); 59 | }); 60 | 61 | it('should unlink file and remove directory', async () => { 62 | await configured; 63 | if (fs.getMount('/').metadata.readonly) { 64 | return; 65 | } 66 | await promisify(fs.unlink)(fileName); 67 | await promisify(fs.rmdir)(dirName); 68 | }); 69 | }); 70 | 71 | describe.each(backends)('%s Read File Test', (name, options) => { 72 | const configured = configure({ fs: name, options }); 73 | const fn = path.join(fixturesDir, 'empty.txt'); 74 | 75 | it('should read file asynchronously', async () => { 76 | await configured; 77 | const data: Buffer = await promisify(fs.readFile)(fn); 78 | expect(data).toBeDefined(); 79 | }); 80 | 81 | it('should read file with utf-8 encoding asynchronously', async () => { 82 | await configured; 83 | const data: string = await promisify(fs.readFile)(fn, 'utf8'); 84 | expect(data).toBe(''); 85 | }); 86 | 87 | if (fs.getMount('/').metadata.synchronous) { 88 | it('should read file synchronously', async () => { 89 | await configured; 90 | const data: Buffer = fs.readFileSync(fn); 91 | expect(data).toBeDefined(); 92 | }); 93 | 94 | it('should read file with utf-8 encoding synchronously', async () => { 95 | await configured; 96 | const data: string = fs.readFileSync(fn, 'utf8'); 97 | expect(data).toBe(''); 98 | }); 99 | } 100 | }); 101 | -------------------------------------------------------------------------------- /test/tests/all/readFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, fixturesDir } from '../../common'; 2 | import path from 'path'; 3 | 4 | describe.each(backends)('%s fs file reading', (name, options) => { 5 | const configured = configure({ fs: name, options }); 6 | 7 | const filepath = path.join(fixturesDir, 'elipses.txt'); 8 | 9 | it('should read file synchronously and verify the content', async () => { 10 | await configured; 11 | if (!fs.getMount('/').metadata.synchronous) { 12 | return; 13 | } 14 | const content = fs.readFileSync(filepath, 'utf8'); 15 | 16 | for (let i = 0; i < content.length; i++) { 17 | expect(content[i]).toBe('\u2026'); 18 | } 19 | 20 | expect(content.length).toBe(10000); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/tests/all/readdir.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | 6 | describe.each(backends)('%s Directory Reading', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | 9 | it('Cannot call readdir on a file (synchronous)', () => { 10 | let wasThrown = false; 11 | if (fs.getMount('/').metadata.synchronous) { 12 | try { 13 | fs.readdirSync(path.join(fixturesDir, 'a.js')); 14 | } catch (e) { 15 | wasThrown = true; 16 | expect(e.code).toBe('ENOTDIR'); 17 | } 18 | expect(wasThrown).toBeTruthy(); 19 | } 20 | }); 21 | 22 | it('Cannot call readdir on a non-existent directory (synchronous)', () => { 23 | let wasThrown = false; 24 | if (fs.getMount('/').metadata.synchronous) { 25 | try { 26 | fs.readdirSync('/does/not/exist'); 27 | } catch (e) { 28 | wasThrown = true; 29 | expect(e.code).toBe('ENOENT'); 30 | } 31 | expect(wasThrown).toBeTruthy(); 32 | } 33 | }); 34 | 35 | it('Cannot call readdir on a file (asynchronous)', async () => { 36 | await configured; 37 | try { 38 | await promisify(fs.readdir)(path.join(fixturesDir, 'a.js')); 39 | } catch (err) { 40 | expect(err).toBeTruthy(); 41 | expect(err.code).toBe('ENOTDIR'); 42 | } 43 | }); 44 | 45 | it('Cannot call readdir on a non-existent directory (asynchronous)', async () => { 46 | await configured; 47 | try { 48 | await promisify(fs.readdir)('/does/not/exist'); 49 | } catch (err) { 50 | expect(err).toBeTruthy(); 51 | expect(err.code).toBe('ENOENT'); 52 | } 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/tests/all/rename.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure } from '../../common'; 2 | import * as path from 'path'; 3 | import { promisify } from 'node:util'; 4 | 5 | describe.each(backends)('%s File and Directory Rename Tests', (name, options) => { 6 | const configured = configure({ fs: name, options }); 7 | 8 | /** 9 | * Creates the following directory structure within the given dir: 10 | * - _rename_me 11 | * - lol.txt 12 | * - file.dat 13 | */ 14 | async function populate_directory(dir) { 15 | const dir1 = path.resolve(dir, '_rename_me'); 16 | const file1 = path.resolve(dir, 'file.dat'); 17 | const file2 = path.resolve(dir1, 'lol.txt'); 18 | 19 | await promisify(fs.mkdir)(dir1); 20 | await promisify(fs.writeFile)(file1, Buffer.from('filedata')); 21 | await promisify(fs.writeFile)(file2, Buffer.from('lololol')); 22 | } 23 | 24 | /** 25 | * Check that the directory structure created in populate_directory remains. 26 | */ 27 | async function check_directory(dir) { 28 | const dir1 = path.resolve(dir, '_rename_me'); 29 | const file1 = path.resolve(dir, 'file.dat'); 30 | const file2 = path.resolve(dir1, 'lol.txt'); 31 | 32 | const contents = await promisify(fs.readdir)(dir); 33 | expect(contents.length).toBe(2); 34 | 35 | const contentsDir1 = await promisify(fs.readdir)(dir1); 36 | expect(contentsDir1.length).toBe(1); 37 | 38 | const existsFile1 = await promisify(fs.exists)(file1); 39 | expect(existsFile1).toBe(true); 40 | 41 | const existsFile2 = await promisify(fs.exists)(file2); 42 | expect(existsFile2).toBe(true); 43 | } 44 | 45 | it('Directory Rename', async () => { 46 | await configured; 47 | if (fs.getMount('/').metadata.readonly) { 48 | return; 49 | } 50 | const oldDir = '/rename_test'; 51 | const newDir = '/rename_test2'; 52 | 53 | await promisify(fs.mkdir)(oldDir); 54 | 55 | await populate_directory(oldDir); 56 | 57 | await promisify(fs.rename)(oldDir, oldDir); 58 | 59 | await check_directory(oldDir); 60 | 61 | await promisify(fs.mkdir)(newDir); 62 | await promisify(fs.rmdir)(newDir); 63 | await promisify(fs.rename)(oldDir, newDir); 64 | 65 | await check_directory(newDir); 66 | 67 | const exists = await promisify(fs.exists)(oldDir); 68 | expect(exists).toBe(false); 69 | 70 | await promisify(fs.mkdir)(oldDir); 71 | await populate_directory(oldDir); 72 | await promisify(fs.rename)(oldDir, path.resolve(newDir, 'newDir')); 73 | }); 74 | 75 | it('File Rename', async () => { 76 | await configured; 77 | if (fs.getMount('/').metadata.readonly) { 78 | return; 79 | } 80 | const fileDir = '/rename_file_test'; 81 | const file1 = path.resolve(fileDir, 'fun.js'); 82 | const file2 = path.resolve(fileDir, 'fun2.js'); 83 | 84 | await promisify(fs.mkdir)(fileDir); 85 | await promisify(fs.writeFile)(file1, Buffer.from('while(1) alert("Hey! Listen!");')); 86 | await promisify(fs.rename)(file1, file1); 87 | await promisify(fs.rename)(file1, file2); 88 | 89 | await promisify(fs.writeFile)(file1, Buffer.from('hey')); 90 | await promisify(fs.rename)(file1, file2); 91 | 92 | const contents = await promisify(fs.readFile)(file2); 93 | expect(contents.toString()).toBe('hey'); 94 | 95 | const exists = await promisify(fs.exists)(file1); 96 | expect(exists).toBe(false); 97 | }); 98 | 99 | it('File to Directory and Directory to File Rename', async () => { 100 | await configured; 101 | if (fs.getMount('/').metadata.readonly) { 102 | return; 103 | } 104 | const dir = '/rename_filedir_test'; 105 | const file = '/rename_filedir_test.txt'; 106 | 107 | await promisify(fs.mkdir)(dir); 108 | await promisify(fs.writeFile)(file, Buffer.from('file contents go here')); 109 | 110 | try { 111 | await promisify(fs.rename)(file, dir); 112 | } catch (e) { 113 | // Some *native* file systems throw EISDIR, others throw EPERM.... accept both. 114 | expect(e.code === 'EISDIR' || e.code === 'EPERM').toBe(true); 115 | } 116 | 117 | // JV: Removing test for now. I noticed that you can do that in Node v0.12 on Mac, 118 | // but it might be FS independent. 119 | /*fs.rename(dir, file, function (e) { 120 | if (e == null) { 121 | throw new Error("Failed invariant: Cannot rename a directory over a file."); 122 | } else { 123 | assert(e.code === 'ENOTDIR'); 124 | } 125 | });*/ 126 | }); 127 | 128 | it('Cannot Rename a Directory Inside Itself', async () => { 129 | await configured; 130 | if (fs.getMount('/').metadata.readonly) { 131 | return; 132 | } 133 | const renDir1 = '/renamedir_1'; 134 | const renDir2 = '/renamedir_1/lol'; 135 | 136 | await promisify(fs.mkdir)(renDir1); 137 | 138 | try { 139 | await promisify(fs.rename)(renDir1, renDir2); 140 | } catch (e) { 141 | expect(e.code).toBe('EBUSY'); 142 | } 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/tests/all/rmdir.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure } from '../../common'; 2 | import { promisify } from 'node:util'; 3 | 4 | describe.each(backends)('%s Directory Removal', (name, options) => { 5 | const configured = configure({ fs: name, options }); 6 | 7 | it('Cannot remove non-empty directories', async () => { 8 | await configured; 9 | 10 | await promisify(fs.mkdir)('/rmdirTest'); 11 | await promisify(fs.mkdir)('/rmdirTest/rmdirTest2'); 12 | 13 | try { 14 | await promisify(fs.rmdir)('/rmdirTest'); 15 | } catch (err) { 16 | expect(err).not.toBeNull(); 17 | expect(err.code).toBe('ENOTEMPTY'); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/tests/all/stat.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | import { promisify } from 'node:util'; 4 | 5 | describe.each(backends)('%s File Stat Test', (name, options) => { 6 | const configured = configure({ fs: name, options }); 7 | const existing_dir = fixturesDir; 8 | const existing_file = path.join(fixturesDir, 'x.txt'); 9 | 10 | it('should handle empty file path', async () => { 11 | await configured; 12 | try { 13 | await promisify(fs.stat)(''); 14 | } catch (err) { 15 | expect(err).toBeTruthy(); 16 | } 17 | }); 18 | 19 | it('should stat existing directory', async () => { 20 | await configured; 21 | const stats = await promisify(fs.stat)(existing_dir); 22 | expect(stats.mtime).toBeInstanceOf(Date); 23 | }); 24 | 25 | it('should lstat existing directory', async () => { 26 | await configured; 27 | const stats = await promisify(fs.lstat)(existing_dir); 28 | expect(stats.mtime).toBeInstanceOf(Date); 29 | }); 30 | 31 | it('should fstat existing file', async () => { 32 | await configured; 33 | const fd = await promisify(fs.open)(existing_file, 'r'); 34 | expect(fd).toBeTruthy(); 35 | 36 | const stats = await promisify(fs.fstat)(fd); 37 | expect(stats.mtime).toBeInstanceOf(Date); 38 | await promisify(fs.close)(fd); 39 | }); 40 | 41 | if (fs.getMount('/').metadata.synchronous) { 42 | it('should fstatSync existing file', async () => { 43 | await configured; 44 | const fd = await promisify(fs.open)(existing_file, 'r'); 45 | const stats = fs.fstatSync(fd); 46 | expect(stats.mtime).toBeInstanceOf(Date); 47 | await promisify(fs.close)(fd); 48 | }); 49 | } 50 | 51 | it('should stat existing file', async () => { 52 | await configured; 53 | const s = await promisify(fs.stat)(existing_file); 54 | expect(s.isDirectory()).toBe(false); 55 | expect(s.isFile()).toBe(true); 56 | expect(s.isSocket()).toBe(false); 57 | //expect(s.isBlockDevice()).toBe(false); 58 | expect(s.isCharacterDevice()).toBe(false); 59 | expect(s.isFIFO()).toBe(false); 60 | expect(s.isSymbolicLink()).toBe(false); 61 | expect(s.mtime).toBeInstanceOf(Date); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/tests/all/symlink.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | 6 | describe.each(backends)('%s Link and Symlink Test', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | const readFileAsync = promisify(fs.readFile); 9 | 10 | it('should create and read symbolic link', async () => { 11 | await configured; 12 | if (fs.getMount('/').metadata.supportsLinks) { 13 | const linkData = path.join(fixturesDir, '/cycles/root.js'); 14 | const linkPath = path.join(tmpDir, 'symlink1.js'); 15 | 16 | // Delete previously created link 17 | try { 18 | await promisify(fs.unlink)(linkPath); 19 | } catch (e) {} 20 | 21 | await promisify(fs.symlink)(linkData, linkPath); 22 | console.log('symlink done'); 23 | 24 | const destination = await promisify(fs.readlink)(linkPath); 25 | expect(destination).toBe(linkData); 26 | } 27 | }); 28 | 29 | it('should create and read hard link', async () => { 30 | await configured; 31 | if (fs.getMount('/').metadata.supportsLinks) { 32 | const srcPath = path.join(fixturesDir, 'cycles', 'root.js'); 33 | const dstPath = path.join(tmpDir, 'link1.js'); 34 | 35 | // Delete previously created link 36 | try { 37 | await promisify(fs.unlink)(dstPath); 38 | } catch (e) {} 39 | 40 | await promisify(fs.link)(srcPath, dstPath); 41 | console.log('hard link done'); 42 | 43 | const srcContent = await readFileAsync(srcPath, 'utf8'); 44 | const dstContent = await readFileAsync(dstPath, 'utf8'); 45 | expect(srcContent).toBe(dstContent); 46 | } 47 | }); 48 | }); 49 | 50 | describe.each(backends)('%s Symbolic Link Test', (name, options) => { 51 | const configured = configure({ fs: name, options }); 52 | 53 | // test creating and reading symbolic link 54 | const linkData = path.join(fixturesDir, 'cycles/'); 55 | const linkPath = path.join(tmpDir, 'cycles_link'); 56 | 57 | const unlinkAsync = promisify(fs.unlink); 58 | const existsAsync = promisify(fs.existsSync); 59 | 60 | beforeAll(async () => { 61 | await configured; 62 | 63 | // Delete previously created link 64 | await unlinkAsync(linkPath); 65 | 66 | console.log('linkData: ' + linkData); 67 | console.log('linkPath: ' + linkPath); 68 | 69 | await promisify(fs.symlink)(linkData, linkPath, 'junction'); 70 | return; 71 | }); 72 | 73 | it('should lstat symbolic link', async () => { 74 | await configured; 75 | if (fs.getMount('/').metadata.readonly || !fs.getMount('/').metadata.supportsLinks) { 76 | return; 77 | } 78 | 79 | const stats = await promisify(fs.lstat)(linkPath); 80 | expect(stats.isSymbolicLink()).toBe(true); 81 | }); 82 | 83 | it('should readlink symbolic link', async () => { 84 | await configured; 85 | if (fs.getMount('/').metadata.readonly || !fs.getMount('/').metadata.supportsLinks) { 86 | return; 87 | } 88 | const destination = await promisify(fs.readlink)(linkPath); 89 | expect(destination).toBe(linkData); 90 | }); 91 | 92 | it('should unlink symbolic link', async () => { 93 | await configured; 94 | if (fs.getMount('/').metadata.readonly || !fs.getMount('/').metadata.supportsLinks) { 95 | return; 96 | } 97 | await unlinkAsync(linkPath); 98 | expect(await existsAsync(linkPath)).toBe(false); 99 | expect(await existsAsync(linkData)).toBe(true); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/tests/all/truncate.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | 6 | describe.each(backends)('%s Truncate Tests', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | let filename: string; 9 | const data = Buffer.alloc(1024 * 16, 'x'); 10 | let success: number; 11 | 12 | beforeAll(() => { 13 | const tmp = tmpDir; 14 | filename = path.resolve(tmp, 'truncate-file.txt'); 15 | }); 16 | 17 | beforeEach(() => { 18 | success = 0; 19 | }); 20 | 21 | afterEach(async () => { 22 | await promisify(fs.unlink)(filename); 23 | }); 24 | 25 | it('Truncate Sync', () => { 26 | if (!fs.getMount('/').metadata.synchronous) return; 27 | 28 | fs.writeFileSync(filename, data); 29 | expect(fs.statSync(filename).size).toBe(1024 * 16); 30 | 31 | fs.truncateSync(filename, 1024); 32 | expect(fs.statSync(filename).size).toBe(1024); 33 | 34 | fs.truncateSync(filename); 35 | expect(fs.statSync(filename).size).toBe(0); 36 | 37 | fs.writeFileSync(filename, data); 38 | expect(fs.statSync(filename).size).toBe(1024 * 16); 39 | 40 | /* once fs.ftruncateSync is supported. 41 | const fd = fs.openSync(filename, 'r+'); 42 | fs.ftruncateSync(fd, 1024); 43 | stat = fs.statSync(filename); 44 | expect(stat.size).toBe(1024); 45 | 46 | fs.ftruncateSync(fd); 47 | stat = fs.statSync(filename); 48 | expect(stat.size).toBe(0); 49 | 50 | fs.closeSync(fd); 51 | */ 52 | }); 53 | 54 | it('Truncate Async', async () => { 55 | await configured; 56 | 57 | if (fs.getMount('/').metadata.readonly || !fs.getMount('/').metadata.synchronous) { 58 | return; 59 | } 60 | 61 | const stat = promisify(fs.stat); 62 | 63 | await promisify(fs.writeFile)(filename, data); 64 | expect((await stat(filename)).size).toBe(1024 * 16); 65 | 66 | await promisify(fs.truncate)(filename, 1024); 67 | expect((await stat(filename)).size).toBe(1024); 68 | 69 | await promisify(fs.truncate)(filename); 70 | expect((await stat(filename)).size).toBe(0); 71 | 72 | await promisify(fs.writeFile)(filename, data); 73 | expect((await stat(filename)).size).toBe(1024 * 16); 74 | 75 | const fd = await promisify(fs.open)(filename, 'w'); 76 | 77 | await promisify(fs.ftruncate)(fd, 1024); 78 | await promisify(fs.fsync)(fd); 79 | expect((await stat(filename)).size).toBe(1024); 80 | 81 | await promisify(fs.ftruncate)(fd); 82 | await promisify(fs.fsync)(fd); 83 | expect((await stat(filename)).size).toBe(0); 84 | 85 | await promisify(fs.close)(fd); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/tests/all/utimes.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | 6 | describe.each(backends)('%s Utimes Tests', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | 9 | const filename = path.join(fixturesDir, 'x.txt'); 10 | 11 | function expect_errno(syscall: string, resource: string | number, err: NodeJS.ErrnoException, errno: string) { 12 | expect(err.code).toEqual(errno); 13 | } 14 | 15 | function expect_ok(syscall: string, resource: string | number, atime: Date | number, mtime: Date | number) { 16 | const expected_mtime = fs._toUnixTimestamp(mtime); 17 | const stats = typeof resource == 'string' ? fs.statSync(resource) : (fs.fsyncSync(resource), fs.fstatSync(resource)); 18 | const real_mtime = fs._toUnixTimestamp(stats.mtime); 19 | // check up to single-second precision 20 | // sub-second precision is OS and fs dependent 21 | expect(Math.floor(expected_mtime)).toEqual(Math.floor(real_mtime)); 22 | } 23 | 24 | async function runTest(atime: Date | number, mtime: Date | number): Promise { 25 | await configured; 26 | 27 | await promisify(fs.utimes)(filename, atime, mtime); 28 | expect_ok('utimes', filename, atime, mtime); 29 | 30 | await promisify(fs.utimes)('foobarbaz', atime, mtime).catch(err => { 31 | expect_errno('utimes', 'foobarbaz', err, 'ENOENT'); 32 | }); 33 | 34 | // don't close this fd 35 | const fd = await promisify(fs.open)(filename, 'r'); 36 | 37 | await promisify(fs.futimes)(fd, atime, mtime); 38 | expect_ok('futimes', fd, atime, mtime); 39 | 40 | await promisify(fs.futimes)(-1, atime, mtime).catch(err => { 41 | expect_errno('futimes', -1, err, 'EBADF'); 42 | }); 43 | 44 | if (!fs.getMount('/').metadata.synchronous) { 45 | return; 46 | } 47 | 48 | fs.utimesSync(filename, atime, mtime); 49 | expect_ok('utimesSync', filename, atime, mtime); 50 | 51 | // some systems don't have futimes 52 | // if there's an error, it should be ENOSYS 53 | try { 54 | fs.futimesSync(fd, atime, mtime); 55 | expect_ok('futimesSync', fd, atime, mtime); 56 | } catch (ex) { 57 | expect_errno('futimesSync', fd, ex, 'ENOSYS'); 58 | } 59 | 60 | let err: NodeJS.ErrnoException; 61 | err = undefined; 62 | try { 63 | fs.utimesSync('foobarbaz', atime, mtime); 64 | } catch (ex) { 65 | err = ex; 66 | } 67 | expect_errno('utimesSync', 'foobarbaz', err, 'ENOENT'); 68 | 69 | err = undefined; 70 | try { 71 | fs.futimesSync(-1, atime, mtime); 72 | } catch (ex) { 73 | err = ex; 74 | } 75 | expect_errno('futimesSync', -1, err, 'EBADF'); 76 | } 77 | 78 | it('utimes should work', async () => { 79 | await configured; 80 | if (!fs.getMount('/').metadata.supportsProperties) { 81 | return; 82 | } 83 | await runTest(new Date('1982/09/10 13:37:00'), new Date('1982/09/10 13:37:00')); 84 | await runTest(new Date(), new Date()); 85 | await runTest(123456.789, 123456.789); 86 | const stats = fs.statSync(filename); 87 | await runTest(stats.mtime, stats.mtime); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/tests/all/write.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | 6 | const open = promisify(fs.open); 7 | const write = promisify(fs.write); 8 | const close = promisify(fs.close); 9 | const readFile = promisify(fs.readFile); 10 | const unlink = promisify(fs.unlink); 11 | 12 | describe.each(backends)('%s fs.write', (name, options) => { 13 | const configured = configure({ fs: name, options }); 14 | it('should write file with specified content asynchronously', async () => { 15 | await configured; 16 | if (fs.getMount('/').metadata.readonly) { 17 | return; 18 | } 19 | 20 | const fn = path.join(tmpDir, 'write.txt'); 21 | const fn2 = path.join(tmpDir, 'write2.txt'); 22 | const expected = 'ümlaut.'; 23 | 24 | const fd = await open(fn, 'w', 0o644); 25 | await write(fd, '', 0, 'utf8'); 26 | const written = await write(fd, expected, 0, 'utf8'); 27 | expect(written).toBe(Buffer.byteLength(expected)); 28 | await close(fd); 29 | 30 | const data = await readFile(fn, 'utf8'); 31 | expect(data).toBe(expected); 32 | 33 | await unlink(fn); 34 | const fd2 = await open(fn2, 'w', 0o644); 35 | await write(fd2, '', 0, 'utf8'); 36 | const written2 = await write(fd2, expected, 0, 'utf8'); 37 | expect(written2).toBe(Buffer.byteLength(expected)); 38 | await close(fd2); 39 | 40 | const data2 = await readFile(fn2, 'utf8'); 41 | expect(data2).toBe(expected); 42 | 43 | await unlink(fn2); 44 | }); 45 | 46 | it('should write a buffer to a file asynchronously', async () => { 47 | await configured; 48 | if (fs.getMount('/').metadata.readonly) { 49 | return; 50 | } 51 | 52 | const filename = path.join(tmpDir, 'write.txt'); 53 | const expected = Buffer.from('hello'); 54 | 55 | const fd = await promisify(fs.open)(filename, 'w', 0o644); 56 | 57 | const written = await promisify(fs.write)(fd, expected, 0, expected.length, null); 58 | 59 | expect(expected.length).toBe(written); 60 | 61 | await promisify(fs.close)(fd); 62 | 63 | const found = await promisify(fs.readFile)(filename, 'utf8'); 64 | expect(expected.toString()).toBe(found); 65 | 66 | await promisify(fs.unlink)(filename); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/tests/all/writeFile.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { promisify } from 'node:util'; 5 | import { jest } from '@jest/globals'; 6 | 7 | const s = 8 | '南越国是前203年至前111年存在于岭南地区的一个国家,国都位于番禺,疆域包括今天中国的广东、广西两省区的大部份地区,福建省、湖南、贵州、云南的一小部份地区和越南的北部。南越国是秦朝灭亡后,由南海郡尉赵佗于前203年起兵兼并桂林郡和象郡后建立。前196年和前179年,南越国曾先后两次名义上臣属于西汉,成为西汉的“外臣”。前112年,南越国末代君主赵建德与西汉发生战争,被汉武帝于前111年所灭。南越国共存在93年,历经五代君主。南越国是岭南地区的第一个有记载的政权国家,采用封建制和郡县制并存的制度,它的建立保证了秦末乱世岭南地区社会秩序的稳定,有效的改善了岭南地区落后的政治、经济现状。\n'; 9 | 10 | describe.each(backends)('%s fs.writeFile', (name, options) => { 11 | const configured = configure({ fs: name, options }); 12 | 13 | afterEach(() => { 14 | jest.restoreAllMocks(); 15 | }); 16 | 17 | it('should write and read file with specified content', async () => { 18 | await configured; 19 | 20 | if (fs.getMount('/').metadata.readonly) { 21 | return; 22 | } 23 | 24 | const join = path.join; 25 | const filename = join(tmpDir, 'test.txt'); 26 | await promisify(fs.writeFile)(filename, s); 27 | const buffer = await promisify(fs.readFile); 28 | filename; 29 | const expected = Buffer.byteLength(s); 30 | expect(expected).toBe(buffer.length); 31 | 32 | await promisify(fs.unlink)(filename); 33 | }); 34 | 35 | it('should write and read file using buffer', async () => { 36 | await configured; 37 | 38 | if (fs.getMount('/').metadata.readonly) { 39 | return; 40 | } 41 | 42 | const join = path.join; 43 | const filename = join(tmpDir, 'test2.txt'); 44 | 45 | const buf = Buffer.from(s, 'utf8'); 46 | 47 | await promisify(fs.writeFile)(filename, buf); 48 | const buffer = await promisify(fs.readFile); 49 | filename; 50 | expect(buf.length).toBe(buffer.length); 51 | 52 | await promisify(fs.unlink)(filename); 53 | }); 54 | 55 | it('should write base64 data to a file and read it back asynchronously', async () => { 56 | await configured; 57 | 58 | if (fs.getMount('/').metadata.readonly) { 59 | return; 60 | } 61 | 62 | const data = 63 | '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAQABADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDhfBUFl/wkOmPqKJJZw3aiZFBw4z93jnkkc9u9dj8XLfSI/EBt7DTo7ea2Ox5YXVo5FC7gTjq24nJPXNVtO0KATRvNHCIg3zoWJWQHqp+o4pun+EtJ0zxBq8mnLJa2d1L50NvnKRjJBUE5PAx3NYxxUY0pRtvYHSc5Ka2X9d7H/9k='; 64 | 65 | const buf = Buffer.from(data, 'base64'); 66 | const filePath = path.join(tmpDir, 'test.jpg'); 67 | 68 | await promisify(fs.writeFile)(filePath, buf); 69 | 70 | const bufdat = await promisify(fs.readFile)(filePath); 71 | expect(bufdat.toString('base64')).toBe(data); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/tests/all/writeFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | import { jest } from '@jest/globals'; 5 | 6 | describe.each(backends)('%s File Writing with Custom Mode', (name, options) => { 7 | const configured = configure({ fs: name, options }); 8 | afterEach(() => { 9 | jest.restoreAllMocks(); 10 | }); 11 | 12 | it('should write file synchronously with custom mode', async () => { 13 | await configured; 14 | 15 | const file = path.join(tmpDir, 'testWriteFileSync.txt'); 16 | const mode = 0o755; 17 | 18 | jest.spyOn(fs, 'openSync').mockImplementation((...args) => { 19 | return fs.openSync.apply(fs, args); 20 | }); 21 | 22 | jest.spyOn(fs, 'closeSync').mockImplementation((...args) => { 23 | return fs.closeSync.apply(fs, args); 24 | }); 25 | 26 | fs.writeFileSync(file, '123', { mode: mode }); 27 | 28 | const content = fs.readFileSync(file, { encoding: 'utf8' }); 29 | expect(content).toBe('123'); 30 | 31 | if (fs.getMount('/').metadata.supportsProperties) { 32 | const actual = fs.statSync(file).mode & 0o777; 33 | expect(actual).toBe(mode); 34 | } 35 | 36 | fs.unlinkSync(file); 37 | }); 38 | 39 | it('should append to a file synchronously with custom mode', async () => { 40 | await configured; 41 | 42 | const file = path.join(tmpDir, 'testAppendFileSync.txt'); 43 | const mode = 0o755; 44 | 45 | jest.spyOn(fs, 'openSync').mockImplementation((...args) => { 46 | return fs.openSync.apply(fs, args); 47 | }); 48 | 49 | jest.spyOn(fs, 'closeSync').mockImplementation((...args) => { 50 | return fs.closeSync.apply(fs, args); 51 | }); 52 | 53 | fs.appendFileSync(file, 'abc', { mode: mode }); 54 | 55 | const content = fs.readFileSync(file, { encoding: 'utf8' }); 56 | expect(content).toBe('abc'); 57 | 58 | if (fs.getMount('/').metadata.supportsProperties) { 59 | expect(fs.statSync(file).mode & mode).toBe(mode); 60 | } 61 | 62 | fs.unlinkSync(file); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/tests/all/writeSync.test.ts: -------------------------------------------------------------------------------- 1 | import { backends, fs, configure, tmpDir, fixturesDir } from '../../common'; 2 | import * as path from 'path'; 3 | 4 | describe.each(backends)('%s fs.writeSync', (name, options) => { 5 | const configured = configure({ fs: name, options }); 6 | it('should write file synchronously with specified content', async () => { 7 | await configured; 8 | 9 | if (fs.getMount('/').metadata.readonly || !fs.getMount('/').metadata.synchronous) { 10 | return; 11 | } 12 | 13 | const fn = path.join(tmpDir, 'write.txt'); 14 | const foo = 'foo'; 15 | const fd = fs.openSync(fn, 'w'); 16 | 17 | let written = fs.writeSync(fd, ''); 18 | expect(written).toBe(0); 19 | 20 | fs.writeSync(fd, foo); 21 | 22 | const bar = 'bár'; 23 | written = fs.writeSync(fd, Buffer.from(bar), 0, Buffer.byteLength(bar)); 24 | expect(written).toBeGreaterThan(3); 25 | 26 | fs.closeSync(fd); 27 | 28 | expect(fs.readFileSync(fn).toString()).toBe('foobár'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "outDir": "../build/temp/test", 7 | "lib": ["esnext", "dom"], 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": ["**/*", "*"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "target": "es2015", 5 | "outDir": "dist", 6 | "lib": ["dom", "esnext"], 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "inlineSources": true, 10 | "declaration": true, 11 | "emitDeclarationOnly": true, 12 | "typeRoots": ["node_modules/@types"] 13 | }, 14 | "include": ["src/**/*"] 15 | } 16 | --------------------------------------------------------------------------------