├── .github ├── FUNDING.yml └── workflows │ ├── checks.yml │ ├── gh-pages.yml │ ├── lint.yml │ └── mirror.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── demo ├── 12.ts ├── JSON.ts ├── createReadStream.ts ├── createWriteStream.ts ├── crud-and-cas │ ├── README.md │ ├── main.ts │ ├── node.ts │ └── webpack.config.js ├── fsa-to-node-sync-tests │ ├── README.md │ ├── main.ts │ ├── webpack.config.js │ └── worker.ts ├── fsa-to-node-zipfile │ ├── README.md │ ├── main.ts │ ├── webpack.config.js │ └── worker.ts ├── git-fsa │ ├── README.md │ ├── main.ts │ ├── webpack.config.js │ └── worker.ts ├── git-opfs │ ├── README.md │ ├── main.ts │ ├── webpack.config.js │ └── worker.ts ├── git │ ├── README.md │ └── index.ts ├── localstorage.ts ├── mountSync.ts ├── permissions.ts ├── print │ └── fs.ts ├── readFileSync.ts ├── relative-path.ts ├── rename.ts ├── runkit.js ├── snapshot │ └── index.ts ├── symlink.ts ├── throw-error.ts ├── toJSON.ts ├── volume │ └── print-tree.ts ├── watch.ts └── write.ts ├── docs ├── casfs │ └── index.md ├── crudfs │ └── index.md ├── fsa │ ├── fs-to-fsa.md │ └── fsa-to-fs.md ├── node │ ├── dependencies.md │ ├── index.md │ ├── reference.md │ ├── relative-paths.md │ └── usage.md ├── print │ └── index.md └── snapshot │ └── index.md ├── package.json ├── renovate.json ├── src ├── Dir.ts ├── Dirent.ts ├── Stats.ts ├── __tests__ │ ├── hasBigInt.js │ ├── index.test.ts │ ├── node.test.ts │ ├── process.test.ts │ ├── promises.test.ts │ ├── queueMicrotask.ts │ ├── setTimeoutUnref.test.ts │ ├── util.ts │ ├── volume.test.ts │ └── volume │ │ ├── FileHandle.test.ts │ │ ├── ReadStream.test.ts │ │ ├── WriteStream.test.ts │ │ ├── __snapshots__ │ │ ├── mkdirSync.test.ts.snap │ │ ├── renameSync.test.ts.snap │ │ └── writeSync.test.ts.snap │ │ ├── appendFile.test.ts │ │ ├── appendFileSync.test.ts │ │ ├── chmodSync.test.ts │ │ ├── closeSync.test.ts │ │ ├── copyFile.test.ts │ │ ├── copyFileSync.test.ts │ │ ├── exists.test.ts │ │ ├── existsSync.test.ts │ │ ├── lutimesSync.test.ts │ │ ├── mkdirSync.test.ts │ │ ├── openSync.test.ts │ │ ├── readFile.test.ts │ │ ├── readSync.test.ts │ │ ├── readdirSync.test.ts │ │ ├── realpathSync.test.ts │ │ ├── rename.test.ts │ │ ├── renameSync.test.ts │ │ ├── rmPromise.test.ts │ │ ├── rmSync.test.ts │ │ ├── statSync.test.ts │ │ ├── toString.test.ts │ │ ├── utimesSync.test.ts │ │ ├── write.test.ts │ │ ├── writeFileSync.test.ts │ │ └── writeSync.test.ts ├── cas │ ├── README.md │ └── types.ts ├── constants.ts ├── consts │ ├── AMODE.ts │ └── FLAG.ts ├── crud-to-cas │ ├── CrudCas.ts │ ├── CrudCasBase.ts │ ├── __tests__ │ │ ├── CrudCas.test.ts │ │ ├── CrudCasBase.test.ts │ │ ├── __snapshots__ │ │ │ └── CrudCas.test.ts.snap │ │ └── testCasfs.ts │ ├── index.ts │ └── util.ts ├── crud │ ├── README.md │ ├── __tests__ │ │ ├── matryoshka.test.ts │ │ └── testCrudfs.ts │ ├── types.ts │ └── util.ts ├── encoding.ts ├── fsa-to-crud │ ├── FsaCrud.ts │ ├── __tests__ │ │ └── FsaCrud.test.ts │ ├── index.ts │ └── util.ts ├── fsa-to-node │ ├── FsaNodeCore.ts │ ├── FsaNodeDirent.ts │ ├── FsaNodeFs.ts │ ├── FsaNodeFsOpenFile.ts │ ├── FsaNodeReadStream.ts │ ├── FsaNodeStats.ts │ ├── FsaNodeWriteStream.ts │ ├── __tests__ │ │ ├── FsaNodeFs.test.ts │ │ └── util.test.ts │ ├── constants.ts │ ├── index.ts │ ├── json.ts │ ├── types.ts │ ├── util.ts │ └── worker │ │ ├── FsaNodeSyncAdapterWorker.ts │ │ ├── FsaNodeSyncWorker.ts │ │ ├── SyncMessenger.ts │ │ ├── constants.ts │ │ └── types.ts ├── fsa │ └── types.ts ├── index.ts ├── internal │ ├── buffer.ts │ └── errors.ts ├── node-to-crud │ ├── NodeCrud.ts │ ├── __tests__ │ │ └── FsaCrud.test.ts │ └── index.ts ├── node-to-fsa │ ├── NodeFileSystemDirectoryHandle.ts │ ├── NodeFileSystemFileHandle.ts │ ├── NodeFileSystemHandle.ts │ ├── NodeFileSystemSyncAccessHandle.ts │ ├── NodeFileSystemWritableFileStream.ts │ ├── NodePermissionStatus.ts │ ├── README.md │ ├── __tests__ │ │ ├── NodeFileSystemDirectoryHandle.test.ts │ │ ├── NodeFileSystemFileHandle.test.ts │ │ ├── NodeFileSystemHandle.test.ts │ │ ├── NodeFileSystemSyncAccessHandle.test.ts │ │ ├── NodeFileSystemWritableFileStream.test.ts │ │ ├── scenarios.test.ts │ │ └── util.test.ts │ ├── index.ts │ ├── types.ts │ └── util.ts ├── node.ts ├── node │ ├── FileHandle.ts │ ├── FsPromises.ts │ ├── constants.ts │ ├── lists │ │ ├── fsCallbackApiList.ts │ │ ├── fsCommonObjectsList.ts │ │ └── fsSynchronousApiList.ts │ ├── options.ts │ ├── types │ │ ├── FsCallbackApi.ts │ │ ├── FsCommonObjects.ts │ │ ├── FsPromisesApi.ts │ │ ├── FsSynchronousApi.ts │ │ ├── index.ts │ │ ├── misc.ts │ │ └── options.ts │ └── util.ts ├── print │ ├── __tests__ │ │ └── index.test.ts │ └── index.ts ├── process.ts ├── queueMicrotask.ts ├── setTimeoutUnref.ts ├── snapshot │ ├── __tests__ │ │ ├── async.test.ts │ │ ├── binary.test.ts │ │ ├── json.test.ts │ │ └── sync.test.ts │ ├── async.ts │ ├── binary.ts │ ├── constants.ts │ ├── index.ts │ ├── json.ts │ ├── shared.ts │ ├── sync.ts │ └── types.ts ├── thingies │ ├── Defer.ts │ ├── concurrency.ts │ ├── go.ts │ ├── index.ts │ ├── of.ts │ └── types.ts ├── volume-localstorage.ts ├── volume.ts └── webfs │ ├── index.ts │ └── webpack.config.js ├── tsconfig.json ├── tslint.json └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: streamich 4 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | pull_request: 9 | branches: 10 | - master 11 | - next 12 | 13 | jobs: 14 | prepare-yarn-cache: 15 | name: Prepare yarn cache 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: lts/* 24 | cache: yarn 25 | 26 | - name: Validate cache 27 | env: 28 | # Use PnP and disable postinstall scripts as this just needs to 29 | # populate the cache for the other jobs 30 | YARN_NODE_LINKER: pnp 31 | YARN_ENABLE_SCRIPTS: false 32 | run: yarn --immutable 33 | 34 | typecheck: 35 | needs: prepare-yarn-cache 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: actions/setup-node@v4 40 | with: 41 | node-version: lts/* 42 | cache: yarn 43 | - name: install 44 | run: yarn 45 | - name: run typecheck 46 | run: yarn typecheck 47 | - name: run Prettier 48 | run: yarn prettier:check 49 | 50 | test-node: 51 | name: Test on Node.js v${{ matrix.node-version }} 52 | needs: prepare-yarn-cache 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | node-version: [18.x, 20.x] 57 | runs-on: ubuntu-latest 58 | 59 | steps: 60 | - uses: actions/checkout@v4 61 | - name: Use Node.js ${{ matrix.node-version }} 62 | uses: actions/setup-node@v4 63 | with: 64 | node-version: ${{ matrix.node-version }} 65 | cache: yarn 66 | - name: install 67 | run: yarn install 68 | - name: run tests 69 | run: yarn test 70 | env: 71 | CI: true 72 | test-os: 73 | name: Test on ${{ matrix.os }} using Node.js LTS 74 | needs: prepare-yarn-cache 75 | strategy: 76 | fail-fast: false 77 | matrix: 78 | os: [ubuntu-latest, windows-latest, macOS-latest] 79 | runs-on: ${{ matrix.os }} 80 | 81 | steps: 82 | - uses: actions/checkout@v4 83 | - uses: actions/setup-node@v4 84 | with: 85 | node-version: lts/* 86 | cache: yarn 87 | - name: install 88 | run: yarn install 89 | - name: run tests 90 | run: yarn test 91 | env: 92 | CI: true 93 | release: 94 | if: ${{ github.event_name == 'push' && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/next') }} 95 | name: Release new version 96 | needs: [typecheck, test-node, test-os] 97 | runs-on: ubuntu-latest 98 | steps: 99 | - uses: actions/checkout@v4 100 | - uses: actions/setup-node@v4 101 | with: 102 | node-version: lts/* 103 | cache: yarn 104 | - name: Install 105 | run: yarn 106 | - name: Test 107 | run: yarn test 108 | - name: Build 109 | run: yarn build 110 | - name: Release 111 | run: npx semantic-release 112 | env: 113 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 115 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | gh-pages: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [20.x] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | cache: yarn 20 | - run: yarn install --frozen-lockfile 21 | - run: yarn typedoc 22 | - run: yarn coverage 23 | - run: yarn build:pages 24 | - name: Publish to gh-pages 25 | uses: peaceiris/actions-gh-pages@v4 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_dir: ./gh-pages 29 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | with: 10 | fetch-depth: 0 11 | - uses: wagoid/commitlint-github-action@v5 12 | with: 13 | configFile: './package.json' 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/mirror.yml: -------------------------------------------------------------------------------- 1 | name: Mirror 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | mirror: 9 | name: Push To Gitlab 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - name: Push To Gitlab 16 | env: 17 | token: ${{ secrets.GITLAB_TOKEN }} 18 | run: | 19 | git config user.name "streamich" 20 | git config user.email "memfs@users.noreply.github.com" 21 | git remote add mirror "https://oauth2:${token}@gitlab.com/streamich/memfs.git" 22 | git push mirror master 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | /.idea 4 | .nyc_output 5 | /coverage/ 6 | package-lock.json 7 | /lib/ 8 | /gh-pages/ 9 | /dist 10 | /fs-test/ 11 | /typedocs/ 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | /.idea 4 | .nyc_output 5 | coverage 6 | package-lock.json 7 | /lib/ 8 | /gh-pages 9 | /dist 10 | /fs-test/ 11 | CHANGELOG.md 12 | 13 | src/json-joy 14 | src/thingies 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team [at](https://github.com/streamich/memfs/issues). The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memfs 2 | 3 | [![][chat-badge]][chat] [![][npm-badge]][npm-url] 4 | 5 | [chat]: https://onp4.com/@vadim/~memfs 6 | [chat-badge]: https://img.shields.io/badge/Chat-%F0%9F%92%AC-green?style=flat&logo=chat&link=https://onp4.com/@vadim/~memfs 7 | [npm-url]: https://www.npmjs.com/package/memfs 8 | [npm-badge]: https://img.shields.io/npm/v/memfs.svg 9 | 10 | JavaScript file system utilities for Node.js and browser. 11 | 12 | ## Install 13 | 14 | ```shell 15 | npm i memfs 16 | ``` 17 | 18 | ## Resources 19 | 20 | - Documentation 21 | - [In-memory Node.js `fs` API](./docs/node/index.md) 22 | - `experimental` [`fs` to File System Access API adapter](./docs/fsa/fs-to-fsa.md) 23 | - `experimental` [File System Access API to `fs` adapter](./docs/fsa/fsa-to-fs.md) 24 | - `experimental` [`crudfs` a CRUD-like file system abstraction](./docs/crudfs/index.md) 25 | - `experimental` [`casfs` Content Addressable Storage file system abstraction](./docs/casfs/index.md) 26 | - [Directory `snapshot` utility](./docs/snapshot/index.md) 27 | - [`print` directory tree to terminal](./docs/print/index.md) 28 | - [Code reference](https://streamich.github.io/memfs/) 29 | - [Test coverage](https://streamich.github.io/memfs/coverage/lcov-report/) 30 | 31 | ## Demos 32 | 33 | - [Git in browser, which writes to a real folder](demo/git-fsa/README.md) 34 | - [Git in browser, which writes to OPFS file system](demo/git-opfs/README.md) 35 | - [Git on in-memory file system](demo/git/README.md) 36 | - [`fs` in browser, creates a `.tar` file in real folder](demo/fsa-to-node-zipfile/README.md) 37 | - [`fs` in browser, synchronous API, writes to real folder](demo/fsa-to-node-sync-tests/README.md) 38 | - [`crudfs` and `casfs` in browser and Node.js interoperability](demo/crud-and-cas/README.md) 39 | 40 | ## See also 41 | 42 | - [`unionfs`][unionfs] - creates a union of multiple filesystem volumes 43 | - [`fs-monkey`][fs-monkey] - monkey-patches Node's `fs` module and `require` function 44 | - [`linkfs`][linkfs] - redirects filesystem paths 45 | - [`spyfs`][spyfs] - spies on filesystem actions 46 | 47 | [unionfs]: https://github.com/streamich/unionfs 48 | [fs-monkey]: https://github.com/streamich/fs-monkey 49 | [linkfs]: https://github.com/streamich/linkfs 50 | [spyfs]: https://github.com/streamich/spyfs 51 | 52 | ## License 53 | 54 | Apache 2.0 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities. The latest major version 6 | will support security patches. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Please report (suspected) security vulnerabilities to 11 | **[streamich@gmail.com](mailto:streamich@gmail.com)**. We will try to respond 12 | within 48 hours. If the issue is confirmed, we will release a patch as soon 13 | as possible depending on complexity. 14 | -------------------------------------------------------------------------------- /demo/12.ts: -------------------------------------------------------------------------------- 1 | const memfs = require('../lib'); 2 | 3 | memfs.fs.writeFileSync( 4 | '/watson.json', 5 | JSON.stringify({ 6 | ocorrencia_id: 9001, 7 | }), 8 | ); 9 | 10 | console.log(memfs.fs.readFileSync('/watson.json', 'utf8')); 11 | -------------------------------------------------------------------------------- /demo/JSON.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/volume'; 2 | 3 | const vol = Volume.fromJSON( 4 | { 5 | './src/index.js': ` 6 | import React from 'react'; 7 | import {render} from 'react-dom'; 8 | import {App} from './components/app'; 9 | 10 | const el = document.createElement('div'); 11 | document.body.appendChild(el); 12 | render(el, React.createElement(App, {})); 13 | `, 14 | 15 | './README.md': ` 16 | # Hello World 17 | 18 | This is some super cool project. 19 | `, 20 | 21 | '.node_modules/EMPTY': '', 22 | }, 23 | '/app', 24 | ); 25 | 26 | console.log(vol.toJSON()); 27 | console.log(vol.readFileSync('/app/src/index.js', 'utf8')); 28 | console.log(vol.readFileSync('/app/README.md', 'utf8')); 29 | -------------------------------------------------------------------------------- /demo/createReadStream.ts: -------------------------------------------------------------------------------- 1 | import { vol } from '../src/index'; 2 | 3 | vol.writeFileSync('/readme', '# Hello World'); 4 | const rs = vol.createReadStream('/readme', 'utf8'); 5 | rs.on('data', data => { 6 | console.log('data', data.toString()); 7 | }); 8 | -------------------------------------------------------------------------------- /demo/createWriteStream.ts: -------------------------------------------------------------------------------- 1 | import { vol } from '../src/index'; 2 | 3 | const ws = vol.createWriteStream('/readme', 'utf8'); 4 | ws.end('lol'); 5 | ws.on('finish', () => { 6 | console.log(vol.readFileSync('/readme').toString()); 7 | }); 8 | -------------------------------------------------------------------------------- /demo/crud-and-cas/README.md: -------------------------------------------------------------------------------- 1 | This demo showcases `crudfs` and `casfs` interoperability in browser and Node.js. 2 | 3 | First, in browser an object is stored into `casfs` (Content Addressable Storage) 4 | and its hash (CID) is persisted using `crudfs`, in a real user folder. 5 | 6 | Then, from Node.js, the CID is retrieved using `crudfs` and the object is read 7 | using `casfs`. 8 | 9 | https://github.com/streamich/memfs/assets/9773803/02ba339c-6e13-4712-a02f-672674300d27 10 | 11 | Run: 12 | 13 | ``` 14 | yarn demo:git-fsa 15 | ``` 16 | -------------------------------------------------------------------------------- /demo/crud-and-cas/main.ts: -------------------------------------------------------------------------------- 1 | import { FsaCrud } from '../../src/fsa-to-crud'; 2 | import { CrudCas } from '../../src/crud-to-cas'; 3 | import type * as fsa from '../../src/fsa/types'; 4 | 5 | const hash = async (data: Uint8Array): Promise => { 6 | const hashBuffer = await crypto.subtle.digest('SHA-256', data); 7 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 8 | const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); 9 | return hashHex; 10 | }; 11 | 12 | const demo = async (dir: fsa.IFileSystemDirectoryHandle) => { 13 | try { 14 | const crud = new FsaCrud(dir); 15 | const cas = new CrudCas(await crud.from(['objects']), { hash }); 16 | 17 | // Store "Hello, world!" in object storage 18 | const cid = await cas.put(new TextEncoder().encode('Hello, world!')); 19 | 20 | // Store the CID in the refs/heads/main.txt file 21 | await crud.put(['refs', 'heads'], 'main.txt', new TextEncoder().encode(cid)); 22 | } catch (error) { 23 | console.log(error); 24 | console.log((error).name); 25 | } 26 | }; 27 | 28 | const main = async () => { 29 | const button = document.createElement('button'); 30 | button.textContent = 'Select an empty folder'; 31 | document.body.appendChild(button); 32 | button.onclick = async () => { 33 | const dir = await (window as any).showDirectoryPicker({ id: 'demo', mode: 'readwrite' }); 34 | await demo(dir); 35 | }; 36 | }; 37 | 38 | main(); 39 | -------------------------------------------------------------------------------- /demo/crud-and-cas/node.ts: -------------------------------------------------------------------------------- 1 | // Run: npx ts-node demo/crud-and-cas/node.ts 2 | 3 | import { NodeCrud } from '../../src/node-to-crud'; 4 | import { CrudCas } from '../../src/crud-to-cas'; 5 | import * as fs from 'fs'; 6 | const root = require('app-root-path'); 7 | const path = require('path'); 8 | 9 | const hash = async (data: Uint8Array): Promise => { 10 | const hashBuffer = await crypto.subtle.digest('SHA-256', data); 11 | const hashArray = Array.from(new Uint8Array(hashBuffer)); 12 | const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); 13 | return hashHex; 14 | }; 15 | 16 | const main = async () => { 17 | const dir = path.resolve(root.path, 'fs-test'); 18 | const crud = new NodeCrud({ fs: fs.promises, dir }); 19 | const cas = new CrudCas(await crud.from(['objects']), { hash }); 20 | 21 | // Retrieve the CID from the refs/heads/main.txt file 22 | const cid = await crud.get(['refs', 'heads'], 'main.txt'); 23 | const cidText = Buffer.from(cid).toString(); 24 | console.log('CID:', cidText); 25 | 26 | // Retrieve the data from the object storage 27 | const data = await cas.get(cidText); 28 | const dataText = Buffer.from(data).toString(); 29 | console.log('Content:', dataText); 30 | }; 31 | 32 | main(); 33 | -------------------------------------------------------------------------------- /demo/crud-and-cas/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const root = require('app-root-path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | entry: { 9 | bundle: __dirname + '/main', 10 | }, 11 | plugins: [ 12 | new HtmlWebpackPlugin({ 13 | title: 'Development', 14 | }), 15 | ], 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.tsx?$/, 20 | exclude: /node_modules/, 21 | loader: 'ts-loader', 22 | }, 23 | ], 24 | }, 25 | resolve: { 26 | extensions: ['.tsx', '.ts', '.js'], 27 | }, 28 | output: { 29 | filename: '[name].js', 30 | path: path.resolve(root.path, 'dist'), 31 | }, 32 | devServer: { 33 | port: 9876, 34 | hot: false, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /demo/fsa-to-node-sync-tests/README.md: -------------------------------------------------------------------------------- 1 | # Synchronous `fs` in browser 2 | 3 | This demo executes tests of **synchronous** Node.js API built on top of File 4 | System Access API in browser. 5 | 6 | There are two tests: 7 | 8 | - OPFS — virtual file system built into browsers. 9 | - Native File System Access API folder — a real folder that user explicitly grants permission to. 10 | 11 | See the assertions run in the browser console. 12 | 13 | https://github.com/streamich/memfs/assets/9773803/f841b6cc-728d-4341-b426-3daa6b43f8ac 14 | 15 | Run: 16 | 17 | ``` 18 | demo:fsa-to-node-sync-tests 19 | ``` 20 | -------------------------------------------------------------------------------- /demo/fsa-to-node-sync-tests/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const root = require('app-root-path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | entry: { 9 | bundle: __dirname + '/main', 10 | worker: __dirname + '/worker', 11 | }, 12 | plugins: [ 13 | // new ForkTsCheckerWebpackPlugin(), 14 | new HtmlWebpackPlugin({ 15 | title: 'Development', 16 | }), 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.tsx?$/, 22 | exclude: /node_modules/, 23 | loader: 'ts-loader', 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: ['.tsx', '.ts', '.js'], 29 | fallback: { 30 | assert: require.resolve('assert'), 31 | buffer: require.resolve('buffer'), 32 | path: require.resolve('path-browserify'), 33 | process: require.resolve('process/browser'), 34 | // stream: require.resolve('streamx'), 35 | stream: require.resolve('readable-stream'), 36 | url: require.resolve('url'), 37 | util: require.resolve('util'), 38 | // fs: path.resolve(__dirname, '../../src/index.ts'), 39 | }, 40 | }, 41 | output: { 42 | filename: '[name].js', 43 | path: path.resolve(root.path, 'dist'), 44 | }, 45 | devServer: { 46 | // HTTPS is required for SharedArrayBuffer to work. 47 | https: true, 48 | headers: { 49 | // These two headers are required for SharedArrayBuffer to work. 50 | 'Cross-Origin-Opener-Policy': 'same-origin', 51 | 'Cross-Origin-Embedder-Policy': 'require-corp', 52 | }, 53 | port: 9876, 54 | hot: false, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /demo/fsa-to-node-sync-tests/worker.ts: -------------------------------------------------------------------------------- 1 | (self as any).process = require('process/browser'); 2 | (self as any).Buffer = require('buffer').Buffer; 3 | 4 | import { FsaNodeSyncWorker } from '../../src/fsa-to-node/worker/FsaNodeSyncWorker'; 5 | 6 | if (typeof window === 'undefined') { 7 | const worker = new FsaNodeSyncWorker(); 8 | worker.start(); 9 | } 10 | -------------------------------------------------------------------------------- /demo/fsa-to-node-zipfile/README.md: -------------------------------------------------------------------------------- 1 | This demo shows how `tar-stream` package can be used to create a zip file from 2 | a folder selected with File System Access API. 3 | 4 | Below demo uses the File System Access API in the browser to get access to a real folder 5 | on the file system. It then converts a [`FileSystemDirectoryHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle) instance 6 | to a Node-like `fs` file system. 7 | 8 | It then creates two text files using `fs.promises.writeFile`, and then uses `tar-stream` package to create a zip file 9 | and write it back got the file system using Node's `fs.createWriteStream`. 10 | 11 | https://github.com/streamich/memfs/assets/9773803/8dc61d1e-61bf-4dfc-973b-028332fd4473 12 | 13 | Run: 14 | 15 | ``` 16 | demo:fsa-to-node-zipfile 17 | ``` 18 | -------------------------------------------------------------------------------- /demo/fsa-to-node-zipfile/main.ts: -------------------------------------------------------------------------------- 1 | (window as any).process = require('process/browser'); 2 | (window as any).Buffer = require('buffer').Buffer; 3 | 4 | import type * as fsa from '../../src/fsa/types'; 5 | import { FsaNodeFs } from '../../src/fsa-to-node'; 6 | const tar = require('tar-stream'); 7 | 8 | const demo = async (dir: fsa.IFileSystemDirectoryHandle) => { 9 | const fs = new FsaNodeFs(dir); 10 | await fs.promises.writeFile('hello.txt', 'Hello World'); 11 | await fs.promises.writeFile('cool.txt', 'Cool Worlds channel'); 12 | 13 | const list = (await fs.promises.readdir('/')) as string[]; 14 | 15 | const pack = tar.pack(); 16 | const tarball = fs.createWriteStream('backups.tar'); 17 | pack.pipe(tarball); 18 | 19 | for (const item of list) { 20 | if (item[0] === '.') continue; 21 | const stat = await fs.promises.stat(item); 22 | if (!stat.isFile()) continue; 23 | pack.entry({ name: '/backups/' + item }, await fs.promises.readFile('/' + item), () => {}); 24 | } 25 | 26 | pack.finalize(); 27 | }; 28 | 29 | const main = async () => { 30 | const button = document.createElement('button'); 31 | button.textContent = 'Select an empty folder'; 32 | document.body.appendChild(button); 33 | button.onclick = async () => { 34 | const dir = await (window as any).showDirectoryPicker({ id: 'demo', mode: 'readwrite' }); 35 | await demo(dir); 36 | }; 37 | }; 38 | 39 | main(); 40 | -------------------------------------------------------------------------------- /demo/fsa-to-node-zipfile/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const root = require('app-root-path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | entry: { 9 | bundle: __dirname + '/main', 10 | worker: __dirname + '/worker', 11 | }, 12 | plugins: [ 13 | new HtmlWebpackPlugin({ 14 | title: 'Development', 15 | }), 16 | ], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | exclude: /node_modules/, 22 | loader: 'ts-loader', 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: ['.tsx', '.ts', '.js'], 28 | fallback: { 29 | assert: require.resolve('assert'), 30 | buffer: require.resolve('buffer'), 31 | path: require.resolve('path-browserify'), 32 | process: require.resolve('process/browser'), 33 | stream: require.resolve('readable-stream'), 34 | url: require.resolve('url'), 35 | util: require.resolve('util'), 36 | }, 37 | }, 38 | output: { 39 | filename: '[name].js', 40 | path: path.resolve(root.path, 'dist'), 41 | }, 42 | devServer: { 43 | // HTTPS is required for SharedArrayBuffer to work. 44 | https: true, 45 | headers: { 46 | // These two headers are required for SharedArrayBuffer to work. 47 | 'Cross-Origin-Opener-Policy': 'same-origin', 48 | 'Cross-Origin-Embedder-Policy': 'require-corp', 49 | }, 50 | port: 9876, 51 | hot: false, 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /demo/fsa-to-node-zipfile/worker.ts: -------------------------------------------------------------------------------- 1 | (self as any).process = require('process/browser'); 2 | (self as any).Buffer = require('buffer').Buffer; 3 | 4 | import { FsaNodeSyncWorker } from '../../src/fsa-to-node/worker/FsaNodeSyncWorker'; 5 | 6 | if (typeof window === 'undefined') { 7 | const worker = new FsaNodeSyncWorker(); 8 | worker.start(); 9 | } 10 | -------------------------------------------------------------------------------- /demo/git-fsa/README.md: -------------------------------------------------------------------------------- 1 | This demo showcase how to run Git in browser but write to a real user file system 2 | folder. It is possible through File System Access API. The API allows to request 3 | a folder from user and then use it as a real file system in browser. 4 | 5 | In this demo we use `memfs` to create a Node `fs`-like file system in browser 6 | out of the folder provided by File System Access API. We then use `isomorphic-git` 7 | to run Git commands on that file system. 8 | 9 | In the demo itself we initiate a Git repo, then we create a `README.md` file, we 10 | stage it, and finally we commit it. 11 | 12 | https://github.com/streamich/memfs/assets/9773803/c15212e8-3ee2-4d2a-b325-9fbdcc377c12 13 | 14 | Run: 15 | 16 | ``` 17 | yarn demo:git-fsa 18 | ``` 19 | -------------------------------------------------------------------------------- /demo/git-fsa/main.ts: -------------------------------------------------------------------------------- 1 | (window as any).process = require('process/browser'); 2 | (window as any).Buffer = require('buffer').Buffer; 3 | 4 | import { FsaNodeFs } from '../../src/fsa-to-node'; 5 | import type * as fsa from '../../src/fsa/types'; 6 | 7 | import git from 'isomorphic-git'; 8 | 9 | const demo = async (dir: fsa.IFileSystemDirectoryHandle) => { 10 | try { 11 | const fs = ((window).fs = new FsaNodeFs(dir)); 12 | 13 | console.log('Create "/repo" folder'); 14 | await fs.promises.mkdir('/repo'); 15 | 16 | console.log('Init git repo'); 17 | await git.init({ fs, dir: 'repo' }); 18 | 19 | console.log('Create README file'); 20 | await fs.promises.writeFile('/repo/README.md', 'Hello World\n'); 21 | 22 | console.log('Stage README file'); 23 | await git.add({ fs, dir: '/repo', filepath: 'README.md' }); 24 | 25 | console.log('Commit README file'); 26 | await git.commit({ 27 | fs, 28 | dir: '/repo', 29 | author: { name: 'Git', email: 'leonid@kingdom.com' }, 30 | message: 'fea: initial commit', 31 | }); 32 | } catch (error) { 33 | console.log(error); 34 | console.log((error).name); 35 | } 36 | }; 37 | 38 | const main = async () => { 39 | const button = document.createElement('button'); 40 | button.textContent = 'Select an empty folder'; 41 | document.body.appendChild(button); 42 | button.onclick = async () => { 43 | const dir = await (window as any).showDirectoryPicker({ id: 'demo', mode: 'readwrite' }); 44 | await demo(dir); 45 | }; 46 | }; 47 | 48 | main(); 49 | -------------------------------------------------------------------------------- /demo/git-fsa/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const root = require('app-root-path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | entry: { 9 | bundle: __dirname + '/main', 10 | worker: __dirname + '/worker', 11 | }, 12 | plugins: [ 13 | new HtmlWebpackPlugin({ 14 | title: 'Development', 15 | }), 16 | ], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | exclude: /node_modules/, 22 | loader: 'ts-loader', 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: ['.tsx', '.ts', '.js'], 28 | fallback: { 29 | assert: require.resolve('assert'), 30 | buffer: require.resolve('buffer'), 31 | path: require.resolve('path-browserify'), 32 | process: require.resolve('process/browser'), 33 | stream: require.resolve('readable-stream'), 34 | url: require.resolve('url'), 35 | util: require.resolve('util'), 36 | }, 37 | }, 38 | externals: { 39 | fs: 'window.fs', 40 | }, 41 | output: { 42 | filename: '[name].js', 43 | path: path.resolve(root.path, 'dist'), 44 | }, 45 | devServer: { 46 | // HTTPS is required for SharedArrayBuffer to work. 47 | https: true, 48 | headers: { 49 | // These two headers are required for SharedArrayBuffer to work. 50 | 'Cross-Origin-Opener-Policy': 'same-origin', 51 | 'Cross-Origin-Embedder-Policy': 'require-corp', 52 | }, 53 | port: 9876, 54 | hot: false, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /demo/git-fsa/worker.ts: -------------------------------------------------------------------------------- 1 | (self as any).process = require('process/browser'); 2 | (self as any).Buffer = require('buffer').Buffer; 3 | 4 | import { FsaNodeSyncWorker } from '../../src/fsa-to-node/worker/FsaNodeSyncWorker'; 5 | 6 | if (typeof window === 'undefined') { 7 | const worker = new FsaNodeSyncWorker(); 8 | worker.start(); 9 | } 10 | -------------------------------------------------------------------------------- /demo/git-opfs/README.md: -------------------------------------------------------------------------------- 1 | This demo showcase how to run Git in browser built-int OPFS file system. OPFS 2 | stands for (Origin Private File System) it is a virtual file system available 3 | in browser and it requires no permission to access. 4 | 5 | In this demo we use `memfs` to create a Node `fs`-like file system in browser 6 | out of OPFS. We then use `isomorphic-git` to run Git commands on that file system. 7 | 8 | In the demo itself we initiate a Git repo, then we create a `README.md` file, we 9 | stage it, and finally we commit it. 10 | 11 | https://github.com/streamich/memfs/assets/9773803/bbc83f3f-98ad-48cc-9259-b6f543aa1a03 12 | 13 | Run: 14 | 15 | ``` 16 | yarn demo:git-opfs 17 | ``` 18 | 19 | You can install [OPFS Explorer](https://chrome.google.com/webstore/detail/opfs-explorer/acndjpgkpaclldomagafnognkcgjignd) Chrome 20 | extension to verify the contents of the OPFS file system. 21 | -------------------------------------------------------------------------------- /demo/git-opfs/main.ts: -------------------------------------------------------------------------------- 1 | (window as any).process = require('process/browser'); 2 | (window as any).Buffer = require('buffer').Buffer; 3 | 4 | import { FsaNodeFs, FsaNodeSyncAdapterWorker } from '../../src/fsa-to-node'; 5 | import type * as fsa from '../../src/fsa/types'; 6 | 7 | const dir = navigator.storage.getDirectory() as unknown as Promise; 8 | const fs = ((window).fs = new FsaNodeFs(dir)); 9 | 10 | import git from 'isomorphic-git'; 11 | 12 | const main = async () => { 13 | try { 14 | const adapter = await FsaNodeSyncAdapterWorker.start('https://localhost:9876/worker.js', dir); 15 | fs.syncAdapter = adapter; 16 | 17 | console.log('Create "/repo" folder'); 18 | await fs.promises.mkdir('/repo'); 19 | 20 | console.log('Init git repo'); 21 | await git.init({ fs, dir: 'repo' }); 22 | 23 | console.log('Create README file'); 24 | await fs.promises.writeFile('/repo/README.md', 'Hello World\n'); 25 | 26 | console.log('Stage README file'); 27 | await git.add({ fs, dir: '/repo', filepath: 'README.md' }); 28 | 29 | console.log('Commit README file'); 30 | await git.commit({ 31 | fs, 32 | dir: '/repo', 33 | author: { name: 'Git', email: 'leonid@kingdom.com' }, 34 | message: 'fea: initial commit', 35 | }); 36 | } catch (error) { 37 | console.log(error); 38 | console.log((error).name); 39 | } 40 | }; 41 | 42 | main(); 43 | -------------------------------------------------------------------------------- /demo/git-opfs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const root = require('app-root-path'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | devtool: 'inline-source-map', 8 | entry: { 9 | bundle: __dirname + '/main', 10 | worker: __dirname + '/worker', 11 | }, 12 | plugins: [ 13 | new HtmlWebpackPlugin({ 14 | title: 'Development', 15 | }), 16 | ], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.tsx?$/, 21 | exclude: /node_modules/, 22 | loader: 'ts-loader', 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: ['.tsx', '.ts', '.js'], 28 | fallback: { 29 | assert: require.resolve('assert'), 30 | buffer: require.resolve('buffer'), 31 | path: require.resolve('path-browserify'), 32 | process: require.resolve('process/browser'), 33 | stream: require.resolve('readable-stream'), 34 | url: require.resolve('url'), 35 | util: require.resolve('util'), 36 | }, 37 | }, 38 | externals: { 39 | fs: 'window.fs', 40 | }, 41 | output: { 42 | filename: '[name].js', 43 | path: path.resolve(root.path, 'dist'), 44 | }, 45 | devServer: { 46 | // HTTPS is required for SharedArrayBuffer to work. 47 | https: true, 48 | headers: { 49 | // These two headers are required for SharedArrayBuffer to work. 50 | 'Cross-Origin-Opener-Policy': 'same-origin', 51 | 'Cross-Origin-Embedder-Policy': 'require-corp', 52 | }, 53 | port: 9876, 54 | hot: false, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /demo/git-opfs/worker.ts: -------------------------------------------------------------------------------- 1 | (self as any).process = require('process/browser'); 2 | (self as any).Buffer = require('buffer').Buffer; 3 | 4 | import { FsaNodeSyncWorker } from '../../src/fsa-to-node/worker/FsaNodeSyncWorker'; 5 | 6 | if (typeof window === 'undefined') { 7 | const worker = new FsaNodeSyncWorker(); 8 | worker.start(); 9 | } 10 | -------------------------------------------------------------------------------- /demo/git/README.md: -------------------------------------------------------------------------------- 1 | # `isomorphic-git` running on `memfs` in-memory file system 2 | 3 | This demo shows how to run `isomorphic-git` on `memfs` in-memory file system. It 4 | creates a new folder `/repo`, then inits a Git repository there, then creates a 5 | a file `/repo/README.md` and commits it. 6 | 7 | ![](https://github.com/streamich/memfs/assets/9773803/021af8ea-690d-4e4e-9d05-d3e0dea60672) 8 | 9 | Run: 10 | 11 | ``` 12 | yarn 13 | npx ts-node demo/git/index.ts 14 | ``` 15 | 16 | The demo will print the snapshot of the file system after each step. 17 | -------------------------------------------------------------------------------- /demo/git/index.ts: -------------------------------------------------------------------------------- 1 | // Run: npx ts-node demo/git/index.ts 2 | 3 | import git from 'isomorphic-git'; 4 | import { memfs } from '../../src'; 5 | 6 | const main = async () => { 7 | const { fs } = memfs(); 8 | 9 | fs.mkdirSync('/repo'); 10 | console.log('New folder:', (fs).__vol.toJSON()); 11 | 12 | await git.init({ fs, dir: '/repo' }); 13 | console.log('Git init:', (fs).__vol.toJSON()); 14 | 15 | fs.writeFileSync('/repo/README.md', 'Hello World\n'); 16 | console.log('README added:', (fs).__vol.toJSON()); 17 | 18 | await git.add({ fs, dir: '/repo', filepath: 'README.md' }); 19 | console.log('README staged:', (fs).__vol.toJSON()); 20 | 21 | await git.commit({ 22 | fs, 23 | dir: '/repo', 24 | author: { name: 'Git', email: 'leonid@kingdom.com' }, 25 | message: 'fea: initial commit', 26 | }); 27 | console.log('README committed:', (fs).__vol.toJSON()); 28 | }; 29 | 30 | main(); 31 | -------------------------------------------------------------------------------- /demo/localstorage.ts: -------------------------------------------------------------------------------- 1 | import { createVolume } from '../src/volume-localstorage'; 2 | 3 | const obj = {}; 4 | const Volume = createVolume('default', obj); 5 | 6 | const vol = new Volume(); 7 | vol.fromJSON({ '/foo': 'bar', '/foo2': 'bar2' }); 8 | // vol.unlinkSync('/foo'); 9 | 10 | console.log(obj); 11 | console.log(vol.toJSON()); 12 | -------------------------------------------------------------------------------- /demo/mountSync.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/index'; 2 | 3 | const vol = new Volume(); 4 | vol.mountSync('/test', { 5 | foo: 'bar', 6 | }); 7 | 8 | console.log(vol.toJSON()); 9 | -------------------------------------------------------------------------------- /demo/permissions.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/index'; 2 | 3 | const vol = Volume.fromJSON({ '/foo': 'bar' }); 4 | 5 | console.log(vol.readFileSync('/foo', 'utf8')); 6 | 7 | vol.chmodSync('/foo', 0); 8 | 9 | console.log(vol.readFileSync('/foo', 'utf8')); 10 | -------------------------------------------------------------------------------- /demo/readFileSync.ts: -------------------------------------------------------------------------------- 1 | import { fs } from '../src/index'; 2 | 3 | fs.writeFileSync('/test.txt', 'hello...'); 4 | console.log(fs.readFileSync('/test.txt', 'utf8')); 5 | -------------------------------------------------------------------------------- /demo/relative-path.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/volume'; 2 | 3 | const vol = Volume.fromJSON({ './README': 'Hello' }); 4 | 5 | console.log(vol.toJSON()); 6 | console.log(vol.readdirSync('/home')); 7 | -------------------------------------------------------------------------------- /demo/rename.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/volume'; 2 | 3 | const vol = Volume.fromJSON({ '/foo/foo': 'bar' }); 4 | vol.renameSync('/foo/foo', '/foo/foo2'); 5 | console.log(vol.toJSON()); 6 | -------------------------------------------------------------------------------- /demo/runkit.js: -------------------------------------------------------------------------------- 1 | const { fs } = require('memfs'); 2 | 3 | fs.writeFileSync('/hello.txt', 'Hello World'); 4 | 5 | console.log(fs.readFileSync('/hello.txt', 'utf8')); 6 | -------------------------------------------------------------------------------- /demo/snapshot/index.ts: -------------------------------------------------------------------------------- 1 | // Run: npx ts-node demo/snapshot/index.ts 2 | 3 | import { memfs } from '../../src'; 4 | import * as snapshot from '../../src/snapshot'; 5 | 6 | const data = { 7 | '/': { 8 | file1: '...', 9 | dir: { 10 | file2: '...', 11 | }, 12 | }, 13 | }; 14 | 15 | const { fs } = memfs(data); 16 | 17 | console.log(snapshot.toSnapshotSync({ fs })); 18 | console.log(snapshot.toBinarySnapshotSync({ fs })); 19 | console.log(snapshot.toJsonSnapshotSync({ fs })); 20 | console.log(Buffer.from(snapshot.toJsonSnapshotSync({ fs })).toString()); 21 | -------------------------------------------------------------------------------- /demo/symlink.ts: -------------------------------------------------------------------------------- 1 | // Run: npx ts-node demo/symlink.ts 2 | 3 | import { vol } from '../src'; 4 | 5 | vol.fromJSON({ '/a1/a2/a3/a4/a5/hello.txt': 'world!' }); 6 | console.log(vol.readFileSync('/a1/a2/a3/a4/a5/hello.txt', 'utf8')); 7 | 8 | vol.symlinkSync('/a1/a2/a3/a4/a5/hello.txt', '/link'); 9 | console.log(vol.readFileSync('/link', 'utf8')); 10 | 11 | vol.symlinkSync('/a1', '/b1'); 12 | console.log(vol.readFileSync('/b1/a2/a3/a4/a5/hello.txt', 'utf8')); 13 | 14 | vol.symlinkSync('/a1/a2', '/b2'); 15 | console.log(vol.readFileSync('/b2/a3/a4/a5/hello.txt', 'utf8')); 16 | 17 | vol.symlinkSync('/b2', '/c2'); 18 | console.log(vol.readFileSync('/c2/a3/a4/a5/hello.txt', 'utf8')); 19 | 20 | vol.mkdirSync('/d1/d2', { recursive: true }); 21 | vol.symlinkSync('/c2', '/d1/d2/to-c2'); 22 | console.log(vol.readFileSync('/d1/d2/to-c2/a3/a4/a5/hello.txt', 'utf8')); 23 | -------------------------------------------------------------------------------- /demo/throw-error.ts: -------------------------------------------------------------------------------- 1 | const errors = require('../src/internal/errors'); 2 | 3 | const err = new errors.TypeError('ENOENT', 'Test'); 4 | console.log(err); 5 | -------------------------------------------------------------------------------- /demo/toJSON.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../src/index'; 2 | 3 | const vol = Volume.fromJSON({ 4 | '/foo': 'bar', 5 | '/dir1/file.js': '// comment...', 6 | '/dir2/index.js': 'process', 7 | '/dir2/main.js': 'console.log(123)', 8 | }); 9 | console.log(vol.toJSON()); 10 | 11 | console.log(vol.toJSON('/dir2')); 12 | 13 | console.log(vol.toJSON('/dir1')); 14 | 15 | console.log(vol.toJSON(['/dir2', '/dir1'])); 16 | 17 | console.log(vol.toJSON('/')); 18 | 19 | let a = { a: 1 }; 20 | console.log(vol.toJSON('/dir1', a)); 21 | 22 | console.log(vol.toJSON('/dir2', {}, true)); 23 | -------------------------------------------------------------------------------- /demo/volume/print-tree.ts: -------------------------------------------------------------------------------- 1 | // Run: npx ts-node demo/volume/print-tree.ts 2 | 3 | import { memfs } from '../../src'; 4 | 5 | const { vol } = memfs({ 6 | '/Users/streamich/src/github/memfs/src': { 7 | 'package.json': '...', 8 | 'tsconfig.json': '...', 9 | 'index.ts': '...', 10 | util: { 11 | 'index.ts': '...', 12 | print: { 13 | 'index.ts': '...', 14 | 'printTree.ts': '...', 15 | }, 16 | }, 17 | }, 18 | }); 19 | 20 | console.log(vol.toTree()); 21 | 22 | // Output: 23 | // / 24 | // └─ Users/ 25 | // └─ streamich/ 26 | // └─ src/ 27 | // └─ github/ 28 | // └─ memfs/ 29 | // └─ src/ 30 | // ├─ index.ts 31 | // ├─ package.json 32 | // ├─ tsconfig.json 33 | // └─ util/ 34 | // ├─ index.ts 35 | // └─ print/ 36 | // ├─ index.ts 37 | // └─ printTree.ts 38 | -------------------------------------------------------------------------------- /demo/watch.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { Volume } from '../src/volume'; 4 | 5 | // const filename = path.join(__dirname, '../test/mock/text.txt'); 6 | // fs.watch(filename, (event, filename) => { 7 | // console.log(event, filename); 8 | // }); 9 | 10 | const vol = Volume.fromJSON({ '/hello.txt': 'World' }); 11 | vol.watch('/hello.txt', {}, (event, filename) => { 12 | console.log(event, filename); 13 | console.log(vol.readFileSync('/hello.txt', 'utf8')); 14 | }); 15 | 16 | vol.appendFileSync('/hello.txt', '!'); 17 | 18 | setTimeout(() => { 19 | vol.appendFileSync('/hello.txt', ' OK?'); 20 | }, 1000); 21 | -------------------------------------------------------------------------------- /demo/write.ts: -------------------------------------------------------------------------------- 1 | import { fs } from '../src/index'; 2 | 3 | const fd = fs.openSync('/test.txt', 'w'); 4 | const data = '123'; 5 | fs.write(fd, Buffer.from(data), (err, bytes, buf) => { 6 | // console.log(err, bytes, buf); 7 | fs.closeSync(fd); 8 | }); 9 | -------------------------------------------------------------------------------- /docs/casfs/index.md: -------------------------------------------------------------------------------- 1 | # `casfs` 2 | 3 | `casfs` is a Content Addressable Storage (CAS) abstraction over a file system. 4 | It has no folders nor files. Instead, it has _blobs_ which are identified by their content. 5 | 6 | Essentially, it provides two main operations: `put` and `get`. The `put` operation 7 | takes a blob and stores it in the underlying file system and returns the blob's hash digest. 8 | The `get` operation takes a hash and returns the blob, which matches the hash digest, if it exists. 9 | 10 | ## Usage 11 | 12 | ### `casfs` on top of Node.js `fs` module 13 | 14 | `casfs` builds on top of [`crudfs`](../crudfs//index.md), and `crudfs`—in turn—builds on top of 15 | [File System Access API](../fsa/fs-to-fsa.md). 16 | 17 | ```js 18 | import * as fs from 'fs'; 19 | import { nodeToFsa } from 'memfs/lib/node-to-fsa'; 20 | import { FsaCrud } from 'memfs/lib/fsa-to-crud'; 21 | 22 | const fsa = nodeToFsa(fs, '/path/to/folder', { mode: 'readwrite' }); 23 | const crud = new FsaCrud(fsa); 24 | const cas = new CrudCas(crud, { hash }); 25 | ``` 26 | 27 | The `hash` is a function which computes a hash digest `string` from a `Uint8Array` blob. 28 | Here is how one could look like: 29 | 30 | ```ts 31 | import { createHash } from 'crypto'; 32 | 33 | const hash = async (blob: Uint8Array): Promise => { 34 | const shasum = createHash('sha1'); 35 | shasum.update(blob); 36 | return shasum.digest('hex'); 37 | }; 38 | ``` 39 | 40 | Now that you have a `cas` instance, you can use it to `put` and `get` blobs. 41 | 42 | ```js 43 | const blob = new Uint8Array([1, 2, 3]); 44 | 45 | const hash = await cas.put(blob); 46 | console.log(hash); // 9dc58b6d4e8eefb5a3c3e0c9f4a1a0b1b2b3b4b5 47 | 48 | const blob2 = await cas.get(hash); 49 | ``` 50 | 51 | You can also delete blobs: 52 | 53 | ```js 54 | await cas.del(hash); 55 | ``` 56 | 57 | And retrieve information about blobs: 58 | 59 | ```js 60 | const info = await cas.info(hash); 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/crudfs/index.md: -------------------------------------------------------------------------------- 1 | # `crudfs` 2 | 3 | `crudfs` is a CRUD-like file system abstraction. It is a thin wrapper around a 4 | real file system, which allows to perform CRUD operations on files and folders. 5 | It is intended to be light (so can be efficiently bundled for browser), 6 | have small API surface but cover most of the useful file manipulation scenarios. 7 | 8 | Folder are referred to as _collections_ or _types_; and files are referred to as _resources_. 9 | 10 | ## Usage 11 | 12 | ### `crudfs` from File System Access API 13 | 14 | You can construct `crudfs` on top of the File System Access API `FileSystemDirectoryHandle` like so: 15 | 16 | ```js 17 | import { FsaCrud } from 'memfs/lib/fsa-to-crud'; 18 | 19 | const crud = new FsaCrud(dirHandle); 20 | ``` 21 | 22 | Now you can use the `crud` instance to execute CRUD operations on the file system. 23 | See the available methods [here](../../src/crud/types.ts). 24 | 25 | In this below example we create a `/user/files` collection which contains two files: 26 | 27 | ```js 28 | await crud.put(['user', 'files'], 'file1.bin', new Uint8Array([1, 2, 3])); 29 | await crud.put(['user', 'files'], 'file2.bin', new Uint8Array([1, 2, 3])); 30 | ``` 31 | 32 | We can list all resources in the `/user/files` collection: 33 | 34 | ```js 35 | const files = await crud.list(['user', 'files']); 36 | ``` 37 | 38 | Retrieve a resource contents: 39 | 40 | ```js 41 | const file1 = await crud.get(['user', 'files'], 'file1.bin'); 42 | ``` 43 | 44 | Delete a resource: 45 | 46 | ```js 47 | await crud.delete(['user', 'files'], 'file1.bin'); 48 | ``` 49 | 50 | Drop all resources in a collection: 51 | 52 | ```js 53 | await crud.drop(['user', 'files']); 54 | ``` 55 | 56 | ### `crudfs` from Node.js `fs` module 57 | 58 | You can run `crudfs` on top of Node.js `fs` module like so: 59 | 60 | ```js 61 | import * as fs from 'fs'; 62 | import { NodeCrud } from 'memfs/lib/node-to-crud'; 63 | 64 | const crud = new NodeCrud({ fs: fs.promises, dir: '/path/to/folder' }); 65 | ``` 66 | 67 | #### Indirectly with FAS in-between 68 | 69 | You can run `crudfs` on top of Node.js `fs` module by using File System Access API 70 | adapter on top of the Node.js `fs` module: 71 | 72 | ```js 73 | import * as fs from 'fs'; 74 | import { nodeToFsa } from 'memfs/lib/node-to-fsa'; 75 | import { FsaCrud } from 'memfs/lib/fsa-to-crud'; 76 | 77 | const dir = nodeToFsa(fs, '/path/to/folder', { mode: 'readwrite' }); 78 | const crud = new FsaCrud(dir); 79 | ``` 80 | -------------------------------------------------------------------------------- /docs/fsa/fs-to-fsa.md: -------------------------------------------------------------------------------- 1 | # Node `fs` API to File System Access API 2 | 3 | `memfs` implements the web [File System Access (FSA) API][fsa] (formerly known as 4 | Native File System API) on top of Node's `fs`-like filesystem API. This means you 5 | can instantiate an FSA-compatible API on top of Node.js `fs` module, 6 | or on top of `memfs` [in-memory filesystem](../node/index.md), or on top of any 7 | other filesystem that implements Node's `fs` API. 8 | 9 | [fsa]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API 10 | 11 | ## Usage 12 | 13 | Crate a [`FileSystemDirectoryHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle) out 14 | of any folder on your filesystem: 15 | 16 | ```js 17 | import { nodeToFsa } from 'memfs/lib/node-to-fsa'; 18 | 19 | const dir = nodeToFsa(fs, '/path/to/folder', { mode: 'readwrite' }); 20 | ``` 21 | 22 | The `fs` Node filesystem API can be the real `fs` module or any `fs` like object, for example, 23 | an in-memory one provided by `memfs`. 24 | 25 | Now you can use the `dir` handle to execute all the File System Access API 26 | methods, for example, create a new file: 27 | 28 | ```js 29 | const file = await dir.getFileHandle('foo.txt', { create: true }); 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/fsa/fsa-to-fs.md: -------------------------------------------------------------------------------- 1 | # File System Access API to Node `fs` API 2 | 3 | This adapter implements Node's `fs`-like filesystem API on top of the web 4 | [File System Access (FSA) API][fsa]. 5 | 6 | This allows you to run Node.js code in browser, for example, run any Node.js 7 | package that uses `fs` module. 8 | 9 | [fsa]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API 10 | 11 | ## Usage 12 | 13 | You need to get hold of [`FileSystemDirectoryHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle) and then 14 | use convert it to `fs`-like filesystem. 15 | 16 | ```js 17 | import { FsaNodeFs } from 'memfs/lib/fsa-to-node'; 18 | 19 | const fs = new FsaNodeFs(dir); 20 | ``` 21 | 22 | Now you can use the `fs` filesystem API to execute any of the Node's `fs` methods. 23 | 24 | ```js 25 | await fs.promises.writeFile('/hello.txt', 'Hello World!'); 26 | ``` 27 | 28 | Out ouf the box most asynchronous API methods are supported, including callbacks API, 29 | promises API, write stream, and read stream. 30 | 31 | ## Synchronous API 32 | 33 | It is possible to use synchronous API, but it requires some extra setup. You need 34 | to setup a synchronous filesystem adapter for that. (See sync demo below.) 35 | 36 | ```js 37 | import { FsaNodeFs, FsaNodeSyncAdapterWorker } from 'memfs/lib/fsa-to-node'; 38 | 39 | const adapter = await FsaNodeSyncAdapterWorker.start('https:///worker.js', dir); 40 | const fs = new FsaNodeFs(dir, adapter); 41 | ``` 42 | 43 | Where `'https:///worker.js'` is a path to a worker file, which could look like this: 44 | 45 | ```js 46 | import { FsaNodeSyncWorker } from '../../src/fsa-to-node/worker/FsaNodeSyncWorker'; 47 | 48 | if (typeof window === 'undefined') { 49 | const worker = new FsaNodeSyncWorker(); 50 | worker.start(); 51 | } 52 | ``` 53 | 54 | You will also need to run your app through HTTPS and with [COI enabled](https://web.dev/cross-origin-isolation-guide/). 55 | Using Webpack dev server you can do it like this: 56 | 57 | ```js 58 | { 59 | devServer: { 60 | // HTTPS is required for Atomics and SharedArrayBuffer to work. 61 | https: true, 62 | headers: { 63 | // These two headers are required for Atomics and SharedArrayBuffer to work. 64 | "Cross-Origin-Opener-Policy": "same-origin", 65 | "Cross-Origin-Embedder-Policy": "require-corp", 66 | }, 67 | }, 68 | }, 69 | ``` 70 | 71 | Now most of the synchronous API should work, see the sync demo below. 72 | 73 | ## Demos 74 | 75 | - Async API and WriteStream: `yarn demo:fsa-to-node-zipfile` - [Readme](../../demo/fsa-to-node-zipfile/README.md) 76 | - Synchronous API: `yarn demo:fsa-to-node-sync-tests` - [Readme](../../demo/fsa-to-node-sync-tests/README.md) 77 | -------------------------------------------------------------------------------- /docs/node/dependencies.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | This package depends on the following Node modules: `buffer`, `events`, 4 | `streams`, `path`. 5 | 6 | It also uses `process` and `setImmediate` globals, but mocks them, if not 7 | available. 8 | -------------------------------------------------------------------------------- /docs/node/index.md: -------------------------------------------------------------------------------- 1 | # Node `fs` API in-memory implementation 2 | 3 | In-memory file-system with [Node's `fs` API](https://nodejs.org/api/fs.html). 4 | 5 | - Node's `fs` API implemented, see [missing list](https://github.com/streamich/memfs/issues/735) 6 | - Stores files in memory, in `Buffer`s 7 | - Throws sameish\* errors as Node.js 8 | - Has concept of _i-nodes_ 9 | - Implements _hard links_ 10 | - Implements _soft links_ (aka symlinks, symbolic links) 11 | - Can be used in browser, see `/demo` folder 12 | 13 | ## Docs 14 | 15 | - [Getting started](./usage.md) 16 | - [Reference](./reference.md) 17 | - [Relative paths](./relative-paths.md) 18 | - [Dependencies](./dependencies.md) 19 | 20 | [chat]: https://onp4.com/@vadim/~memfs 21 | [chat-badge]: https://img.shields.io/badge/Chat-%F0%9F%92%AC-green?style=flat&logo=chat&link=https://onp4.com/@vadim/~memfs 22 | [npm-url]: https://www.npmjs.com/package/memfs 23 | [npm-badge]: https://img.shields.io/npm/v/memfs.svg 24 | [travis-url]: https://travis-ci.org/streamich/memfs 25 | [travis-badge]: https://travis-ci.org/streamich/memfs.svg?branch=master 26 | [memfs]: https://github.com/streamich/memfs 27 | [unionfs]: https://github.com/streamich/unionfs 28 | [linkfs]: https://github.com/streamich/linkfs 29 | [spyfs]: https://github.com/streamich/spyfs 30 | [fs-monkey]: https://github.com/streamich/fs-monkey 31 | -------------------------------------------------------------------------------- /docs/node/relative-paths.md: -------------------------------------------------------------------------------- 1 | # Relative paths 2 | 3 | If you work with _absolute_ paths, you should get what you expect from `memfs`. 4 | 5 | You can also use _relative_ paths but the gotcha is that then `memfs` needs 6 | to somehow resolve those relative paths into absolute paths. `memfs` will use 7 | the value of `process.cwd()` to resolve the relative paths. The problem is 8 | that `process.cwd()` specifies the _current working directory_ of your 9 | on-disk filesystem and you will probably not have that directory available in your 10 | `memfs` volume. 11 | 12 | The best solution is to always use absolute paths. Alternatively, you can use 13 | `mkdir` method to recursively create the current working directory in your 14 | volume: 15 | 16 | ```js 17 | vol.mkdirSync(process.cwd(), { recursive: true }); 18 | ``` 19 | 20 | Or, you can set the current working directory to `/`, which 21 | is one folder that exists in all `memfs` volumes: 22 | 23 | ```js 24 | process.chdir('/'); 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/node/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | > See [demo](https://runkit.com/streamich/memfs-getting-started) on RunKit. 4 | 5 | ```js 6 | import { fs } from 'memfs'; 7 | 8 | fs.writeFileSync('/hello.txt', 'World!'); 9 | fs.readFileSync('/hello.txt', 'utf8'); // World! 10 | ``` 11 | 12 | Create a file system from a plain JSON: 13 | 14 | ```js 15 | import { fs, vol } from 'memfs'; 16 | 17 | const json = { 18 | './README.md': '1', 19 | './src/index.js': '2', 20 | './node_modules/debug/index.js': '3', 21 | }; 22 | vol.fromJSON(json, '/app'); 23 | 24 | fs.readFileSync('/app/README.md', 'utf8'); // 1 25 | vol.readFileSync('/app/src/index.js', 'utf8'); // 2 26 | ``` 27 | 28 | Export to JSON: 29 | 30 | ```js 31 | vol.writeFileSync('/script.sh', 'sudo rm -rf *'); 32 | vol.toJSON(); // {"/script.sh": "sudo rm -rf *"} 33 | ``` 34 | 35 | Use it for testing: 36 | 37 | ```js 38 | vol.writeFileSync('/foo', 'bar'); 39 | expect(vol.toJSON()).toEqual({ '/foo': 'bar' }); 40 | ``` 41 | 42 | Construct new `memfs` volumes: 43 | 44 | ```js 45 | import { memfs } from 'memfs'; 46 | 47 | const { fs, vol } = memfs({ '/foo': 'bar' }); 48 | 49 | fs.readFileSync('/foo', 'utf8'); // bar 50 | ``` 51 | 52 | Create as many filesystem volumes as you need: 53 | 54 | ```js 55 | import { Volume } from 'memfs'; 56 | 57 | const vol = Volume.fromJSON({ '/foo': 'bar' }); 58 | vol.readFileSync('/foo'); // bar 59 | 60 | const vol2 = Volume.fromJSON({ '/foo': 'bar 2' }); 61 | vol2.readFileSync('/foo'); // bar 2 62 | ``` 63 | 64 | Use `memfs` together with [`unionfs`][unionfs] to create one filesystem 65 | from your in-memory volumes and the real disk filesystem: 66 | 67 | [unionfs]: https://github.com/streamich/unionfs 68 | 69 | ```js 70 | import * as fs from 'fs'; 71 | import { ufs } from 'unionfs'; 72 | 73 | ufs.use(fs).use(vol); 74 | 75 | ufs.readFileSync('/foo'); // bar 76 | ``` 77 | 78 | Use [`fs-monkey`][fs-monkey] to monkey-patch Node's `require` function: 79 | 80 | [fs-monkey]: https://github.com/streamich/fs-monkey 81 | 82 | ```js 83 | import { patchRequire } from 'fs-monkey'; 84 | 85 | vol.writeFileSync('/index.js', 'console.log("hi world")'); 86 | patchRequire(vol); 87 | require('/index'); // hi world 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/print/index.md: -------------------------------------------------------------------------------- 1 | # `print` utility 2 | 3 | The print utility allows one to print an ASCII tree of some file system directory. 4 | You pass in the file system object and the folder path, and it will print out the tree. 5 | 6 | Here is the [`/src` folder print demo of this project](../../demo/print/fs.ts): 7 | 8 | ```ts 9 | import * as fs from 'fs'; 10 | import { toTreeSync } from 'memfs/lib/print'; 11 | 12 | console.log(toTreeSync(fs, { dir: process.cwd() + '/src/fsa-to-node' })); 13 | 14 | // Output: 15 | // src/ 16 | // ├─ Dirent.ts 17 | // ├─ Stats.ts 18 | // ├─ __tests__/ 19 | // │ ├─ hasBigInt.js 20 | // │ ├─ index.test.ts 21 | // │ ├─ node.test.ts 22 | // │ ├─ process.test.ts 23 | // │ ├─ promises.test.ts 24 | // ... 25 | ``` 26 | 27 | You can pass in any `fs` implementation, including the in-memory one from `memfs`. 28 | 29 | ```ts 30 | import { memfs } from 'memfs'; 31 | 32 | const { fs } = memfs({ 33 | '/Users/streamich/src/github/memfs/src': { 34 | 'package.json': '...', 35 | 'tsconfig.json': '...', 36 | }, 37 | }); 38 | 39 | console.log(toTreeSync(fs)); 40 | 41 | // / 42 | // └─ Users/ 43 | // └─ streamich/ 44 | // └─ src/ 45 | // └─ github/ 46 | // └─ memfs/ 47 | // └─ src/ 48 | // ├─ package.json 49 | // ├─ tsconfig.json 50 | ``` 51 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "lockFileMaintenance": { 4 | "enabled": true, 5 | "automerge": true, 6 | "schedule": ["before 3am on the first day of the month"] 7 | }, 8 | "rangeStrategy": "replace", 9 | "postUpdateOptions": ["yarnDedupeHighest"] 10 | } 11 | -------------------------------------------------------------------------------- /src/Dirent.ts: -------------------------------------------------------------------------------- 1 | import { Link } from './node'; 2 | import { constants } from './constants'; 3 | import { TEncodingExtended, strToEncoding, TDataOut } from './encoding'; 4 | import type { IDirent } from './node/types/misc'; 5 | 6 | const { S_IFMT, S_IFDIR, S_IFREG, S_IFBLK, S_IFCHR, S_IFLNK, S_IFIFO, S_IFSOCK } = constants; 7 | 8 | /** 9 | * A directory entry, like `fs.Dirent`. 10 | */ 11 | export class Dirent implements IDirent { 12 | static build(link: Link, encoding: TEncodingExtended | undefined) { 13 | const dirent = new Dirent(); 14 | const { mode } = link.getNode(); 15 | 16 | dirent.name = strToEncoding(link.getName(), encoding); 17 | dirent.mode = mode; 18 | dirent.path = link.getParentPath(); 19 | dirent.parentPath = dirent.path; 20 | 21 | return dirent; 22 | } 23 | 24 | name: TDataOut = ''; 25 | path = ''; 26 | parentPath = ''; 27 | private mode: number = 0; 28 | 29 | private _checkModeProperty(property: number): boolean { 30 | return (this.mode & S_IFMT) === property; 31 | } 32 | 33 | isDirectory(): boolean { 34 | return this._checkModeProperty(S_IFDIR); 35 | } 36 | 37 | isFile(): boolean { 38 | return this._checkModeProperty(S_IFREG); 39 | } 40 | 41 | isBlockDevice(): boolean { 42 | return this._checkModeProperty(S_IFBLK); 43 | } 44 | 45 | isCharacterDevice(): boolean { 46 | return this._checkModeProperty(S_IFCHR); 47 | } 48 | 49 | isSymbolicLink(): boolean { 50 | return this._checkModeProperty(S_IFLNK); 51 | } 52 | 53 | isFIFO(): boolean { 54 | return this._checkModeProperty(S_IFIFO); 55 | } 56 | 57 | isSocket(): boolean { 58 | return this._checkModeProperty(S_IFSOCK); 59 | } 60 | } 61 | 62 | export default Dirent; 63 | -------------------------------------------------------------------------------- /src/Stats.ts: -------------------------------------------------------------------------------- 1 | import { Node } from './node'; 2 | import { constants } from './constants'; 3 | 4 | const { S_IFMT, S_IFDIR, S_IFREG, S_IFBLK, S_IFCHR, S_IFLNK, S_IFIFO, S_IFSOCK } = constants; 5 | 6 | export type TStatNumber = number | bigint; 7 | 8 | /** 9 | * Statistics about a file/directory, like `fs.Stats`. 10 | */ 11 | export class Stats { 12 | static build(node: Node, bigint: false): Stats; 13 | static build(node: Node, bigint: true): Stats; 14 | static build(node: Node, bigint?: boolean): Stats; 15 | static build(node: Node, bigint: boolean = false): Stats { 16 | const stats = new Stats(); 17 | const { uid, gid, atime, mtime, ctime } = node; 18 | 19 | const getStatNumber = !bigint ? number => number : number => BigInt(number); 20 | 21 | // Copy all values on Stats from Node, so that if Node values 22 | // change, values on Stats would still be the old ones, 23 | // just like in Node fs. 24 | 25 | stats.uid = getStatNumber(uid); 26 | stats.gid = getStatNumber(gid); 27 | 28 | stats.rdev = getStatNumber(node.rdev); 29 | stats.blksize = getStatNumber(4096); 30 | stats.ino = getStatNumber(node.ino); 31 | stats.size = getStatNumber(node.getSize()); 32 | stats.blocks = getStatNumber(1); 33 | 34 | stats.atime = atime; 35 | stats.mtime = mtime; 36 | stats.ctime = ctime; 37 | stats.birthtime = ctime; 38 | 39 | stats.atimeMs = getStatNumber(atime.getTime()); 40 | stats.mtimeMs = getStatNumber(mtime.getTime()); 41 | const ctimeMs = getStatNumber(ctime.getTime()); 42 | stats.ctimeMs = ctimeMs; 43 | stats.birthtimeMs = ctimeMs; 44 | 45 | if (bigint) { 46 | stats.atimeNs = BigInt(atime.getTime()) * BigInt(1000000); 47 | stats.mtimeNs = BigInt(mtime.getTime()) * BigInt(1000000); 48 | const ctimeNs = BigInt(ctime.getTime()) * BigInt(1000000); 49 | stats.ctimeNs = ctimeNs; 50 | stats.birthtimeNs = ctimeNs; 51 | } 52 | 53 | stats.dev = getStatNumber(0); 54 | stats.mode = getStatNumber(node.mode); 55 | stats.nlink = getStatNumber(node.nlink); 56 | 57 | return stats; 58 | } 59 | 60 | uid: T; 61 | gid: T; 62 | 63 | rdev: T; 64 | blksize: T; 65 | ino: T; 66 | size: T; 67 | blocks: T; 68 | 69 | atime: Date; 70 | mtime: Date; 71 | ctime: Date; 72 | birthtime: Date; 73 | 74 | atimeMs: T; 75 | mtimeMs: T; 76 | ctimeMs: T; 77 | birthtimeMs: T; 78 | 79 | // additional properties that exist when bigint is true 80 | atimeNs: T extends bigint ? T : undefined; 81 | mtimeNs: T extends bigint ? T : undefined; 82 | ctimeNs: T extends bigint ? T : undefined; 83 | birthtimeNs: T extends bigint ? T : undefined; 84 | 85 | dev: T; 86 | mode: T; 87 | nlink: T; 88 | 89 | private _checkModeProperty(property: number): boolean { 90 | return (Number(this.mode) & S_IFMT) === property; 91 | } 92 | 93 | isDirectory(): boolean { 94 | return this._checkModeProperty(S_IFDIR); 95 | } 96 | 97 | isFile(): boolean { 98 | return this._checkModeProperty(S_IFREG); 99 | } 100 | 101 | isBlockDevice(): boolean { 102 | return this._checkModeProperty(S_IFBLK); 103 | } 104 | 105 | isCharacterDevice(): boolean { 106 | return this._checkModeProperty(S_IFCHR); 107 | } 108 | 109 | isSymbolicLink(): boolean { 110 | return this._checkModeProperty(S_IFLNK); 111 | } 112 | 113 | isFIFO(): boolean { 114 | return this._checkModeProperty(S_IFIFO); 115 | } 116 | 117 | isSocket(): boolean { 118 | return this._checkModeProperty(S_IFSOCK); 119 | } 120 | } 121 | 122 | export default Stats; 123 | -------------------------------------------------------------------------------- /src/__tests__/hasBigInt.js: -------------------------------------------------------------------------------- 1 | exports.default = typeof BigInt === 'function'; 2 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../volume'; 2 | import { constants } from '../constants'; 3 | import { fsCallbackApiList } from '../node/lists/fsCallbackApiList'; 4 | import { fsSynchronousApiList } from '../node/lists/fsSynchronousApiList'; 5 | 6 | const memfs = require('../index'); 7 | 8 | describe('memfs', () => { 9 | it('Exports Volume constructor', () => { 10 | expect(typeof memfs.Volume).toBe('function'); 11 | expect(memfs.Volume).toBe(Volume); 12 | }); 13 | 14 | it('Exports constants', () => { 15 | expect(memfs.F_OK).toBe(constants.F_OK); 16 | expect(memfs.R_OK).toBe(constants.R_OK); 17 | expect(memfs.W_OK).toBe(constants.W_OK); 18 | expect(memfs.X_OK).toBe(constants.X_OK); 19 | expect(memfs.constants).toEqual(constants); 20 | }); 21 | 22 | it('Exports constructors', () => { 23 | expect(typeof memfs.Stats).toBe('function'); 24 | expect(typeof memfs.Dirent).toBe('function'); 25 | expect(typeof memfs.ReadStream).toBe('function'); 26 | expect(typeof memfs.WriteStream).toBe('function'); 27 | expect(typeof memfs.FSWatcher).toBe('function'); 28 | expect(typeof memfs.StatWatcher).toBe('function'); 29 | }); 30 | 31 | it('Exports _toUnixTimestamp', () => { 32 | expect(typeof memfs._toUnixTimestamp).toBe('function'); 33 | }); 34 | 35 | it("Exports all Node's filesystem API methods", () => { 36 | for (const method of fsCallbackApiList) { 37 | expect(typeof memfs[method]).toBe('function'); 38 | } 39 | for (const method of fsSynchronousApiList) { 40 | expect(typeof memfs[method]).toBe('function'); 41 | } 42 | }); 43 | 44 | it('Exports promises API', () => { 45 | expect(typeof memfs.promises).toBe('object'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/__tests__/process.test.ts: -------------------------------------------------------------------------------- 1 | import _process, { createProcess } from '../process'; 2 | 3 | describe('process', () => { 4 | describe('createProcess', () => { 5 | const proc = createProcess(); 6 | it('Exports default object', () => { 7 | expect(typeof _process).toBe('object'); 8 | }); 9 | it('.cwd()', () => { 10 | expect(typeof proc.cwd()).toBe('string'); 11 | }); 12 | it('.env', () => { 13 | expect(typeof proc.env).toBe('object'); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__tests__/queueMicrotask.ts: -------------------------------------------------------------------------------- 1 | import queueMicrotask from '../queueMicrotask'; 2 | 3 | describe('queueMicrotask', () => { 4 | it('Is a function', () => { 5 | expect(typeof queueMicrotask).toBe('function'); 6 | }); 7 | it('Execute callback on next event loop cycle', done => { 8 | queueMicrotask(done); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/setTimeoutUnref.test.ts: -------------------------------------------------------------------------------- 1 | import setTimeoutUnref from '../setTimeoutUnref'; 2 | 3 | describe('setTimeoutUnref', () => { 4 | it('Executes callback', done => { 5 | setTimeoutUnref(done); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/__tests__/util.ts: -------------------------------------------------------------------------------- 1 | import { createFsFromVolume, Volume } from '..'; 2 | import { Link, Node } from '../node'; 3 | 4 | // Turn the done callback into an incremental one that will only fire after being called 5 | // `times` times, failing with the first reported error if such exists. 6 | // Useful for testing callback-style functions with several different fixtures without 7 | // having to clutter the test suite with a multitude of individual tests (like it.each would). 8 | export const multitest = (_done: (err?: Error) => void, times: number) => { 9 | let err; 10 | return function done(_err?: Error) { 11 | err ??= _err; 12 | if (!--times) _done(_err); 13 | }; 14 | }; 15 | 16 | export const create = (json: { [s: string]: string } = { '/foo': 'bar' }) => { 17 | const vol = Volume.fromJSON(json); 18 | return vol; 19 | }; 20 | 21 | export const createFs = (json?) => { 22 | return createFsFromVolume(create(json)); 23 | }; 24 | 25 | export const tryGetChild = (link: Link, name: string): Link => { 26 | const child = link.getChild(name); 27 | 28 | if (!child) { 29 | throw new Error(`expected link to have a child named "${name}"`); 30 | } 31 | 32 | return child; 33 | }; 34 | 35 | export const tryGetChildNode = (link: Link, name: string): Node => tryGetChild(link, name).getNode(); 36 | 37 | const nodeMajorVersion = +process.version.split('.')[0].slice(1); 38 | 39 | /** 40 | * The `File` global is available only starting in Node v20. Hence we run the 41 | * tests only in those versions. 42 | */ 43 | export const onlyOnNode20 = nodeMajorVersion >= 20 ? describe : describe.skip; 44 | -------------------------------------------------------------------------------- /src/__tests__/volume/FileHandle.test.ts: -------------------------------------------------------------------------------- 1 | import { fromStream } from '@jsonjoy.com/util/lib/streams/fromStream'; 2 | import { createFs } from '../util'; 3 | 4 | describe('FileHandle', () => { 5 | describe('.readableWebStream()', () => { 6 | it('can read contest of a file', async () => { 7 | const fs = createFs(); 8 | fs.writeFileSync('/foo', 'bar'); 9 | const handle = await fs.promises.open('/foo', 'r'); 10 | const stream = handle.readableWebStream(); 11 | expect(stream).toBeInstanceOf(ReadableStream); 12 | const data = fromStream(stream); 13 | expect(await data).toEqual(Buffer.from('bar')); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__tests__/volume/ReadStream.test.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '../util'; 2 | 3 | describe('ReadStream', () => { 4 | it('fs has ReadStream constructor', () => { 5 | const fs = createFs(); 6 | expect(typeof fs.ReadStream).toEqual('function'); 7 | }); 8 | it('ReadStream has constructor and prototype property', () => { 9 | const fs = createFs(); 10 | expect(typeof fs.ReadStream.constructor).toEqual('function'); 11 | expect(typeof fs.ReadStream.prototype).toEqual('object'); 12 | }); 13 | it('Can read basic file', done => { 14 | const fs = createFs({ '/a': 'b' }); 15 | const rs = new fs.ReadStream('/a', 'utf8'); 16 | rs.on('data', data => { 17 | expect(String(data)).toEqual('b'); 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should emit EACCES error when file has insufficient permissions', done => { 23 | const fs = createFs({ '/test': 'test' }); 24 | fs.chmodSync('/test', 0o333); // wx 25 | new fs.ReadStream('/test') 26 | .on('error', err => { 27 | expect(err).toBeInstanceOf(Error); 28 | expect(err).toHaveProperty('code', 'EACCES'); 29 | done(); 30 | }) 31 | .on('open', () => { 32 | done(new Error("Expected ReadStream to emit EACCES but it didn't")); 33 | }); 34 | }); 35 | 36 | it('should emit EACCES error when containing directory has insufficient permissions', done => { 37 | const fs = createFs({ '/foo/test': 'test' }); 38 | fs.chmodSync('/foo', 0o666); // rw 39 | new fs.ReadStream('/foo/test') 40 | .on('error', err => { 41 | expect(err).toBeInstanceOf(Error); 42 | expect(err).toHaveProperty('code', 'EACCES'); 43 | done(); 44 | }) 45 | .on('open', () => { 46 | done(new Error("Expected ReadStream to emit EACCES but it didn't")); 47 | }); 48 | }); 49 | 50 | it('should emit EACCES error when intermediate directory has insufficient permissions', done => { 51 | const fs = createFs({ '/foo/test': 'test' }); 52 | fs.chmodSync('/', 0o666); // rw 53 | new fs.ReadStream('/foo/test') 54 | .on('error', err => { 55 | expect(err).toBeInstanceOf(Error); 56 | expect(err).toHaveProperty('code', 'EACCES'); 57 | done(); 58 | }) 59 | .on('open', () => { 60 | done(new Error("Expected ReadStream to emit EACCES but it didn't")); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/__tests__/volume/WriteStream.test.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '../util'; 2 | 3 | describe('WriteStream', () => { 4 | it('fs has WriteStream constructor', () => { 5 | const fs = createFs(); 6 | expect(typeof fs.WriteStream).toBe('function'); 7 | }); 8 | it('WriteStream has constructor and prototype property', () => { 9 | const fs = createFs(); 10 | expect(typeof fs.WriteStream.constructor).toBe('function'); 11 | expect(typeof fs.WriteStream.prototype).toBe('object'); 12 | }); 13 | it('Can write basic file', done => { 14 | const fs = createFs({ '/a': 'b' }); 15 | const ws = new fs.WriteStream('/a', 'utf8'); 16 | ws.end('d'); 17 | ws.on('finish', () => { 18 | expect(fs.readFileSync('/a', 'utf8')).toBe('d'); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should emit EACCES error when file has insufficient permissions', done => { 24 | const fs = createFs({ '/test': 'test' }); 25 | fs.chmodSync('/test', 0o555); // rx 26 | new fs.WriteStream('/test') 27 | .on('error', err => { 28 | expect(err).toBeInstanceOf(Error); 29 | expect(err).toHaveProperty('code', 'EACCES'); 30 | done(); 31 | }) 32 | .on('open', () => { 33 | done(new Error("Expected WriteStream to emit EACCES but it didn't")); 34 | }); 35 | }); 36 | 37 | it('should emit EACCES error for an existing file when containing directory has insufficient permissions', done => { 38 | const fs = createFs({ '/foo/test': 'test' }); 39 | fs.chmodSync('/foo', 0o666); // rw 40 | new fs.WriteStream('/foo/test') 41 | .on('error', err => { 42 | expect(err).toBeInstanceOf(Error); 43 | expect(err).toHaveProperty('code', 'EACCES'); 44 | done(); 45 | }) 46 | .on('open', () => { 47 | done(new Error("Expected WriteStream to emit EACCES but it didn't")); 48 | }); 49 | }); 50 | 51 | it('should emit EACCES error for when intermediate directory has insufficient permissions', done => { 52 | const fs = createFs({ '/foo/test': 'test' }); 53 | fs.chmodSync('/', 0o666); // rw 54 | new fs.WriteStream('/foo/test') 55 | .on('error', err => { 56 | expect(err).toBeInstanceOf(Error); 57 | expect(err).toHaveProperty('code', 'EACCES'); 58 | done(); 59 | }) 60 | .on('open', () => { 61 | done(new Error("Expected WriteStream to emit EACCES but it didn't")); 62 | }); 63 | }); 64 | 65 | it('should emit EACCES error for a non-existent file when containing directory has insufficient permissions', done => { 66 | const fs = createFs({}); 67 | fs.mkdirSync('/foo', { mode: 0o555 }); // rx 68 | new fs.WriteStream('/foo/test') 69 | .on('error', err => { 70 | expect(err).toBeInstanceOf(Error); 71 | expect(err).toHaveProperty('code', 'EACCES'); 72 | done(); 73 | }) 74 | .on('open', () => { 75 | done(new Error("Expected WriteStream to emit EACCES but it didn't")); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/volume/__snapshots__/mkdirSync.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mkdirSync throws when creating root directory 1`] = `"EEXIST: file already exists, mkdir '/'"`; 4 | 5 | exports[`mkdirSync throws when re-creating existing directory 1`] = `"EEXIST: file already exists, mkdir '/new-dir'"`; 6 | -------------------------------------------------------------------------------- /src/__tests__/volume/__snapshots__/renameSync.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renameSync(fromPath, toPath) Throws if path is of wrong type 1`] = `"path must be a string, Buffer, or Uint8Array"`; 4 | 5 | exports[`renameSync(fromPath, toPath) Throws on no params 1`] = `"path must be a string, Buffer, or Uint8Array"`; 6 | 7 | exports[`renameSync(fromPath, toPath) Throws on only one param 1`] = `"path must be a string, Buffer, or Uint8Array"`; 8 | -------------------------------------------------------------------------------- /src/__tests__/volume/__snapshots__/writeSync.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`.writeSync(fd, buffer, offset, length, position) Write string to file 1`] = ` 4 | { 5 | "/foo": "test", 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/__tests__/volume/appendFile.test.ts: -------------------------------------------------------------------------------- 1 | import { create, multitest } from '../util'; 2 | 3 | describe('appendFile(file, data[, options], callback)', () => { 4 | it('Simple write to non-existing file', done => { 5 | const vol = create(); 6 | vol.appendFile('/test', 'hello', (err, res) => { 7 | expect(vol.readFileSync('/test', 'utf8')).toEqual('hello'); 8 | done(); 9 | }); 10 | }); 11 | it('Append to existing file', done => { 12 | const vol = create({ '/a': 'b' }); 13 | vol.appendFile('/a', 'c', (err, res) => { 14 | expect(vol.readFileSync('/a', 'utf8')).toEqual('bc'); 15 | done(); 16 | }); 17 | }); 18 | 19 | it('Appending gives EACCES without sufficient permissions on the file', done => { 20 | const vol = create({ '/foo': 'foo' }); 21 | vol.chmodSync('/foo', 0o555); // rx across the board 22 | vol.appendFile('/foo', 'bar', err => { 23 | try { 24 | expect(err).toBeInstanceOf(Error); 25 | expect(err).toHaveProperty('code', 'EACCES'); 26 | done(); 27 | } catch (failure) { 28 | done(failure); 29 | } 30 | }); 31 | }); 32 | 33 | it('Appending gives EACCES if file does not exist and containing directory has insufficient permissions', _done => { 34 | const perms = [ 35 | 0o555, // rx across the board 36 | 0o666, // rw across the board 37 | ]; 38 | const done = multitest(_done, perms.length); 39 | 40 | perms.forEach(perm => { 41 | const vol = create({}); 42 | vol.mkdirSync('/foo', { mode: perm }); 43 | vol.appendFile('/foo/test', 'bar', err => { 44 | try { 45 | expect(err).toBeInstanceOf(Error); 46 | expect(err).toHaveProperty('code', 'EACCES'); 47 | done(); 48 | } catch (failure) { 49 | done(failure); 50 | } 51 | }); 52 | }); 53 | }); 54 | 55 | it('Appending gives EACCES if intermediate directory has insufficient permissions', done => { 56 | const vol = create({}); 57 | vol.mkdirSync('/foo'); 58 | vol.chmodSync('/', 0o666); // rw 59 | vol.appendFile('/foo/test', 'bar', err => { 60 | try { 61 | expect(err).toBeInstanceOf(Error); 62 | expect(err).toHaveProperty('code', 'EACCES'); 63 | done(); 64 | } catch (failure) { 65 | done(failure); 66 | } 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/__tests__/volume/appendFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('appendFileSync(file, data, options)', () => { 4 | it('Simple write to non-existing file', () => { 5 | const vol = create(); 6 | vol.appendFileSync('/test', 'hello'); 7 | expect(vol.readFileSync('/test', 'utf8')).toEqual('hello'); 8 | }); 9 | it('Append to existing file', () => { 10 | const vol = create({ '/a': 'b' }); 11 | vol.appendFileSync('/a', 'c'); 12 | expect(vol.readFileSync('/a', 'utf8')).toEqual('bc'); 13 | }); 14 | it('Appending throws EACCES without sufficient permissions on the file', () => { 15 | const vol = create({ '/foo': 'foo' }); 16 | vol.chmodSync('/foo', 0o555); // rx across the board 17 | expect(() => { 18 | vol.appendFileSync('/foo', 'bar'); 19 | }).toThrowError(/EACCES/); 20 | }); 21 | it('Appending throws EACCES if file does not exist and containing directory has insufficient permissions', () => { 22 | const perms = [ 23 | 0o555, // rx across the board 24 | // 0o666, // rw across the board 25 | // 0o111, // x 26 | // 0o222 // w 27 | ]; 28 | perms.forEach(perm => { 29 | const vol = create({}); 30 | vol.mkdirSync('/foo', perm); 31 | expect(() => { 32 | vol.appendFileSync('/foo/test', 'bar'); 33 | }).toThrowError(/EACCES/); 34 | }); 35 | }); 36 | it('Appending throws EACCES if intermediate directory has insufficient permissions', () => { 37 | const vol = create({ '/foo/test': 'test' }); 38 | vol.chmodSync('/', 0o666); // rw 39 | expect(() => { 40 | vol.appendFileSync('/foo/test', 'bar'); 41 | }).toThrowError(/EACCES/); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/volume/chmodSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('chmodSync', () => { 4 | it('should be able to chmod files and directories owned by the UID regardless of their permissions', () => { 5 | const perms = [ 6 | 0o777, // rwx 7 | 0o666, // rw 8 | 0o555, // rx 9 | 0o444, // r 10 | 0o333, // wx 11 | 0o222, // w 12 | 0o111, // x 13 | 0o000, // none 14 | ]; 15 | // Check for directories 16 | perms.forEach(perm => { 17 | const vol = create({}); 18 | vol.mkdirSync('/foo', { mode: perm }); 19 | expect(() => { 20 | vol.chmodSync('/foo', 0o777); 21 | }).not.toThrow(); 22 | }); 23 | // Check for files 24 | perms.forEach(perm => { 25 | const vol = create({ '/foo': 'foo' }); 26 | expect(() => { 27 | vol.chmodSync('/foo', 0o777); 28 | }).not.toThrow(); 29 | }); 30 | }); 31 | 32 | it('should chmod the target of a symlink, not the symlink itself', () => { 33 | const vol = create({ '/target': 'contents' }); 34 | vol.symlinkSync('/target', '/link'); 35 | const expectedLink = vol.lstatSync('/link').mode; 36 | const expectedTarget = vol.statSync('/target').mode & ~0o777; 37 | vol.chmodSync('/link', 0); 38 | 39 | expect(vol.lstatSync('/link').mode).toEqual(expectedLink); 40 | expect(vol.statSync('/target').mode).toEqual(expectedTarget); 41 | }); 42 | 43 | it.skip('should throw EPERM when trying to chmod targets not owned by the uid', () => { 44 | const uid = process.getuid() + 1; 45 | // Check for directories 46 | const vol = create({}); 47 | vol.mkdirSync('/foo'); 48 | vol.chownSync('/foo', uid, process.getgid()); 49 | expect(() => { 50 | vol.chmodSync('/foo', 0o777); 51 | }).toThrow(/PERM/); 52 | }); 53 | 54 | it("should throw ENOENT when target doesn't exist", () => { 55 | const vol = create({}); 56 | expect(() => { 57 | vol.chmodSync('/foo', 0o777); 58 | }).toThrow(/ENOENT/); 59 | }); 60 | 61 | it('should throw EACCES when containing directory has insufficient permissions', () => { 62 | const vol = create({ '/foo/test': 'test' }); 63 | vol.chmodSync('/foo', 0o666); // rw 64 | expect(() => { 65 | vol.chmodSync('/foo/test', 0o777); 66 | }).toThrow(/EACCES/); 67 | }); 68 | 69 | it('should throw EACCES when intermediate directory has insufficient permissions', () => { 70 | const vol = create({ '/foo/test': 'test' }); 71 | vol.chmodSync('/', 0o666); // rw 72 | expect(() => { 73 | vol.chmodSync('/foo/test', 0o777); 74 | }).toThrow(/EACCES/); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/__tests__/volume/closeSync.test.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../..'; 2 | 3 | describe('.closeSync(fd)', () => { 4 | const vol = new Volume(); 5 | it('Closes file without errors', () => { 6 | const fd = vol.openSync('/test.txt', 'w'); 7 | vol.closeSync(fd); 8 | }); 9 | it('Correct error when file descriptor is not a number', () => { 10 | const vol = Volume.fromJSON({ '/foo': 'bar' }); 11 | try { 12 | const fd = vol.openSync('/foo', 'r'); 13 | vol.closeSync(String(fd) as any); 14 | throw Error('This should not throw'); 15 | } catch (err) { 16 | expect(err.message).toEqual('fd must be a file descriptor'); 17 | } 18 | }); 19 | it('Closing file descriptor that does not exist', () => { 20 | const vol = new Volume(); 21 | try { 22 | vol.closeSync(1234); 23 | throw Error('This should not throw'); 24 | } catch (err) { 25 | expect(err.code).toEqual('EBADF'); 26 | } 27 | }); 28 | it('Closing same file descriptor twice throws EBADF', () => { 29 | const fd = vol.openSync('/test.txt', 'w'); 30 | vol.closeSync(fd); 31 | try { 32 | vol.closeSync(fd); 33 | throw Error('This should not throw'); 34 | } catch (err) { 35 | expect(err.code).toEqual('EBADF'); 36 | } 37 | }); 38 | it('Closing a file decreases the number of open files', () => { 39 | const fd = vol.openSync('/test.txt', 'w'); 40 | const openFiles = vol.openFiles; 41 | vol.closeSync(fd); 42 | expect(openFiles).toBeGreaterThan(vol.openFiles); 43 | }); 44 | it('When closing a file, its descriptor is added to the pool of descriptors to be reused', () => { 45 | const fd = vol.openSync('/test.txt', 'w'); 46 | const usedFdLength = vol.releasedFds.length; 47 | vol.closeSync(fd); 48 | expect(usedFdLength).toBeLessThan(vol.releasedFds.length); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/__tests__/volume/exists.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('exists(path, callback)', () => { 4 | const vol = create(); 5 | it('Returns true if file exists', done => { 6 | vol.exists('/foo', exists => { 7 | expect(exists).toEqual(true); 8 | done(); 9 | }); 10 | }); 11 | it('Returns false if file does not exist', done => { 12 | vol.exists('/foo2', exists => { 13 | expect(exists).toEqual(false); 14 | done(); 15 | }); 16 | }); 17 | it('Throws correct error if callback not provided', done => { 18 | try { 19 | vol.exists('/foo', undefined as any); 20 | throw new Error('not_this'); 21 | } catch (err) { 22 | expect(err.message).toEqual('callback must be a function'); 23 | done(); 24 | } 25 | }); 26 | it('invalid path type should throw', () => { 27 | try { 28 | vol.exists(123 as any, () => {}); 29 | throw new Error('not_this'); 30 | } catch (err) { 31 | expect(err.message !== 'not_this').toEqual(true); 32 | } 33 | }); 34 | it('gives false if permissions on containing directory are insufficient', done => { 35 | // Experimentally determined: fs.exists treats missing permissions as "file does not exist", 36 | // presumably because due to the non-standard callback signature there is no way to signal 37 | // that permissions were insufficient 38 | const vol = create({ '/foo/bar': 'test' }); 39 | vol.chmodSync('/foo', 0o666); // rw across the board 40 | vol.exists('/foo/bar', exists => { 41 | try { 42 | expect(exists).toEqual(false); 43 | done(); 44 | } catch (failure) { 45 | done(failure); 46 | } 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/__tests__/volume/existsSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('existsSync(path)', () => { 4 | const vol = create(); 5 | it('Returns true if file exists', () => { 6 | const result = vol.existsSync('/foo'); 7 | expect(result).toEqual(true); 8 | }); 9 | it('Returns false if file does not exist', () => { 10 | const result = vol.existsSync('/foo2'); 11 | expect(result).toEqual(false); 12 | }); 13 | it('invalid path type should not throw', () => { 14 | expect(vol.existsSync(123 as any)).toEqual(false); 15 | }); 16 | it('returns false if permissions are insufficient on containing directory', () => { 17 | // Experimentally determined: fs.existsSync treats missing permissions as "file does not exist", 18 | // even though it could throw EACCES instead. 19 | // This is presumably to achieve unity of behavior with fs.exists. 20 | const vol = create({ '/foo/bar': 'test' }); 21 | vol.chmodSync('/foo', 0o666); // rw across the board 22 | expect(vol.existsSync('/foo/bar')).toEqual(false); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/__tests__/volume/lutimesSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('lutimesSync', () => { 4 | it('should be able to lutimes symlinks regardless of their permissions', () => { 5 | const perms = [ 6 | 0o777, // rwx 7 | 0o666, // rw 8 | 0o555, // rx 9 | 0o444, // r 10 | 0o333, // wx 11 | 0o222, // w 12 | 0o111, // x 13 | 0o000, // none 14 | ]; 15 | // Check for directories 16 | perms.forEach(perm => { 17 | const vol = create({ '/target': 'test' }); 18 | vol.symlinkSync('/target', '/test'); 19 | expect(() => { 20 | vol.lutimesSync('/test', 0, 0); 21 | }).not.toThrow(); 22 | }); 23 | }); 24 | 25 | it('should set atime and mtime on the link itself, not the target', () => { 26 | const vol = create({ '/target': 'test' }); 27 | vol.symlinkSync('/target', '/test'); 28 | vol.lutimesSync('/test', new Date(1), new Date(2)); 29 | const linkStats = vol.lstatSync('/test'); 30 | const targetStats = vol.statSync('/target'); 31 | 32 | expect(linkStats.atime).toEqual(new Date(1)); 33 | expect(linkStats.mtime).toEqual(new Date(2)); 34 | 35 | expect(targetStats.atime).not.toEqual(new Date(1)); 36 | expect(targetStats.mtime).not.toEqual(new Date(2)); 37 | }); 38 | 39 | it("should throw ENOENT when target doesn't exist", () => { 40 | const vol = create({ '/target': 'test' }); 41 | // Don't create symlink this time 42 | expect(() => { 43 | vol.lutimesSync('/test', 0, 0); 44 | }).toThrow(/ENOENT/); 45 | }); 46 | 47 | it('should throw EACCES when containing directory has insufficient permissions', () => { 48 | const vol = create({ '/target': 'test' }); 49 | vol.mkdirSync('/foo'); 50 | vol.symlinkSync('/target', '/foo/test'); 51 | vol.chmodSync('/foo', 0o666); // rw 52 | expect(() => { 53 | vol.lutimesSync('/foo/test', 0, 0); 54 | }).toThrow(/EACCES/); 55 | }); 56 | 57 | it('should throw EACCES when intermediate directory has insufficient permissions', () => { 58 | const vol = create({ '/target': 'test' }); 59 | vol.mkdirSync('/foo'); 60 | vol.symlinkSync('/target', '/foo/test'); 61 | vol.chmodSync('/', 0o666); // rw 62 | expect(() => { 63 | vol.lutimesSync('/foo/test', 0, 0); 64 | }).toThrow(/EACCES/); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/__tests__/volume/openSync.test.ts: -------------------------------------------------------------------------------- 1 | import { createFs } from '../util'; 2 | 3 | describe('openSync(path, mode[, flags])', () => { 4 | it('should return a file descriptor', () => { 5 | const fs = createFs(); 6 | const fd = fs.openSync('/foo', 'w'); 7 | expect(typeof fd).toEqual('number'); 8 | }); 9 | 10 | it('throws ENOTDIR when trying to create a non-existent file inside another file', () => { 11 | const fs = createFs(); 12 | 13 | expect(() => { 14 | fs.openSync('/foo/baz', 'a'); 15 | }).toThrow(/ENOTDIR/); 16 | }); 17 | 18 | describe('permissions', () => { 19 | it('opening for writing throws EACCES without sufficient permissions on the file', () => { 20 | const flags = ['a', 'w', 'r+']; // append, write, read+write 21 | flags.forEach(intent => { 22 | const fs = createFs(); 23 | fs.chmodSync('/foo', 0o555); // rx across the board 24 | expect(() => { 25 | fs.openSync('/foo', intent); 26 | }).toThrowError(/EACCES/); 27 | }); 28 | }); 29 | 30 | it('opening for reading throws EACCES without sufficient permissions on the file', () => { 31 | const flags = ['a+', 'r', 'w+']; // append+read, read, write+read 32 | flags.forEach(intent => { 33 | const fs = createFs(); 34 | fs.chmodSync('/foo', 0o333); // wx across the board 35 | expect(() => { 36 | fs.openSync('/foo', intent); 37 | }).toThrowError(/EACCES/); 38 | }); 39 | }); 40 | 41 | it('opening for anything throws EACCES without sufficient permissions on the containing directory of an existing file', () => { 42 | const flags = ['a+', 'r', 'w']; // append+read, read, write 43 | flags.forEach(intent => { 44 | const fs = createFs({ '/foo/bar': 'test' }); 45 | fs.chmodSync('/foo', 0o666); // wr across the board 46 | expect(() => { 47 | fs.openSync('/foo/bar', intent); 48 | }).toThrowError(/EACCES/); 49 | }); 50 | }); 51 | 52 | it('opening for anything throws EACCES without sufficient permissions on an intermediate directory', () => { 53 | const flags = ['a+', 'r', 'w']; // append+read, read, write 54 | flags.forEach(intent => { 55 | const fs = createFs({ '/foo/bar': 'test' }); 56 | fs.chmodSync('/', 0o666); // wr across the board 57 | expect(() => { 58 | fs.openSync('/foo/bar', intent); 59 | }).toThrowError(/EACCES/); 60 | }); 61 | }); 62 | 63 | it('opening for anything throws EACCES without sufficient permissions on the containing directory of an non-existent file', () => { 64 | const flags = ['a+', 'r', 'w']; // append+read, read, write 65 | flags.forEach(intent => { 66 | const fs = createFs({}); 67 | fs.mkdirSync('/foo', { mode: 0o666 }); // wr 68 | expect(() => { 69 | fs.openSync('/foo/bar', intent); 70 | }).toThrowError(/EACCES/); 71 | }); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/__tests__/volume/readFile.test.ts: -------------------------------------------------------------------------------- 1 | import { of } from '../../thingies'; 2 | import { memfs } from '../..'; 3 | 4 | describe('.readFile()', () => { 5 | it('can read a file', async () => { 6 | const { fs } = memfs({ '/dir/test.txt': '01234567' }); 7 | const data = await fs.promises.readFile('/dir/test.txt', { encoding: 'utf8' }); 8 | expect(data).toBe('01234567'); 9 | }); 10 | 11 | it('throws if file does not exist', async () => { 12 | const { fs } = memfs({ '/dir/test.txt': '01234567' }); 13 | const [, err] = await of(fs.promises.readFile('/dir/test-NOT-FOUND.txt', { encoding: 'utf8' })); 14 | expect(err).toBeInstanceOf(Error); 15 | expect((err).code).toBe('ENOENT'); 16 | }); 17 | 18 | it('throws EACCES if file has insufficient permissions', async () => { 19 | const { fs } = memfs({ '/foo': 'test' }); 20 | fs.chmodSync('/foo', 0o333); // wx 21 | return expect(fs.promises.readFile('/foo')).rejects.toThrow(/EACCES/); 22 | }); 23 | 24 | it('throws EACCES if containing directory has insufficient permissions', async () => { 25 | const { fs } = memfs({ '/foo/bar': 'test' }); 26 | fs.chmodSync('/foo', 0o666); // rw 27 | return expect(fs.promises.readFile('/foo/bar')).rejects.toThrow(/EACCES/); 28 | }); 29 | 30 | it('throws EACCES if intermediate directory has insufficient permissions', async () => { 31 | const { fs } = memfs({ '/foo/bar': 'test' }); 32 | fs.chmodSync('/', 0o666); // rw 33 | return expect(fs.promises.readFile('/foo/bar')).rejects.toThrow(/EACCES/); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/__tests__/volume/readSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('.readSync(fd, buffer, offset, length, position)', () => { 4 | it('Basic read file', () => { 5 | const vol = create({ '/test.txt': '01234567' }); 6 | const buf = Buffer.alloc(3, 0); 7 | const bytes = vol.readSync(vol.openSync('/test.txt', 'r'), buf, 0, 3, 3); 8 | expect(bytes).toBe(3); 9 | expect(buf.equals(Buffer.from('345'))).toBe(true); 10 | }); 11 | it('Attempt to read more than buffer space should throw ERR_OUT_OF_RANGE', () => { 12 | const vol = create({ '/test.txt': '01234567' }); 13 | const buf = Buffer.alloc(3, 0); 14 | const fn = () => vol.readSync(vol.openSync('/test.txt', 'r'), buf, 0, 10, 3); 15 | expect(fn).toThrow('ERR_OUT_OF_RANGE'); 16 | }); 17 | it('Read over file boundary', () => { 18 | const vol = create({ '/test.txt': '01234567' }); 19 | const buf = Buffer.alloc(3, 0); 20 | const bytes = vol.readSync(vol.openSync('/test.txt', 'r'), buf, 0, 3, 6); 21 | expect(bytes).toBe(2); 22 | expect(buf.equals(Buffer.from('67\0'))).toBe(true); 23 | }); 24 | it('Read multiple times, caret position should adjust', () => { 25 | const vol = create({ '/test.txt': '01234567' }); 26 | const buf = Buffer.alloc(3, 0); 27 | const fd = vol.openSync('/test.txt', 'r'); 28 | let bytes = vol.readSync(fd, buf, 0, 3, null); 29 | expect(bytes).toBe(3); 30 | expect(buf.equals(Buffer.from('012'))).toBe(true); 31 | bytes = vol.readSync(fd, buf, 0, 3, null); 32 | expect(bytes).toBe(3); 33 | expect(buf.equals(Buffer.from('345'))).toBe(true); 34 | bytes = vol.readSync(fd, buf, 0, 3, null); 35 | expect(bytes).toBe(2); 36 | expect(buf.equals(Buffer.from('675'))).toBe(true); 37 | bytes = vol.readSync(fd, buf, 0, 3, null); 38 | expect(bytes).toBe(0); 39 | expect(buf.equals(Buffer.from('675'))).toBe(true); 40 | }); 41 | xit('Negative tests', () => {}); 42 | 43 | /* 44 | * No need for permissions tests, because readSync requires a file descriptor, which can only be 45 | * obtained from open or openSync. 46 | */ 47 | }); 48 | -------------------------------------------------------------------------------- /src/__tests__/volume/realpathSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('.realpathSync(...)', () => { 4 | it('works with symlinks, #463', () => { 5 | const vol = create({}); 6 | vol.mkdirSync('/a'); 7 | vol.mkdirSync('/c'); 8 | vol.writeFileSync('/c/index.js', 'alert(123);'); 9 | vol.symlinkSync('/c', '/a/b'); 10 | 11 | const path = vol.realpathSync('/a/b/index.js'); 12 | expect(path).toBe('/c/index.js'); 13 | }); 14 | it('returns the root correctly', () => { 15 | const vol = create({ './a': 'a' }); 16 | expect(vol.realpathSync('/')).toBe('/'); 17 | }); 18 | it('throws EACCES when the containing directory does not have sufficient permissions', () => { 19 | const vol = create({ '/foo/bar': 'bar' }); 20 | vol.chmodSync('/foo', 0o666); // rw 21 | expect(() => { 22 | vol.realpathSync('/foo/bar'); 23 | }).toThrow(/EACCES/); 24 | }); 25 | 26 | it('throws EACCES when an intermediate directory does not have sufficient permissions', () => { 27 | const vol = create({ '/foo/bar': 'bar' }); 28 | vol.chmodSync('/', 0o666); // rw 29 | expect(() => { 30 | vol.realpathSync('/foo/bar'); 31 | }).toThrow(/EACCES/); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/__tests__/volume/rename.test.ts: -------------------------------------------------------------------------------- 1 | import { create, multitest } from '../util'; 2 | 3 | describe('renameSync(fromPath, toPath)', () => { 4 | it('Renames a simple case', done => { 5 | const vol = create({ '/foo': 'bar' }); 6 | vol.rename('/foo', '/foo2', (err, res) => { 7 | expect(vol.toJSON()).toEqual({ '/foo2': 'bar' }); 8 | done(); 9 | }); 10 | }); 11 | 12 | it('gives EACCES when source directory has insufficient permissions', _done => { 13 | const perms = [ 14 | 0o666, // rw 15 | 0o555, // rx - insufficient because the file will be removed from this directory during renaming 16 | ]; 17 | const done = multitest(_done, perms.length); 18 | perms.forEach(perm => { 19 | const vol = create({ '/src/test': 'test' }); 20 | vol.mkdirSync('/dest'); 21 | vol.chmodSync('/src', perm); 22 | vol.rename('/src/test', '/dest/fail', err => { 23 | try { 24 | expect(err).toBeInstanceOf(Error); 25 | expect(err).toHaveProperty('code', 'EACCES'); 26 | done(); 27 | } catch (failure) { 28 | done(failure); 29 | } 30 | }); 31 | }); 32 | }); 33 | 34 | it('gives EACCES when destination directory has insufficient permissions', _done => { 35 | const perms = [ 36 | 0o666, // rw 37 | 0o555, // rx 38 | ]; 39 | const done = multitest(_done, perms.length); 40 | perms.forEach(perm => { 41 | const vol = create({ '/src/test': 'test' }); 42 | vol.mkdirSync('/dest', { mode: perm }); 43 | vol.rename('/src/test', '/dest/fail', err => { 44 | try { 45 | expect(err).toBeInstanceOf(Error); 46 | expect(err).toHaveProperty('code', 'EACCES'); 47 | done(); 48 | } catch (failure) { 49 | done(failure); 50 | } 51 | }); 52 | }); 53 | }); 54 | 55 | it('gives EACCES when intermediate directory has insufficient permissions', done => { 56 | const vol = create({ '/src/test': 'test' }); 57 | vol.mkdirSync('/dest'); 58 | vol.chmodSync('/', 0o666); // rw 59 | vol.rename('/src/test', '/dest/fail', err => { 60 | try { 61 | expect(err).toBeInstanceOf(Error); 62 | expect(err).toHaveProperty('code', 'EACCES'); 63 | done(); 64 | } catch (failure) { 65 | done(failure); 66 | } 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/__tests__/volume/statSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('.statSync(...)', () => { 4 | it('works with symlinks, #463', () => { 5 | const vol = create({}); 6 | vol.mkdirSync('/a'); 7 | vol.mkdirSync('/c'); 8 | vol.writeFileSync('/c/index.js', 'alert(123);'); 9 | vol.symlinkSync('/c', '/a/b'); 10 | 11 | const stats = vol.statSync('/a/b/index.js'); 12 | expect(stats.size).toBe(11); 13 | }); 14 | 15 | it('returns rdev', () => { 16 | const vol = create({}); 17 | const fd = vol.openSync('/null', 'w'); 18 | vol.fds[fd].node.rdev = 1; 19 | const stats = vol.statSync('/null'); 20 | expect(stats.rdev).toBe(1); 21 | }); 22 | 23 | it('returns undefined for non-existent targets with the throwIfNoEntry option set to false', () => { 24 | const vol = create({}); 25 | 26 | const stats = vol.statSync('/non-existent', { throwIfNoEntry: false }); 27 | expect(stats).toBeUndefined(); 28 | }); 29 | 30 | it('throws EACCES when for a non-existent file when containing directory does not have sufficient permissions even if throwIfNoEntry option is false', () => { 31 | const vol = create({}); 32 | vol.mkdirSync('/foo', { mode: 0o666 }); // rw 33 | expect(() => { 34 | vol.statSync('/foo/non-existent', { throwIfNoEntry: false }); 35 | }).toThrowError(/EACCES/); 36 | }); 37 | 38 | it('throws EACCES when containing directory does not have sufficient permissions', () => { 39 | const vol = create({ '/foo/test': 'test' }); 40 | vol.chmodSync('/foo', 0o666); // rw 41 | 42 | expect(() => { 43 | vol.statSync('/foo/test'); 44 | }).toThrowError(/EACCES/); 45 | 46 | // Make sure permissions win out against throwIfNoEntry option: 47 | expect(() => { 48 | vol.statSync('/foo/test', { throwIfNoEntry: false }); 49 | }).toThrowError(/EACCES/); 50 | }); 51 | 52 | it('throws EACCES when intermediate directory does not have sufficient permissions', () => { 53 | const vol = create({ '/foo/test': 'test' }); 54 | vol.chmodSync('/', 0o666); // rw 55 | 56 | expect(() => { 57 | vol.statSync('/foo/test'); 58 | }).toThrowError(/EACCES/); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/__tests__/volume/toString.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('toString', () => { 4 | it('allow files to be named "toString", #463', () => { 5 | const vol = create({}); 6 | vol.writeFileSync('/toString', 'pwned'); 7 | 8 | expect(vol.readFileSync('/toString', 'utf8')).toBe('pwned'); 9 | expect(vol.toJSON()).toEqual({ 10 | '/toString': 'pwned', 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/volume/utimesSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | 3 | describe('utimesSync', () => { 4 | it('should be able to utimes files and directories regardless of their permissions', () => { 5 | const perms = [ 6 | 0o777, // rwx 7 | 0o666, // rw 8 | 0o555, // rx 9 | 0o444, // r 10 | 0o333, // wx 11 | 0o222, // w 12 | 0o111, // x 13 | 0o000, // none 14 | ]; 15 | // Check for directories 16 | perms.forEach(perm => { 17 | const vol = create({}); 18 | vol.mkdirSync('/foo', { mode: perm }); 19 | expect(() => { 20 | vol.utimesSync('/foo', 0, 0); 21 | }).not.toThrow(); 22 | }); 23 | // Check for files 24 | perms.forEach(perm => { 25 | const vol = create({ '/foo': 'foo' }); 26 | expect(() => { 27 | vol.utimesSync('/foo', 0, 0); 28 | }).not.toThrow(); 29 | }); 30 | }); 31 | 32 | it('should set atime and mtime on a file', () => { 33 | const vol = create({ '/foo/test': 'test' }); 34 | vol.utimesSync('/foo/test', new Date(1), new Date(2)); 35 | const { atime, mtime } = vol.statSync('/foo/test'); 36 | expect(atime).toEqual(new Date(1)); 37 | expect(mtime).toEqual(new Date(2)); 38 | }); 39 | 40 | it('should set atime and mtime on a directory', () => { 41 | const vol = create({ '/foo/test': 'test' }); 42 | vol.utimesSync('/foo', new Date(1), new Date(2)); 43 | const { atime, mtime } = vol.statSync('/foo'); 44 | expect(atime).toEqual(new Date(1)); 45 | expect(mtime).toEqual(new Date(2)); 46 | }); 47 | 48 | it("should throw ENOENT when target doesn't exist", () => { 49 | const vol = create({}); 50 | expect(() => { 51 | vol.utimesSync('/foo', 0, 0); 52 | }).toThrow(/ENOENT/); 53 | }); 54 | 55 | it('should throw EACCES when containing directory has insufficient permissions', () => { 56 | const vol = create({ '/foo/test': 'test' }); 57 | vol.chmodSync('/foo', 0o666); // rw 58 | expect(() => { 59 | vol.utimesSync('/foo/test', 0, 0); 60 | }).toThrow(/EACCES/); 61 | }); 62 | 63 | it('should throw EACCES when intermediate directory has insufficient permissions', () => { 64 | const vol = create({ '/foo/test': 'test' }); 65 | vol.chmodSync('/', 0o666); // rw 66 | expect(() => { 67 | vol.utimesSync('/foo/test', 0, 0); 68 | }).toThrow(/EACCES/); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/__tests__/volume/write.test.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from '../..'; 2 | 3 | const create = (json = { '/foo': 'bar' }) => { 4 | const vol = Volume.fromJSON(json); 5 | return vol; 6 | }; 7 | 8 | describe('write(fs, str, position, encoding, callback)', () => { 9 | it('Simple write to file', done => { 10 | const vol = create(); 11 | const fd = vol.openSync('/test', 'w'); 12 | vol.write(fd, 'lol', 0, 'utf8', (err, bytes, str) => { 13 | expect(err).toEqual(null); 14 | expect(bytes).toEqual(3); 15 | expect(str).toEqual('lol'); 16 | expect(vol.readFileSync('/test', 'utf8')).toEqual('lol'); 17 | done(); 18 | }); 19 | }); 20 | 21 | /* 22 | * No need for permissions tests, because write requires a file descriptor, which can only be 23 | * obtained from open or openSync. 24 | */ 25 | }); 26 | -------------------------------------------------------------------------------- /src/__tests__/volume/writeFileSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create, tryGetChildNode } from '../util'; 2 | import { Node } from '../../node'; 3 | 4 | describe('writeFileSync(path, data[, options])', () => { 5 | const data = 'asdfasidofjasdf'; 6 | it('Create a file at root (/writeFileSync.txt)', () => { 7 | const vol = create(); 8 | vol.writeFileSync('/writeFileSync.txt', data); 9 | 10 | const node = tryGetChildNode(vol.root, 'writeFileSync.txt'); 11 | expect(node).toBeInstanceOf(Node); 12 | expect(node.getString()).toBe(data); 13 | }); 14 | it('Write to file by file descriptor', () => { 15 | const vol = create(); 16 | const fd = vol.openSync('/writeByFd.txt', 'w'); 17 | vol.writeFileSync(fd, data); 18 | const node = tryGetChildNode(vol.root, 'writeByFd.txt'); 19 | expect(node).toBeInstanceOf(Node); 20 | expect(node.getString()).toBe(data); 21 | }); 22 | it('Write to two files (second by fd)', () => { 23 | const vol = create(); 24 | 25 | // 1 26 | vol.writeFileSync('/1.txt', '123'); 27 | 28 | // 2, 3, 4 29 | const fd2 = vol.openSync('/2.txt', 'w'); 30 | const fd3 = vol.openSync('/3.txt', 'w'); 31 | const fd4 = vol.openSync('/4.txt', 'w'); 32 | 33 | vol.writeFileSync(fd2, '456'); 34 | 35 | expect(tryGetChildNode(vol.root, '1.txt').getString()).toBe('123'); 36 | expect(tryGetChildNode(vol.root, '2.txt').getString()).toBe('456'); 37 | }); 38 | it('Write at relative path that does not exist throws correct error', () => { 39 | const vol = create(); 40 | try { 41 | vol.writeFileSync('a/b', 'c'); 42 | throw new Error('not_this'); 43 | } catch (err) { 44 | expect(err.code).toBe('ENOENT'); 45 | } 46 | }); 47 | 48 | it('Write throws EACCES if file exists but has insufficient permissions', () => { 49 | const vol = create({ '/foo/test': 'test' }); 50 | vol.chmodSync('/foo/test', 0o555); // rx 51 | expect(() => { 52 | vol.writeFileSync('/foo/test', 'test'); 53 | }).toThrowError(/EACCES/); 54 | }); 55 | 56 | it('Write throws EACCES without sufficient permissions on containing directory', () => { 57 | const perms = [ 58 | 0o666, // rw 59 | 0o555, // rx, only when target file does not exist yet 60 | ]; 61 | perms.forEach(perm => { 62 | const vol = create({}); 63 | vol.mkdirSync('/foo'); 64 | vol.chmodSync('/foo', perm); 65 | expect(() => { 66 | vol.writeFileSync('/foo/test', 'test'); 67 | }).toThrowError(/EACCES/); 68 | }); 69 | 70 | // If the target file exists, it should not care about the write permission on containing dir 71 | const vol = create({ '/foo/test': 'test' }); 72 | vol.chmodSync('/foo', 0o555); // rx, should be enough 73 | expect(() => { 74 | vol.writeFileSync('/foo/test', 'test'); 75 | }).not.toThrowError(); 76 | }); 77 | 78 | it('Write throws EACCES without sufficient permissions on intermediate directory', () => { 79 | const vol = create({}); 80 | vol.mkdirSync('/foo'); 81 | vol.chmodSync('/', 0o666); // rw 82 | expect(() => { 83 | vol.writeFileSync('/foo/test', 'test'); 84 | }).toThrowError(/EACCES/); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/__tests__/volume/writeSync.test.ts: -------------------------------------------------------------------------------- 1 | import { create } from '../util'; 2 | import { memfs } from '../..'; 3 | 4 | describe('.writeSync(fd, buffer, offset, length, position)', () => { 5 | it('Write binary data to file', () => { 6 | const vol = create({}); 7 | const fd = vol.openSync('/data.bin', 'w+'); 8 | const bytes = vol.writeSync(fd, Buffer.from([1, 2, 3])); 9 | vol.closeSync(fd); 10 | expect(bytes).toBe(3); 11 | expect(Buffer.from([1, 2, 3]).equals(vol.readFileSync('/data.bin') as Buffer)).toBe(true); 12 | }); 13 | 14 | it('Write string to file', () => { 15 | const vol = create({}); 16 | const fd = vol.openSync('/foo', 'w'); 17 | vol.writeSync(fd, 'test'); 18 | expect(vol.toJSON()).toMatchSnapshot(); 19 | }); 20 | 21 | it('can write at offset', () => { 22 | const { fs } = memfs({ foo: '123' }); 23 | const fd = fs.openSync('/foo', 'a+'); 24 | expect(fs.readFileSync('/foo', 'utf8')).toBe('123'); 25 | fs.writeSync(fd, 'x', 1); 26 | expect(fs.readFileSync('/foo', 'utf8')).toBe('1x3'); 27 | }); 28 | 29 | /* 30 | * No need for permissions tests, because write requires a file descriptor, which can only be 31 | * obtained from open or openSync. 32 | */ 33 | }); 34 | -------------------------------------------------------------------------------- /src/cas/README.md: -------------------------------------------------------------------------------- 1 | `casfs` is a Content Addressable Storage (CAS) abstraction over a file system. 2 | It has no folders nor files. Instead, it has _blobs_ which are identified by their content. 3 | 4 | Essentially, it provides two main operations: `put` and `get`. The `put` operation 5 | takes a blob and stores it in the underlying file system and returns the blob's hash digest. 6 | The `get` operation takes a hash and returns the blob, which matches the hash digest, if it exists. 7 | -------------------------------------------------------------------------------- /src/cas/types.ts: -------------------------------------------------------------------------------- 1 | import type { CrudResourceInfo } from '../crud/types'; 2 | 3 | export interface CasApi { 4 | put(blob: Uint8Array): Promise; 5 | get(hash: Hash, options?: CasGetOptions): Promise; 6 | del(hash: Hash, silent?: boolean): Promise; 7 | info(hash: Hash): Promise; 8 | } 9 | 10 | export interface CasGetOptions { 11 | skipVerification?: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const constants = { 2 | O_RDONLY: 0, 3 | O_WRONLY: 1, 4 | O_RDWR: 2, 5 | S_IFMT: 61440, 6 | S_IFREG: 32768, 7 | S_IFDIR: 16384, 8 | S_IFCHR: 8192, 9 | S_IFBLK: 24576, 10 | S_IFIFO: 4096, 11 | S_IFLNK: 40960, 12 | S_IFSOCK: 49152, 13 | O_CREAT: 64, 14 | O_EXCL: 128, 15 | O_NOCTTY: 256, 16 | O_TRUNC: 512, 17 | O_APPEND: 1024, 18 | O_DIRECTORY: 65536, 19 | O_NOATIME: 262144, 20 | O_NOFOLLOW: 131072, 21 | O_SYNC: 1052672, 22 | O_SYMLINK: 2097152, 23 | O_DIRECT: 16384, 24 | O_NONBLOCK: 2048, 25 | S_IRWXU: 448, 26 | S_IRUSR: 256, 27 | S_IWUSR: 128, 28 | S_IXUSR: 64, 29 | S_IRWXG: 56, 30 | S_IRGRP: 32, 31 | S_IWGRP: 16, 32 | S_IXGRP: 8, 33 | S_IRWXO: 7, 34 | S_IROTH: 4, 35 | S_IWOTH: 2, 36 | S_IXOTH: 1, 37 | 38 | F_OK: 0, 39 | R_OK: 4, 40 | W_OK: 2, 41 | X_OK: 1, 42 | 43 | UV_FS_SYMLINK_DIR: 1, 44 | UV_FS_SYMLINK_JUNCTION: 2, 45 | 46 | UV_FS_COPYFILE_EXCL: 1, 47 | UV_FS_COPYFILE_FICLONE: 2, 48 | UV_FS_COPYFILE_FICLONE_FORCE: 4, 49 | COPYFILE_EXCL: 1, 50 | COPYFILE_FICLONE: 2, 51 | COPYFILE_FICLONE_FORCE: 4, 52 | }; 53 | 54 | export const enum S { 55 | ISUID = 0b100000000000, // (04000) set-user-ID (set process effective user ID on execve(2)) 56 | ISGID = 0b10000000000, // (02000) set-group-ID (set process effective group ID on execve(2); mandatory locking, as described in fcntl(2); take a new file's group from parent directory, as described in chown(2) and mkdir(2)) 57 | ISVTX = 0b1000000000, // (01000) sticky bit (restricted deletion flag, as described in unlink(2)) 58 | IRUSR = 0b100000000, // (00400) read by owner 59 | IWUSR = 0b10000000, // (00200) write by owner 60 | IXUSR = 0b1000000, // (00100) execute/search by owner 61 | IRGRP = 0b100000, // (00040) read by group 62 | IWGRP = 0b10000, // (00020) write by group 63 | IXGRP = 0b1000, // (00010) execute/search by group 64 | IROTH = 0b100, // (00004) read by others 65 | IWOTH = 0b10, // (00002) write by others 66 | IXOTH = 0b1, // (00001) execute/search by others 67 | } 68 | -------------------------------------------------------------------------------- /src/consts/AMODE.ts: -------------------------------------------------------------------------------- 1 | // Constants used in `access` system call, see [access(2)](http://man7.org/linux/man-pages/man2/faccessat.2.html). 2 | export const enum AMODE { 3 | /* Tests for the existence of the file. */ 4 | F_OK = 0, 5 | /** Tests for Execute or Search permissions. */ 6 | X_OK = 1, 7 | /** Tests for Write permission. */ 8 | W_OK = 2, 9 | /** Tests for Read permission. */ 10 | R_OK = 4, 11 | } 12 | -------------------------------------------------------------------------------- /src/consts/FLAG.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants used in `open` system calls, see [open(2)](http://man7.org/linux/man-pages/man2/open.2.html). 3 | * 4 | * @see http://man7.org/linux/man-pages/man2/open.2.html 5 | * @see https://www.gnu.org/software/libc/manual/html_node/Open_002dtime-Flags.html 6 | */ 7 | export const enum FLAG { 8 | O_RDONLY = 0, 9 | O_WRONLY = 1, 10 | O_RDWR = 2, 11 | O_ACCMODE = 3, 12 | O_CREAT = 64, 13 | O_EXCL = 128, 14 | O_NOCTTY = 256, 15 | O_TRUNC = 512, 16 | O_APPEND = 1024, 17 | O_NONBLOCK = 2048, 18 | O_DSYNC = 4096, 19 | FASYNC = 8192, 20 | O_DIRECT = 16384, 21 | O_LARGEFILE = 0, 22 | O_DIRECTORY = 65536, 23 | O_NOFOLLOW = 131072, 24 | O_NOATIME = 262144, 25 | O_CLOEXEC = 524288, 26 | O_SYNC = 1052672, 27 | O_NDELAY = 2048, 28 | } 29 | -------------------------------------------------------------------------------- /src/crud-to-cas/CrudCas.ts: -------------------------------------------------------------------------------- 1 | import { hashToLocation } from './util'; 2 | import { CrudCasBase } from './CrudCasBase'; 3 | import type { CrudApi } from '../crud/types'; 4 | 5 | export interface CrudCasOptions { 6 | hash: (blob: Uint8Array) => Promise; 7 | } 8 | 9 | const hashEqual = (h1: string, h2: string) => h1 === h2; 10 | 11 | export class CrudCas extends CrudCasBase { 12 | constructor( 13 | protected readonly crud: CrudApi, 14 | protected readonly options: CrudCasOptions, 15 | ) { 16 | super(crud, options.hash, hashToLocation, hashEqual); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/crud-to-cas/CrudCasBase.ts: -------------------------------------------------------------------------------- 1 | import type { CasApi, CasGetOptions } from '../cas/types'; 2 | import type { CrudApi, CrudResourceInfo } from '../crud/types'; 3 | import type { FsLocation } from '../fsa-to-node/types'; 4 | 5 | const normalizeErrors = async (code: () => Promise): Promise => { 6 | try { 7 | return await code(); 8 | } catch (error) { 9 | if (error && typeof error === 'object') { 10 | switch (error.name) { 11 | case 'ResourceNotFound': 12 | case 'CollectionNotFound': 13 | throw new DOMException(error.message, 'BlobNotFound'); 14 | } 15 | } 16 | throw error; 17 | } 18 | }; 19 | 20 | export class CrudCasBase implements CasApi { 21 | constructor( 22 | protected readonly crud: CrudApi, 23 | protected readonly hash: (blob: Uint8Array) => Promise, 24 | protected readonly hash2Loc: (hash: Hash) => FsLocation, 25 | protected readonly hashEqual: (h1: Hash, h2: Hash) => boolean, 26 | ) {} 27 | 28 | public readonly put = async (blob: Uint8Array): Promise => { 29 | const digest = await this.hash(blob); 30 | const [collection, resource] = this.hash2Loc(digest); 31 | await this.crud.put(collection, resource, blob); 32 | return digest; 33 | }; 34 | 35 | public readonly get = async (hash: Hash, options?: CasGetOptions): Promise => { 36 | const [collection, resource] = this.hash2Loc(hash); 37 | return await normalizeErrors(async () => { 38 | const blob = await this.crud.get(collection, resource); 39 | if (!options?.skipVerification) { 40 | const digest = await this.hash(blob); 41 | if (!this.hashEqual(digest, hash)) throw new DOMException('Blob contents does not match hash', 'Integrity'); 42 | } 43 | return blob; 44 | }); 45 | }; 46 | 47 | public readonly del = async (hash: Hash, silent?: boolean): Promise => { 48 | const [collection, resource] = this.hash2Loc(hash); 49 | await normalizeErrors(async () => { 50 | return await this.crud.del(collection, resource, silent); 51 | }); 52 | }; 53 | 54 | public readonly info = async (hash: Hash): Promise => { 55 | const [collection, resource] = this.hash2Loc(hash); 56 | return await normalizeErrors(async () => { 57 | return await this.crud.info(collection, resource); 58 | }); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/crud-to-cas/__tests__/CrudCas.test.ts: -------------------------------------------------------------------------------- 1 | import { memfs } from '../..'; 2 | import { onlyOnNode20 } from '../../__tests__/util'; 3 | import { NodeFileSystemDirectoryHandle } from '../../node-to-fsa'; 4 | import { FsaCrud } from '../../fsa-to-crud/FsaCrud'; 5 | import { CrudCas } from '../CrudCas'; 6 | import { testCasfs, hash } from './testCasfs'; 7 | import { NodeCrud } from '../../node-to-crud/NodeCrud'; 8 | 9 | onlyOnNode20('CrudCas on FsaCrud', () => { 10 | const setup = () => { 11 | const { fs } = memfs(); 12 | const fsa = new NodeFileSystemDirectoryHandle(fs, '/', { mode: 'readwrite' }); 13 | const crud = new FsaCrud(fsa); 14 | const cas = new CrudCas(crud, { hash }); 15 | return { fs, fsa, crud, cas, snapshot: () => (fs).__vol.toJSON() }; 16 | }; 17 | testCasfs(setup); 18 | }); 19 | 20 | onlyOnNode20('CrudCas on NodeCrud at root', () => { 21 | const setup = () => { 22 | const { fs } = memfs(); 23 | const crud = new NodeCrud({ fs: fs.promises, dir: '/' }); 24 | const cas = new CrudCas(crud, { hash }); 25 | return { fs, crud, cas, snapshot: () => (fs).__vol.toJSON() }; 26 | }; 27 | testCasfs(setup); 28 | }); 29 | 30 | onlyOnNode20('CrudCas on NodeCrud at in sub-folder', () => { 31 | const setup = () => { 32 | const { fs } = memfs({ '/a/b/c': null }); 33 | const crud = new NodeCrud({ fs: fs.promises, dir: '/a/b/c' }); 34 | const cas = new CrudCas(crud, { hash }); 35 | return { fs, crud, cas, snapshot: () => (fs).__vol.toJSON() }; 36 | }; 37 | testCasfs(setup); 38 | }); 39 | -------------------------------------------------------------------------------- /src/crud-to-cas/__tests__/CrudCasBase.test.ts: -------------------------------------------------------------------------------- 1 | import { memfs } from '../..'; 2 | import { onlyOnNode20 } from '../../__tests__/util'; 3 | import { NodeFileSystemDirectoryHandle } from '../../node-to-fsa'; 4 | import { FsaCrud } from '../../fsa-to-crud/FsaCrud'; 5 | import { createHash } from 'crypto'; 6 | import { hashToLocation } from '../util'; 7 | import { CrudCasBase } from '../CrudCasBase'; 8 | import { FsLocation } from '../../fsa-to-node/types'; 9 | 10 | onlyOnNode20('CrudCas on FsaCrud', () => { 11 | const setup = () => { 12 | const { fs } = memfs(); 13 | const fsa = new NodeFileSystemDirectoryHandle(fs, '/', { mode: 'readwrite' }); 14 | const crud = new FsaCrud(fsa); 15 | return { fs, fsa, crud, snapshot: () => (fs).__vol.toJSON() }; 16 | }; 17 | 18 | test('can use a custom hashing digest type', async () => { 19 | const { crud } = setup(); 20 | class Hash { 21 | constructor(public readonly digest: string) {} 22 | } 23 | const hash = async (blob: Uint8Array): Promise => { 24 | const shasum = createHash('sha1'); 25 | shasum.update(blob); 26 | const digest = shasum.digest('hex'); 27 | return new Hash(digest); 28 | }; 29 | const cas = new CrudCasBase( 30 | crud, 31 | hash, 32 | (id: Hash) => hashToLocation(id.digest), 33 | (h1: Hash, h2: Hash) => h1.digest === h2.digest, 34 | ); 35 | const blob = Buffer.from('hello world'); 36 | const id = await cas.put(blob); 37 | expect(id).toBeInstanceOf(Hash); 38 | const id2 = await hash(blob); 39 | expect(id.digest).toEqual(id2.digest); 40 | const blob2 = await cas.get(id); 41 | expect(String.fromCharCode(...blob2)).toEqual('hello world'); 42 | expect(await cas.info(id)).toMatchObject({ size: 11 }); 43 | await cas.del(id2); 44 | expect(() => cas.info(id)).rejects.toThrowError(); 45 | }); 46 | 47 | test('can use custom folder sharding strategy', async () => { 48 | const { crud } = setup(); 49 | const hash = async (blob: Uint8Array): Promise => { 50 | const shasum = createHash('sha1'); 51 | shasum.update(blob); 52 | return shasum.digest('hex'); 53 | }; 54 | const theCustomFolderShardingStrategy = (h: string): FsLocation => [[h[0], h[1], h[2]], h[3]]; 55 | const cas = new CrudCasBase( 56 | crud, 57 | hash, 58 | theCustomFolderShardingStrategy, 59 | (h1: string, h2: string) => h1 === h2, 60 | ); 61 | const blob = Buffer.from('hello world'); 62 | const id = await cas.put(blob); 63 | expect(typeof id).toBe('string'); 64 | const id2 = await hash(blob); 65 | expect(id).toBe(id2); 66 | const blob2 = await cas.get(id); 67 | expect(String.fromCharCode(...blob2)).toEqual('hello world'); 68 | const blob3 = await crud.get([id2[0], id2[1], id2[2]], id2[3]); 69 | expect(String.fromCharCode(...blob3)).toEqual('hello world'); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/crud-to-cas/__tests__/__snapshots__/CrudCas.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CrudCas on FsaCrud .put() can store a blob 1`] = ` 4 | { 5 | "/ed/46/2aae6c35c94fcfb415dbe95f408b9ce91ee846ed": "hello world", 6 | } 7 | `; 8 | 9 | exports[`CrudCas on NodeCrud at in sub-folder .put() can store a blob 1`] = ` 10 | { 11 | "/a/b/c/ed/46/2aae6c35c94fcfb415dbe95f408b9ce91ee846ed": "hello world", 12 | } 13 | `; 14 | 15 | exports[`CrudCas on NodeCrud at root .put() can store a blob 1`] = ` 16 | { 17 | "/ed/46/2aae6c35c94fcfb415dbe95f408b9ce91ee846ed": "hello world", 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/crud-to-cas/index.ts: -------------------------------------------------------------------------------- 1 | export { CrudCas, CrudCasOptions } from './CrudCas'; 2 | -------------------------------------------------------------------------------- /src/crud-to-cas/util.ts: -------------------------------------------------------------------------------- 1 | import type { FsLocation } from '../fsa-to-node/types'; 2 | 3 | export const hashToLocation = (hash: string): FsLocation => { 4 | if (hash.length < 20) throw new TypeError('Hash is too short'); 5 | const lastTwo = hash.slice(-2); 6 | const twoBeforeLastTwo = hash.slice(-4, -2); 7 | const folder = [lastTwo, twoBeforeLastTwo]; 8 | return [folder, hash]; 9 | }; 10 | -------------------------------------------------------------------------------- /src/crud/README.md: -------------------------------------------------------------------------------- 1 | `crudfs` is a files system abstraction which provides CRUD-like API on top of a 2 | file system. It is intended to be light (so can be efficiently bundled for browser), 3 | have small API surface but cover most of the useful file manipulation scenarios. 4 | 5 | Folder are referred to as _collections_ or _types_; and files are referred to as _resources_. 6 | -------------------------------------------------------------------------------- /src/crud/__tests__/matryoshka.test.ts: -------------------------------------------------------------------------------- 1 | import { memfs } from '../..'; 2 | import { onlyOnNode20 } from '../../__tests__/util'; 3 | import { NodeFileSystemDirectoryHandle } from '../../node-to-fsa'; 4 | import { FsaNodeFs } from '../../fsa-to-node'; 5 | import { NodeCrud } from '../../node-to-crud'; 6 | import { testCrudfs } from '../../crud/__tests__/testCrudfs'; 7 | import { FsaCrud } from '../../fsa-to-crud'; 8 | 9 | onlyOnNode20('CRUD matryoshka', () => { 10 | describe('crud(memfs)', () => { 11 | testCrudfs(() => { 12 | const { fs } = memfs(); 13 | const crud = new NodeCrud({ fs: fs.promises, dir: '/' }); 14 | return { crud, snapshot: () => (fs).__vol.toJSON() }; 15 | }); 16 | }); 17 | 18 | describe('crud(fsa(memfs))', () => { 19 | testCrudfs(() => { 20 | const { fs } = memfs(); 21 | const fsa = new NodeFileSystemDirectoryHandle(fs, '/', { mode: 'readwrite' }); 22 | const crud = new FsaCrud(fsa); 23 | return { crud, snapshot: () => (fs).__vol.toJSON() }; 24 | }); 25 | }); 26 | 27 | describe('crud(fs(fsa(memfs)))', () => { 28 | testCrudfs(() => { 29 | const { fs } = memfs(); 30 | const fsa = new NodeFileSystemDirectoryHandle(fs, '/', { mode: 'readwrite' }); 31 | const fs2 = new FsaNodeFs(fsa); 32 | const crud = new NodeCrud({ fs: fs2.promises, dir: '/' }); 33 | return { crud, snapshot: () => (fs).__vol.toJSON() }; 34 | }); 35 | }); 36 | 37 | describe('crud(fsa(fs(fsa(memfs))))', () => { 38 | testCrudfs(() => { 39 | const { fs } = memfs(); 40 | const fsa = new NodeFileSystemDirectoryHandle(fs, '/', { mode: 'readwrite' }); 41 | const fs2 = new FsaNodeFs(fsa); 42 | const fsa2 = new NodeFileSystemDirectoryHandle(fs2, '/', { mode: 'readwrite' }); 43 | const crud = new FsaCrud(fsa2); 44 | return { crud, snapshot: () => (fs).__vol.toJSON() }; 45 | }); 46 | }); 47 | 48 | describe('crud(fs(fsa(fs(fsa(memfs)))))', () => { 49 | testCrudfs(() => { 50 | const { fs } = memfs(); 51 | const fsa = new NodeFileSystemDirectoryHandle(fs, '/', { mode: 'readwrite' }); 52 | const fs2 = new FsaNodeFs(fsa); 53 | const fsa2 = new NodeFileSystemDirectoryHandle(fs2, '/', { mode: 'readwrite' }); 54 | const fs3 = new FsaNodeFs(fsa2); 55 | const crud = new NodeCrud({ fs: fs3.promises, dir: '/' }); 56 | return { crud, snapshot: () => (fs).__vol.toJSON() }; 57 | }); 58 | }); 59 | 60 | describe('crud(fsa(fs(fsa(fs(fsa(memfs))))))', () => { 61 | testCrudfs(() => { 62 | const { fs } = memfs(); 63 | const fsa = new NodeFileSystemDirectoryHandle(fs, '/', { mode: 'readwrite' }); 64 | const fs2 = new FsaNodeFs(fsa); 65 | const fsa2 = new NodeFileSystemDirectoryHandle(fs2, '/', { mode: 'readwrite' }); 66 | const fs3 = new FsaNodeFs(fsa2); 67 | const fsa3 = new NodeFileSystemDirectoryHandle(fs3, '/', { mode: 'readwrite' }); 68 | const crud = new FsaCrud(fsa3); 69 | return { crud, snapshot: () => (fs).__vol.toJSON() }; 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/crud/types.ts: -------------------------------------------------------------------------------- 1 | export interface CrudApi { 2 | /** 3 | * Creates a new resource, or overwrites an existing one. 4 | * 5 | * @param collection Type of the resource, collection name. 6 | * @param id Id of the resource, document name. 7 | * @param data Blob content of the resource. 8 | * @param options Write behavior options. 9 | */ 10 | put: (collection: CrudCollection, id: string, data: Uint8Array, options?: CrudPutOptions) => Promise; 11 | 12 | /** 13 | * Retrieves the content of a resource. 14 | * 15 | * @param collection Type of the resource, collection name. 16 | * @param id Id of the resource, document name. 17 | * @returns Blob content of the resource. 18 | */ 19 | get: (collection: CrudCollection, id: string) => Promise; 20 | 21 | /** 22 | * Deletes a resource. 23 | * 24 | * @param collection Type of the resource, collection name. 25 | * @param id Id of the resource, document name. 26 | * @param silent When true, does not throw an error if the collection or 27 | * resource does not exist. Default is false. 28 | */ 29 | del: (collection: CrudCollection, id: string, silent?: boolean) => Promise; 30 | 31 | /** 32 | * Fetches information about a resource. 33 | * 34 | * @param collection Type of the resource, collection name. 35 | * @param id Id of the resource, document name, if any. 36 | * @returns Information about the resource. 37 | */ 38 | info: (collection: CrudCollection, id?: string) => Promise; 39 | 40 | /** 41 | * Deletes all resources of a collection, and deletes recursively all sub-collections. 42 | * 43 | * @param collection Type of the resource, collection name. 44 | * @param silent When true, does not throw an error if the collection or 45 | * resource does not exist. Default is false. 46 | */ 47 | drop: (collection: CrudCollection, silent?: boolean) => Promise; 48 | 49 | /** 50 | * Iterates over all resources of a collection. 51 | * 52 | * @param collection Type of the resource, collection name. 53 | * @returns Iterator of resources of the given type. 54 | */ 55 | scan: (collection: CrudCollection) => AsyncIterableIterator; 56 | 57 | /** 58 | * Fetches a list of resources of a collection, and sub-collections. 59 | * 60 | * @param collection Type of the resource, collection name. 61 | * @returns List of resources of the given type, and sub-types. 62 | */ 63 | list: (collection: CrudCollection) => Promise; 64 | 65 | /** 66 | * Creates a new CrudApi instance, with the given collection as root. 67 | * 68 | * @param collection Type of the resource, collection name. 69 | * @returns A new CrudApi instance, with the given collection as root. 70 | */ 71 | from: (collection: CrudCollection) => Promise; 72 | } 73 | 74 | export type CrudCollection = string[]; 75 | 76 | export interface CrudPutOptions { 77 | throwIf?: 'exists' | 'missing'; 78 | } 79 | 80 | export interface CrudCollectionEntry { 81 | /** Kind of the resource, type or item. */ 82 | type: 'resource' | 'collection'; 83 | /** Name of the resource. */ 84 | id: string; 85 | } 86 | 87 | export interface CrudResourceInfo extends CrudCollectionEntry { 88 | /** Size of the resource in bytes. */ 89 | size?: number; 90 | /** Timestamp when the resource last modified. */ 91 | modified?: number; 92 | /** Timestamp when the resource was created. */ 93 | created?: number; 94 | } 95 | -------------------------------------------------------------------------------- /src/crud/util.ts: -------------------------------------------------------------------------------- 1 | import { CrudCollection } from './types'; 2 | import { assertName } from '../node-to-fsa/util'; 3 | 4 | export const assertType = (type: CrudCollection, method: string, klass: string): void => { 5 | const length = type.length; 6 | for (let i = 0; i < length; i++) assertName(type[i], method, klass); 7 | }; 8 | -------------------------------------------------------------------------------- /src/encoding.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from './internal/buffer'; 2 | import * as errors from './internal/errors'; 3 | 4 | export type TDataOut = string | Buffer; // Data formats we give back to users. 5 | export type TEncodingExtended = BufferEncoding | 'buffer'; 6 | 7 | export const ENCODING_UTF8: BufferEncoding = 'utf8'; 8 | 9 | export function assertEncoding(encoding: string | undefined) { 10 | if (encoding && !Buffer.isEncoding(encoding)) throw new errors.TypeError('ERR_INVALID_OPT_VALUE_ENCODING', encoding); 11 | } 12 | 13 | export function strToEncoding(str: string, encoding?: TEncodingExtended): TDataOut { 14 | if (!encoding || encoding === ENCODING_UTF8) return str; // UTF-8 15 | if (encoding === 'buffer') return new Buffer(str); // `buffer` encoding 16 | return new Buffer(str).toString(encoding); // Custom encoding 17 | } 18 | -------------------------------------------------------------------------------- /src/fsa-to-crud/__tests__/FsaCrud.test.ts: -------------------------------------------------------------------------------- 1 | import { memfs } from '../..'; 2 | import { onlyOnNode20 } from '../../__tests__/util'; 3 | import { NodeFileSystemDirectoryHandle } from '../../node-to-fsa'; 4 | import { FsaCrud } from '../FsaCrud'; 5 | import { testCrudfs } from '../../crud/__tests__/testCrudfs'; 6 | 7 | const setup = () => { 8 | const { fs } = memfs(); 9 | const fsa = new NodeFileSystemDirectoryHandle(fs, '/', { mode: 'readwrite' }); 10 | const crud = new FsaCrud(fsa); 11 | return { fs, fsa, crud, snapshot: () => (fs).__vol.toJSON() }; 12 | }; 13 | 14 | onlyOnNode20('FsaCrud', () => { 15 | testCrudfs(setup); 16 | }); 17 | -------------------------------------------------------------------------------- /src/fsa-to-crud/index.ts: -------------------------------------------------------------------------------- 1 | export { FsaCrud } from './FsaCrud'; 2 | -------------------------------------------------------------------------------- /src/fsa-to-crud/util.ts: -------------------------------------------------------------------------------- 1 | import type * as crud from '../crud/types'; 2 | 3 | export const newFile404Error = (collection: crud.CrudCollection, id: string) => 4 | new DOMException(`Resource "${id}" in /${collection.join('/')} not found`, 'ResourceNotFound'); 5 | 6 | export const newFolder404Error = (collection: crud.CrudCollection) => 7 | new DOMException(`Collection /${collection.join('/')} does not exist`, 'CollectionNotFound'); 8 | 9 | export const newExistsError = () => new DOMException('Resource already exists', 'Exists'); 10 | 11 | export const newMissingError = () => new DOMException('Resource is missing', 'Missing'); 12 | -------------------------------------------------------------------------------- /src/fsa-to-node/FsaNodeDirent.ts: -------------------------------------------------------------------------------- 1 | import type { IDirent, TDataOut } from '../node/types/misc'; 2 | 3 | export class FsaNodeDirent implements IDirent { 4 | public constructor( 5 | public readonly name: TDataOut, 6 | protected readonly kind: 'file' | 'directory', 7 | ) {} 8 | 9 | public isDirectory(): boolean { 10 | return this.kind === 'directory'; 11 | } 12 | 13 | public isFile(): boolean { 14 | return this.kind === 'file'; 15 | } 16 | 17 | public isBlockDevice(): boolean { 18 | return false; 19 | } 20 | 21 | public isCharacterDevice(): boolean { 22 | return false; 23 | } 24 | 25 | public isSymbolicLink(): boolean { 26 | return false; 27 | } 28 | 29 | public isFIFO(): boolean { 30 | return false; 31 | } 32 | 33 | public isSocket(): boolean { 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/fsa-to-node/FsaNodeFsOpenFile.ts: -------------------------------------------------------------------------------- 1 | import { FLAG } from '../consts/FLAG'; 2 | import type * as fsa from '../fsa/types'; 3 | import type * as misc from '../node/types/misc'; 4 | 5 | /** 6 | * Represents an open file. Stores additional metadata about the open file, such 7 | * as the seek position. 8 | */ 9 | export class FsaNodeFsOpenFile { 10 | protected seek: number = 0; 11 | 12 | /** 13 | * This influences the behavior of the next write operation. On the first 14 | * write we want to overwrite the file or keep the existing data, depending 15 | * with which flags the file was opened. On subsequent writes we want to 16 | * append to the file. 17 | */ 18 | protected keepExistingData: boolean; 19 | 20 | public constructor( 21 | public readonly fd: number, 22 | public readonly createMode: misc.TMode, 23 | public readonly flags: number, 24 | public readonly file: fsa.IFileSystemFileHandle, 25 | public readonly filename: string, 26 | ) { 27 | this.keepExistingData = !!(flags & FLAG.O_APPEND); 28 | } 29 | 30 | public async close(): Promise {} 31 | 32 | public async write(data: ArrayBufferView, seek: number | null): Promise { 33 | if (typeof seek !== 'number') seek = this.seek; 34 | const writer = await this.file.createWritable({ keepExistingData: this.keepExistingData }); 35 | await writer.write({ 36 | type: 'write', 37 | data, 38 | position: seek, 39 | }); 40 | await writer.close(); 41 | this.keepExistingData = true; 42 | this.seek += data.byteLength; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/fsa-to-node/FsaNodeReadStream.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream'; 2 | import { Defer } from '../thingies/Defer'; 3 | import { concurrency } from '../thingies/concurrency'; 4 | import type { FsaNodeFsOpenFile } from './FsaNodeFsOpenFile'; 5 | import type { IReadStream } from '../node/types/misc'; 6 | import type { IReadStreamOptions } from '../node/types/options'; 7 | import type { FsaNodeFs } from './FsaNodeFs'; 8 | 9 | export class FsaNodeReadStream extends Readable implements IReadStream { 10 | protected __pending__: boolean = true; 11 | protected __closed__: boolean = false; 12 | protected __bytes__: number = 0; 13 | protected readonly __mutex__ = concurrency(1); 14 | protected readonly __file__ = new Defer(); 15 | 16 | public constructor( 17 | protected readonly fs: FsaNodeFs, 18 | protected readonly handle: Promise, 19 | public readonly path: string, 20 | protected readonly options: IReadStreamOptions, 21 | ) { 22 | super(); 23 | handle 24 | .then(file => { 25 | if (this.__closed__) return; 26 | this.__file__.resolve(file); 27 | if (this.options.fd !== undefined) this.emit('open', file.fd); 28 | this.emit('ready'); 29 | }) 30 | .catch(error => { 31 | this.__file__.reject(error); 32 | }) 33 | .finally(() => { 34 | this.__pending__ = false; 35 | }); 36 | } 37 | 38 | private async __read__(): Promise { 39 | return await this.__mutex__(async () => { 40 | if (this.__closed__) return; 41 | const { file } = await this.__file__.promise; 42 | const blob = await file.getFile(); 43 | const buffer = await blob.arrayBuffer(); 44 | const start = this.options.start || 0; 45 | let end = typeof this.options.end === 'number' ? this.options.end + 1 : buffer.byteLength; 46 | if (end > buffer.byteLength) end = buffer.byteLength; 47 | const uint8 = new Uint8Array(buffer, start, end - start); 48 | return uint8; 49 | }); 50 | } 51 | 52 | private __close__(): void { 53 | if (this.__closed__) return; 54 | this.__closed__ = true; 55 | if (this.options.autoClose) { 56 | this.__file__.promise 57 | .then(file => { 58 | this.fs.close(file.fd, () => { 59 | this.emit('close'); 60 | }); 61 | return file.close(); 62 | }) 63 | .catch(error => {}); 64 | } 65 | } 66 | 67 | // -------------------------------------------------------------- IReadStream 68 | 69 | public get bytesRead(): number { 70 | return this.__bytes__; 71 | } 72 | 73 | public get pending(): boolean { 74 | return this.__pending__; 75 | } 76 | 77 | // ----------------------------------------------------------------- Readable 78 | 79 | _read() { 80 | this.__read__().then( 81 | (uint8: Uint8Array) => { 82 | if (this.__closed__) return; 83 | if (!uint8) return this.push(null); 84 | this.__bytes__ += uint8.length; 85 | this.__close__(); 86 | this.push(uint8); 87 | this.push(null); 88 | }, 89 | error => { 90 | this.__close__(); 91 | this.destroy(error); 92 | }, 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/fsa-to-node/FsaNodeStats.ts: -------------------------------------------------------------------------------- 1 | import type * as misc from '../node/types/misc'; 2 | 3 | const time: number = 0; 4 | const timex: bigint = typeof BigInt === 'function' ? BigInt(time) : (time as any as bigint); 5 | const date = new Date(time); 6 | 7 | export class FsaNodeStats implements misc.IStats { 8 | public readonly uid: T; 9 | public readonly gid: T; 10 | public readonly rdev: T; 11 | public readonly blksize: T; 12 | public readonly ino: T; 13 | public readonly size: T; 14 | public readonly blocks: T; 15 | public readonly atime: Date; 16 | public readonly mtime: Date; 17 | public readonly ctime: Date; 18 | public readonly birthtime: Date; 19 | public readonly atimeMs: T; 20 | public readonly mtimeMs: T; 21 | public readonly ctimeMs: T; 22 | public readonly birthtimeMs: T; 23 | public readonly dev: T; 24 | public readonly mode: T; 25 | public readonly nlink: T; 26 | 27 | public constructor( 28 | isBigInt: boolean, 29 | size: T, 30 | protected readonly kind: 'file' | 'directory', 31 | ) { 32 | const dummy = (isBigInt ? timex : time) as any as T; 33 | this.uid = dummy; 34 | this.gid = dummy; 35 | this.rdev = dummy; 36 | this.blksize = dummy; 37 | this.ino = dummy; 38 | this.size = size; 39 | this.blocks = dummy; 40 | this.atime = date; 41 | this.mtime = date; 42 | this.ctime = date; 43 | this.birthtime = date; 44 | this.atimeMs = dummy; 45 | this.mtimeMs = dummy; 46 | this.ctimeMs = dummy; 47 | this.birthtimeMs = dummy; 48 | this.dev = dummy; 49 | this.mode = dummy; 50 | this.nlink = dummy; 51 | } 52 | 53 | public isDirectory(): boolean { 54 | return this.kind === 'directory'; 55 | } 56 | 57 | public isFile(): boolean { 58 | return this.kind === 'file'; 59 | } 60 | 61 | public isBlockDevice(): boolean { 62 | return false; 63 | } 64 | 65 | public isCharacterDevice(): boolean { 66 | return false; 67 | } 68 | 69 | public isSymbolicLink(): boolean { 70 | return false; 71 | } 72 | 73 | public isFIFO(): boolean { 74 | return false; 75 | } 76 | 77 | public isSocket(): boolean { 78 | return false; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/fsa-to-node/__tests__/util.test.ts: -------------------------------------------------------------------------------- 1 | import { pathToLocation } from '../util'; 2 | 3 | describe('pathToLocation()', () => { 4 | test('handles an empty string', () => { 5 | expect(pathToLocation('')).toStrictEqual([[], '']); 6 | }); 7 | 8 | test('handles a single slash', () => { 9 | expect(pathToLocation('/')).toStrictEqual([[], '']); 10 | }); 11 | 12 | test('no path, just filename', () => { 13 | expect(pathToLocation('scary.exe')).toStrictEqual([[], 'scary.exe']); 14 | }); 15 | 16 | test('strips trailing slash', () => { 17 | expect(pathToLocation('scary.exe/')).toStrictEqual([[], 'scary.exe']); 18 | }); 19 | 20 | test('multiple steps in the path', () => { 21 | expect(pathToLocation('/gg/wp/hf/gl.txt')).toStrictEqual([['gg', 'wp', 'hf'], 'gl.txt']); 22 | expect(pathToLocation('gg/wp/hf/gl.txt')).toStrictEqual([['gg', 'wp', 'hf'], 'gl.txt']); 23 | expect(pathToLocation('/wp/hf/gl.txt')).toStrictEqual([['wp', 'hf'], 'gl.txt']); 24 | expect(pathToLocation('wp/hf/gl.txt')).toStrictEqual([['wp', 'hf'], 'gl.txt']); 25 | expect(pathToLocation('/hf/gl.txt')).toStrictEqual([['hf'], 'gl.txt']); 26 | expect(pathToLocation('hf/gl.txt')).toStrictEqual([['hf'], 'gl.txt']); 27 | expect(pathToLocation('/gl.txt')).toStrictEqual([[], 'gl.txt']); 28 | expect(pathToLocation('gl.txt')).toStrictEqual([[], 'gl.txt']); 29 | }); 30 | 31 | test('handles double slashes', () => { 32 | expect(pathToLocation('/gg/wp//hf/gl.txt')).toStrictEqual([['gg', 'wp', '', 'hf'], 'gl.txt']); 33 | expect(pathToLocation('//gl.txt')).toStrictEqual([[''], 'gl.txt']); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/fsa-to-node/constants.ts: -------------------------------------------------------------------------------- 1 | export const enum FsaToNodeConstants { 2 | Separator = '/', 3 | } 4 | -------------------------------------------------------------------------------- /src/fsa-to-node/index.ts: -------------------------------------------------------------------------------- 1 | export { FsaNodeFs } from './FsaNodeFs'; 2 | export { FsaNodeSyncAdapterWorker } from './worker/FsaNodeSyncAdapterWorker'; 3 | -------------------------------------------------------------------------------- /src/fsa-to-node/json.ts: -------------------------------------------------------------------------------- 1 | import { encoder, decoder } from '@jsonjoy.com/json-pack/lib/cbor/shared'; 2 | export { encoder, decoder }; 3 | -------------------------------------------------------------------------------- /src/fsa-to-node/types.ts: -------------------------------------------------------------------------------- 1 | import type { IFileSystemFileHandle } from '../fsa/types'; 2 | import type * as opts from '../node/types/options'; 3 | import type * as misc from '../node/types/misc'; 4 | 5 | export type FsLocation = [folder: string[], file: string]; 6 | 7 | /** 8 | * Adapter which implements synchronous calls to the FSA API. 9 | */ 10 | export interface FsaNodeSyncAdapterApi { 11 | stat(location: FsLocation): FsaNodeSyncAdapterStats; 12 | access(req: [filename: string, mode: number]): void; 13 | readFile(req: [filename: string, opts?: opts.IReadFileOptions]): Uint8Array; 14 | writeFile(req: [filename: string, data: Uint8Array, opts?: opts.IWriteFileOptions]): void; 15 | appendFile(req: [filename: string, data: Uint8Array, opts?: opts.IAppendFileOptions]): void; 16 | copy(req: [src: string, dst: string, flags?: number]): void; 17 | move(req: [src: string, dst: string]): void; 18 | rmdir(req: [filename: string, opts?: opts.IRmdirOptions]): void; 19 | rm(req: [filename: string, opts?: opts.IRmOptions]): void; 20 | mkdir(req: [filename: string, opts?: misc.TMode | opts.IMkdirOptions]): string | undefined; 21 | mkdtemp(req: [filename: string, opts?: misc.TMode | opts.IOptions]): string; 22 | trunc(req: [filename: string, len: number]): void; 23 | unlink(req: [filename: string]): void; 24 | readdir(req: [filename: string]): FsaNodeSyncAdapterEntry[]; 25 | read(req: [filename: string, position: number, length: number]): Uint8Array; 26 | write(req: [filename: string, data: Uint8Array, position: number | null]): number; 27 | open(req: [filename: string, flags: number, mode: number]): IFileSystemFileHandle; 28 | } 29 | 30 | export interface FsaNodeSyncAdapter { 31 | call( 32 | method: K, 33 | payload: Parameters[0], 34 | ): ReturnType; 35 | } 36 | 37 | export interface FsaNodeSyncAdapterStats { 38 | kind: 'file' | 'directory'; 39 | size?: number; 40 | } 41 | 42 | export interface FsaNodeSyncAdapterEntry { 43 | kind: 'file' | 'directory'; 44 | name: string; 45 | } 46 | -------------------------------------------------------------------------------- /src/fsa-to-node/util.ts: -------------------------------------------------------------------------------- 1 | import { IFileSystemDirectoryHandle } from '../fsa/types'; 2 | import { FsaToNodeConstants } from './constants'; 3 | import type { FsLocation } from './types'; 4 | 5 | export const pathToLocation = (path: string): FsLocation => { 6 | if (path[0] === FsaToNodeConstants.Separator) path = path.slice(1); 7 | if (path[path.length - 1] === FsaToNodeConstants.Separator) path = path.slice(0, -1); 8 | const lastSlashIndex = path.lastIndexOf(FsaToNodeConstants.Separator); 9 | if (lastSlashIndex === -1) return [[], path]; 10 | const file = path.slice(lastSlashIndex + 1); 11 | const folder = path.slice(0, lastSlashIndex).split(FsaToNodeConstants.Separator); 12 | return [folder, file]; 13 | }; 14 | 15 | export const testDirectoryIsWritable = async (dir: IFileSystemDirectoryHandle): Promise => { 16 | const testFileName = '__memfs_writable_test_file_' + Math.random().toString(36).slice(2) + Date.now(); 17 | try { 18 | await dir.getFileHandle(testFileName, { create: true }); 19 | return true; 20 | } catch { 21 | return false; 22 | } finally { 23 | try { 24 | await dir.removeEntry(testFileName); 25 | } catch (e) {} 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/fsa-to-node/worker/FsaNodeSyncAdapterWorker.ts: -------------------------------------------------------------------------------- 1 | import { Defer } from '../../thingies/Defer'; 2 | import { FsaNodeWorkerMessageCode } from './constants'; 3 | import { SyncMessenger } from './SyncMessenger'; 4 | import { decoder, encoder } from '../json'; 5 | import type * as fsa from '../../fsa/types'; 6 | import type { FsaNodeSyncAdapter, FsaNodeSyncAdapterApi } from '../types'; 7 | import type { 8 | FsaNodeWorkerMsg, 9 | FsaNodeWorkerMsgInit, 10 | FsaNodeWorkerMsgRequest, 11 | FsaNodeWorkerMsgResponse, 12 | FsaNodeWorkerMsgResponseError, 13 | FsaNodeWorkerMsgRootSet, 14 | FsaNodeWorkerMsgSetRoot, 15 | } from './types'; 16 | 17 | let rootId = 0; 18 | 19 | export class FsaNodeSyncAdapterWorker implements FsaNodeSyncAdapter { 20 | public static async start( 21 | url: string, 22 | dir: fsa.IFileSystemDirectoryHandle | Promise, 23 | ): Promise { 24 | const worker = new Worker(url); 25 | const future = new Defer(); 26 | let id = rootId++; 27 | let messenger: SyncMessenger | undefined = undefined; 28 | const _dir = await dir; 29 | worker.onmessage = e => { 30 | const data = e.data; 31 | if (!Array.isArray(data)) return; 32 | const msg = data as FsaNodeWorkerMsg; 33 | const code = msg[0] as FsaNodeWorkerMessageCode; 34 | switch (code) { 35 | case FsaNodeWorkerMessageCode.Init: { 36 | const [, sab] = msg as FsaNodeWorkerMsgInit; 37 | messenger = new SyncMessenger(sab); 38 | const setRootMessage: FsaNodeWorkerMsgSetRoot = [FsaNodeWorkerMessageCode.SetRoot, id, _dir]; 39 | worker.postMessage(setRootMessage); 40 | break; 41 | } 42 | case FsaNodeWorkerMessageCode.RootSet: { 43 | const [, rootId] = msg as FsaNodeWorkerMsgRootSet; 44 | if (id !== rootId) return; 45 | const adapter = new FsaNodeSyncAdapterWorker(messenger!, _dir); 46 | future.resolve(adapter); 47 | break; 48 | } 49 | } 50 | }; 51 | return await future.promise; 52 | } 53 | 54 | public constructor( 55 | protected readonly messenger: SyncMessenger, 56 | protected readonly root: fsa.IFileSystemDirectoryHandle, 57 | ) {} 58 | 59 | public call( 60 | method: K, 61 | payload: Parameters[0], 62 | ): ReturnType { 63 | const request: FsaNodeWorkerMsgRequest = [FsaNodeWorkerMessageCode.Request, method, payload]; 64 | const encoded = encoder.encode(request); 65 | const encodedResponse = this.messenger.callSync(encoded); 66 | type MsgBack = FsaNodeWorkerMsgResponse | FsaNodeWorkerMsgResponseError; 67 | const [code, data] = decoder.decode(encodedResponse) as MsgBack; 68 | switch (code) { 69 | case FsaNodeWorkerMessageCode.Response: 70 | return data as any; 71 | case FsaNodeWorkerMessageCode.ResponseError: 72 | throw data; 73 | default: { 74 | throw new Error('Invalid response message code'); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/fsa-to-node/worker/SyncMessenger.ts: -------------------------------------------------------------------------------- 1 | export type AsyncCallback = (request: Uint8Array) => Promise; 2 | 3 | /** 4 | * @param condition Condition to wait for, when true, the function returns. 5 | * @param ms Maximum time to wait in milliseconds. 6 | */ 7 | const sleepUntil = (condition: () => boolean, ms: number = 100) => { 8 | const start = Date.now(); 9 | while (!condition()) { 10 | const now = Date.now(); 11 | if (now - start > ms) throw new Error('Timeout'); 12 | } 13 | }; 14 | 15 | /** 16 | * `SyncMessenger` allows to execute asynchronous code synchronously. The 17 | * asynchronous code is executed in a Worker thread, while the main thread is 18 | * blocked until the asynchronous code is finished. 19 | * 20 | * First four 4-byte words is the header, where the first word is used for Atomics 21 | * notifications. The second word is used for spin-locking the main thread until 22 | * the asynchronous code is finished. The third word is used to specify payload 23 | * length. The fourth word is currently unused. 24 | * 25 | * The maximum payload size is the size of the SharedArrayBuffer minus the 26 | * header size. 27 | */ 28 | export class SyncMessenger { 29 | protected readonly int32: Int32Array; 30 | protected readonly uint8: Uint8Array; 31 | protected readonly headerSize; 32 | protected readonly dataSize; 33 | 34 | public constructor(protected readonly sab: SharedArrayBuffer) { 35 | this.int32 = new Int32Array(sab); 36 | this.uint8 = new Uint8Array(sab); 37 | this.headerSize = 4 * 4; 38 | this.dataSize = sab.byteLength - this.headerSize; 39 | } 40 | 41 | public callSync(data: Uint8Array): Uint8Array { 42 | const requestLength = data.length; 43 | const headerSize = this.headerSize; 44 | const int32 = this.int32; 45 | int32[1] = 0; 46 | int32[2] = requestLength; 47 | this.uint8.set(data, headerSize); 48 | Atomics.notify(int32, 0); 49 | sleepUntil(() => int32[1] === 1); 50 | const responseLength = int32[2]; 51 | const response = this.uint8.slice(headerSize, headerSize + responseLength); 52 | return response; 53 | } 54 | 55 | public serveAsync(callback: AsyncCallback): void { 56 | const headerSize = this.headerSize; 57 | (async () => { 58 | try { 59 | const int32 = this.int32; 60 | const res = Atomics.wait(int32, 0, 0); 61 | if (res !== 'ok') throw new Error(`Unexpected Atomics.wait result: ${res}`); 62 | const requestLength = this.int32[2]; 63 | const request = this.uint8.slice(headerSize, headerSize + requestLength); 64 | const response = await callback(request); 65 | const responseLength = response.length; 66 | int32[2] = responseLength; 67 | this.uint8.set(response, headerSize); 68 | int32[1] = 1; 69 | } catch {} 70 | this.serveAsync(callback); 71 | })().catch(() => {}); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/fsa-to-node/worker/constants.ts: -------------------------------------------------------------------------------- 1 | export const enum FsaNodeWorkerMessageCode { 2 | Init = 0, 3 | SetRoot, 4 | RootSet, 5 | Request, 6 | Response, 7 | ResponseError, 8 | } 9 | -------------------------------------------------------------------------------- /src/fsa-to-node/worker/types.ts: -------------------------------------------------------------------------------- 1 | import type { IFileSystemDirectoryHandle } from '../../fsa/types'; 2 | import type { FsaNodeWorkerMessageCode } from './constants'; 3 | 4 | export type FsaNodeWorkerMsgInit = [type: FsaNodeWorkerMessageCode.Init, sab: SharedArrayBuffer]; 5 | export type FsaNodeWorkerMsgSetRoot = [ 6 | type: FsaNodeWorkerMessageCode.SetRoot, 7 | id: number, 8 | dir: IFileSystemDirectoryHandle, 9 | ]; 10 | export type FsaNodeWorkerMsgRootSet = [type: FsaNodeWorkerMessageCode.RootSet, id: number]; 11 | export type FsaNodeWorkerMsgRequest = [type: FsaNodeWorkerMessageCode.Request, method: string, data: unknown]; 12 | export type FsaNodeWorkerMsgResponse = [type: FsaNodeWorkerMessageCode.Response, data: unknown]; 13 | export type FsaNodeWorkerMsgResponseError = [type: FsaNodeWorkerMessageCode.ResponseError, data: unknown]; 14 | 15 | export interface FsaNodeWorkerError { 16 | message: string; 17 | code?: string; 18 | } 19 | 20 | export type FsaNodeWorkerMsg = 21 | | FsaNodeWorkerMsgInit 22 | | FsaNodeWorkerMsgSetRoot 23 | | FsaNodeWorkerMsgRootSet 24 | | FsaNodeWorkerMsgRequest 25 | | FsaNodeWorkerMsgResponse 26 | | FsaNodeWorkerMsgResponseError; 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Stats from './Stats'; 2 | import Dirent from './Dirent'; 3 | import { 4 | Volume, 5 | StatWatcher, 6 | FSWatcher, 7 | toUnixTimestamp, 8 | IWriteStream, 9 | DirectoryJSON, 10 | NestedDirectoryJSON, 11 | } from './volume'; 12 | import { constants } from './constants'; 13 | import type { FsPromisesApi } from './node/types'; 14 | import type * as misc from './node/types/misc'; 15 | import { fsSynchronousApiList } from './node/lists/fsSynchronousApiList'; 16 | import { fsCallbackApiList } from './node/lists/fsCallbackApiList'; 17 | const { F_OK, R_OK, W_OK, X_OK } = constants; 18 | 19 | export { DirectoryJSON, NestedDirectoryJSON, Volume }; 20 | 21 | // Default volume. 22 | export const vol = new Volume(); 23 | 24 | export interface IFs extends Volume { 25 | constants: typeof constants; 26 | Stats: new (...args) => Stats; 27 | Dirent: new (...args) => Dirent; 28 | StatWatcher: new () => StatWatcher; 29 | FSWatcher: new () => FSWatcher; 30 | ReadStream: new (...args) => misc.IReadStream; 31 | WriteStream: new (...args) => IWriteStream; 32 | promises: FsPromisesApi; 33 | _toUnixTimestamp; 34 | } 35 | 36 | export function createFsFromVolume(vol: Volume): IFs { 37 | const fs = { F_OK, R_OK, W_OK, X_OK, constants, Stats, Dirent } as any as IFs; 38 | 39 | // Bind FS methods. 40 | for (const method of fsSynchronousApiList) if (typeof vol[method] === 'function') fs[method] = vol[method].bind(vol); 41 | for (const method of fsCallbackApiList) if (typeof vol[method] === 'function') fs[method] = vol[method].bind(vol); 42 | 43 | fs.StatWatcher = vol.StatWatcher; 44 | fs.FSWatcher = vol.FSWatcher; 45 | fs.WriteStream = vol.WriteStream; 46 | fs.ReadStream = vol.ReadStream; 47 | fs.promises = vol.promises; 48 | 49 | fs._toUnixTimestamp = toUnixTimestamp; 50 | (fs as any).__vol = vol; 51 | 52 | return fs; 53 | } 54 | 55 | export const fs: IFs = createFsFromVolume(vol); 56 | 57 | /** 58 | * Creates a new file system instance. 59 | * 60 | * @param json File system structure expressed as a JSON object. 61 | * Use `null` for empty directories and empty string for empty files. 62 | * @param cwd Current working directory. The JSON structure will be created 63 | * relative to this path. 64 | * @returns A `memfs` file system instance, which is a drop-in replacement for 65 | * the `fs` module. 66 | */ 67 | export const memfs = (json: NestedDirectoryJSON = {}, cwd: string = '/'): { fs: IFs; vol: Volume } => { 68 | const vol = Volume.fromNestedJSON(json, cwd); 69 | const fs = createFsFromVolume(vol); 70 | return { fs, vol }; 71 | }; 72 | 73 | export type IFsWithVolume = IFs & { __vol: Volume }; 74 | 75 | declare let module; 76 | module.exports = { ...module.exports, ...fs }; 77 | module.exports.semantic = true; 78 | -------------------------------------------------------------------------------- /src/internal/buffer.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | 3 | function bufferV0P12Ponyfill(arg0: any, ...args: any): Buffer { 4 | return new Buffer(arg0, ...args); 5 | } 6 | 7 | const bufferAllocUnsafe = Buffer.allocUnsafe || bufferV0P12Ponyfill; 8 | const bufferFrom = Buffer.from || bufferV0P12Ponyfill; 9 | 10 | export { Buffer, bufferAllocUnsafe, bufferFrom }; 11 | -------------------------------------------------------------------------------- /src/node-to-crud/__tests__/FsaCrud.test.ts: -------------------------------------------------------------------------------- 1 | import { memfs } from '../..'; 2 | import { onlyOnNode20 } from '../../__tests__/util'; 3 | import { NodeCrud } from '../NodeCrud'; 4 | import { testCrudfs } from '../../crud/__tests__/testCrudfs'; 5 | 6 | const setup = () => { 7 | const { fs } = memfs(); 8 | const crud = new NodeCrud({ 9 | fs: fs.promises, 10 | dir: '/', 11 | }); 12 | return { fs, crud, snapshot: () => (fs).__vol.toJSON() }; 13 | }; 14 | 15 | onlyOnNode20('NodeCrud', () => { 16 | testCrudfs(setup); 17 | }); 18 | -------------------------------------------------------------------------------- /src/node-to-crud/index.ts: -------------------------------------------------------------------------------- 1 | export { NodeCrud, NodeCrudOptions } from './NodeCrud'; 2 | -------------------------------------------------------------------------------- /src/node-to-fsa/NodeFileSystemFileHandle.ts: -------------------------------------------------------------------------------- 1 | import { NodeFileSystemHandle } from './NodeFileSystemHandle'; 2 | import { NodeFileSystemSyncAccessHandle } from './NodeFileSystemSyncAccessHandle'; 3 | import { assertCanWrite, basename, ctx as createCtx, newNotAllowedError } from './util'; 4 | import { NodeFileSystemWritableFileStream } from './NodeFileSystemWritableFileStream'; 5 | import type { NodeFsaContext, NodeFsaFs } from './types'; 6 | import type { IFileSystemFileHandle, IFileSystemSyncAccessHandle } from '../fsa/types'; 7 | 8 | export class NodeFileSystemFileHandle extends NodeFileSystemHandle implements IFileSystemFileHandle { 9 | protected readonly ctx: NodeFsaContext; 10 | 11 | constructor( 12 | protected readonly fs: NodeFsaFs, 13 | public readonly __path: string, 14 | ctx: Partial = {}, 15 | ) { 16 | ctx = createCtx(ctx); 17 | super('file', basename(__path, ctx.separator!)); 18 | this.ctx = ctx as NodeFsaContext; 19 | } 20 | 21 | /** 22 | * Returns a {@link Promise} which resolves to a {@link File} object 23 | * representing the state on disk of the entry represented by the handle. 24 | * 25 | * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/getFile 26 | */ 27 | public async getFile(): Promise { 28 | try { 29 | const path = this.__path; 30 | const promises = this.fs.promises; 31 | const stats = await promises.stat(path); 32 | // TODO: Once implemented, use promises.readAsBlob() instead of promises.readFile(). 33 | const data = await promises.readFile(path); 34 | const file = new File([data], this.name, { lastModified: stats.mtime.getTime() }); 35 | return file; 36 | } catch (error) { 37 | if (error instanceof DOMException) throw error; 38 | if (error && typeof error === 'object') { 39 | switch (error.code) { 40 | case 'EPERM': 41 | case 'EACCES': 42 | throw newNotAllowedError(); 43 | } 44 | } 45 | throw error; 46 | } 47 | } 48 | 49 | /** 50 | * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle 51 | */ 52 | public get createSyncAccessHandle(): undefined | (() => Promise) { 53 | if (!this.ctx.syncHandleAllowed) return undefined; 54 | return async () => new NodeFileSystemSyncAccessHandle(this.fs, this.__path, this.ctx); 55 | } 56 | 57 | /** 58 | * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createWritable 59 | */ 60 | public async createWritable( 61 | { keepExistingData = false }: CreateWritableOptions = { keepExistingData: false }, 62 | ): Promise { 63 | assertCanWrite(this.ctx.mode); 64 | return new NodeFileSystemWritableFileStream(this.fs, this.__path, keepExistingData); 65 | } 66 | } 67 | 68 | export interface CreateWritableOptions { 69 | keepExistingData?: boolean; 70 | } 71 | -------------------------------------------------------------------------------- /src/node-to-fsa/NodeFileSystemHandle.ts: -------------------------------------------------------------------------------- 1 | import { NodePermissionStatus } from './NodePermissionStatus'; 2 | import type { IFileSystemHandle, FileSystemHandlePermissionDescriptor } from '../fsa/types'; 3 | 4 | /** 5 | * Represents a File System Access API file handle `FileSystemHandle` object, 6 | * which was created from a Node.js `fs` module. 7 | * 8 | * @see [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle) 9 | */ 10 | export abstract class NodeFileSystemHandle implements IFileSystemHandle { 11 | constructor( 12 | public readonly kind: 'file' | 'directory', 13 | public readonly name: string, 14 | ) {} 15 | 16 | /** 17 | * Compares two handles to see if the associated entries (either a file or directory) match. 18 | * 19 | * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/isSameEntry 20 | */ 21 | public isSameEntry(fileSystemHandle: NodeFileSystemHandle): boolean { 22 | return ( 23 | this.constructor === fileSystemHandle.constructor && (this as any).__path === (fileSystemHandle as any).__path 24 | ); 25 | } 26 | 27 | /** 28 | * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/queryPermission 29 | */ 30 | public queryPermission( 31 | fileSystemHandlePermissionDescriptor: FileSystemHandlePermissionDescriptor, 32 | ): NodePermissionStatus { 33 | throw new Error('Not implemented'); 34 | } 35 | 36 | /** 37 | * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/remove 38 | */ 39 | public async remove({ recursive }: { recursive?: boolean } = { recursive: false }): Promise { 40 | throw new Error('Not implemented'); 41 | } 42 | 43 | /** 44 | * @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/requestPermission 45 | */ 46 | public requestPermission( 47 | fileSystemHandlePermissionDescriptor: FileSystemHandlePermissionDescriptor, 48 | ): NodePermissionStatus { 49 | throw new Error('Not implemented'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/node-to-fsa/NodePermissionStatus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/PermissionStatus) 3 | */ 4 | export class NodePermissionStatus { 5 | constructor( 6 | public readonly name: string, 7 | public readonly state: 'granted' | 'denied' | 'prompt', 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/node-to-fsa/README.md: -------------------------------------------------------------------------------- 1 | This adapter code converts an instance of [Node.js FS API][node-fs] to a 2 | [File System Access API][fsa] (FSA) instance. 3 | 4 | [node-fs]: https://nodejs.org/api/fs.html 5 | [fsa]: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API 6 | -------------------------------------------------------------------------------- /src/node-to-fsa/__tests__/NodeFileSystemHandle.test.ts: -------------------------------------------------------------------------------- 1 | import { DirectoryJSON, memfs } from '../..'; 2 | import { NodeFileSystemDirectoryHandle } from '../NodeFileSystemDirectoryHandle'; 3 | import { onlyOnNode20 } from '../../__tests__/util'; 4 | 5 | const setup = (json: DirectoryJSON = {}) => { 6 | const { fs } = memfs(json, '/'); 7 | const dir = new NodeFileSystemDirectoryHandle(fs as any, '/', { mode: 'readwrite' }); 8 | return { dir, fs }; 9 | }; 10 | 11 | onlyOnNode20('NodeFileSystemHandle', () => { 12 | test('can instantiate', () => { 13 | const { dir } = setup(); 14 | expect(dir).toBeInstanceOf(NodeFileSystemDirectoryHandle); 15 | }); 16 | 17 | describe('.isSameEntry()', () => { 18 | test('returns true for the same root entry', async () => { 19 | const { dir } = setup(); 20 | expect(dir.isSameEntry(dir)).toBe(true); 21 | }); 22 | 23 | test('returns true for two different instances of the same entry', async () => { 24 | const { dir } = setup({ 25 | subdir: null, 26 | }); 27 | const subdir = await dir.getDirectoryHandle('subdir'); 28 | expect(subdir.isSameEntry(subdir)).toBe(true); 29 | expect(dir.isSameEntry(dir)).toBe(true); 30 | expect(dir.isSameEntry(subdir)).toBe(false); 31 | expect(subdir.isSameEntry(dir)).toBe(false); 32 | }); 33 | 34 | test('returns false when comparing file with a directory', async () => { 35 | const { dir } = setup({ 36 | file: 'lala', 37 | }); 38 | const file = await dir.getFileHandle('file'); 39 | expect(file.isSameEntry(dir)).toBe(false); 40 | expect(dir.isSameEntry(file)).toBe(false); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/node-to-fsa/__tests__/scenarios.test.ts: -------------------------------------------------------------------------------- 1 | import { nodeToFsa } from '..'; 2 | import { memfs } from '../..'; 3 | import { onlyOnNode20 } from '../../__tests__/util'; 4 | 5 | onlyOnNode20('scenarios', () => { 6 | test('can init FSA from an arbitrary FS folder and execute operations', async () => { 7 | const { fs, vol } = memfs({ 8 | '/tmp': null, 9 | '/etc': null, 10 | '/bin': null, 11 | '/Users/kasper/Documents/shopping-list.txt': 'Milk, Eggs, Bread', 12 | }); 13 | const dir = nodeToFsa(fs, '/Users/kasper/Documents', { mode: 'readwrite' }); 14 | const shoppingListFile = await dir.getFileHandle('shopping-list.txt'); 15 | const shoppingList = await shoppingListFile.getFile(); 16 | expect(await shoppingList.text()).toBe('Milk, Eggs, Bread'); 17 | const backupsDir = await dir.getDirectoryHandle('backups', { create: true }); 18 | const backupFile = await backupsDir.getFileHandle('shopping-list.txt', { create: true }); 19 | const writable = await backupFile.createWritable(); 20 | await writable.write(await shoppingList.arrayBuffer()); 21 | await writable.close(); 22 | const logsFileHandle = await dir.getFileHandle('logs.csv', { create: true }); 23 | const logsWritable = await logsFileHandle.createWritable(); 24 | await logsWritable.write('timestamp,level,message\n'); 25 | await logsWritable.write({ type: 'write', data: '2021-01-01T00:00:00Z,INFO,Hello World\n' }); 26 | await logsWritable.close(); 27 | expect(vol.toJSON()).toStrictEqual({ 28 | '/tmp': null, 29 | '/etc': null, 30 | '/bin': null, 31 | '/Users/kasper/Documents/shopping-list.txt': 'Milk, Eggs, Bread', 32 | '/Users/kasper/Documents/backups/shopping-list.txt': 'Milk, Eggs, Bread', 33 | '/Users/kasper/Documents/logs.csv': 'timestamp,level,message\n2021-01-01T00:00:00Z,INFO,Hello World\n', 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/node-to-fsa/__tests__/util.test.ts: -------------------------------------------------------------------------------- 1 | import { basename } from '../util'; 2 | 3 | describe('basename()', () => { 4 | test('handles empty string', () => { 5 | expect(basename('', '/')).toBe(''); 6 | }); 7 | 8 | test('return the same string if there is no nesting', () => { 9 | expect(basename('scary.exe', '/')).toBe('scary.exe'); 10 | }); 11 | 12 | test('ignores slash, if it is the last char', () => { 13 | expect(basename('scary.exe/', '/')).toBe('scary.exe'); 14 | expect(basename('/ab/c/scary.exe/', '/')).toBe('scary.exe'); 15 | }); 16 | 17 | test('returns last step in path', () => { 18 | expect(basename('/gg/wp/hf/gl.txt', '/')).toBe('gl.txt'); 19 | expect(basename('gg/wp/hf/gl.txt', '/')).toBe('gl.txt'); 20 | expect(basename('/wp/hf/gl.txt', '/')).toBe('gl.txt'); 21 | expect(basename('wp/hf/gl.txt', '/')).toBe('gl.txt'); 22 | expect(basename('/hf/gl.txt', '/')).toBe('gl.txt'); 23 | expect(basename('hf/gl.txt', '/')).toBe('gl.txt'); 24 | expect(basename('/gl.txt', '/')).toBe('gl.txt'); 25 | expect(basename('gl.txt', '/')).toBe('gl.txt'); 26 | }); 27 | 28 | test('handles double slashes', () => { 29 | expect(basename('/gg/wp/hf//gl.txt', '/')).toBe('gl.txt'); 30 | expect(basename('//gl.txt', '/')).toBe('gl.txt'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/node-to-fsa/index.ts: -------------------------------------------------------------------------------- 1 | import { NodeFileSystemDirectoryHandle } from './NodeFileSystemDirectoryHandle'; 2 | import { NodeFsaContext, NodeFsaFs } from './types'; 3 | 4 | export * from './types'; 5 | export * from './NodeFileSystemHandle'; 6 | export * from './NodeFileSystemDirectoryHandle'; 7 | export * from './NodeFileSystemFileHandle'; 8 | 9 | export const nodeToFsa = ( 10 | fs: NodeFsaFs, 11 | dirPath: string, 12 | ctx?: Partial, 13 | ): NodeFileSystemDirectoryHandle => { 14 | return new NodeFileSystemDirectoryHandle(fs, dirPath, ctx); 15 | }; 16 | -------------------------------------------------------------------------------- /src/node-to-fsa/types.ts: -------------------------------------------------------------------------------- 1 | import type { FsPromisesApi, FsSynchronousApi } from '../node/types'; 2 | import type { FsCommonObjects } from '../node/types/FsCommonObjects'; 3 | 4 | /** 5 | * Required Node.js `fs` module functions for File System Access API. 6 | */ 7 | export type NodeFsaFs = Pick & { promises: FsPromisesApi } & Pick< 8 | FsSynchronousApi, 9 | 'openSync' | 'fsyncSync' | 'statSync' | 'closeSync' | 'readSync' | 'truncateSync' | 'writeSync' 10 | >; 11 | 12 | export interface NodeFsaContext { 13 | separator: '/' | '\\'; 14 | /** Whether synchronous file handles are allowed. */ 15 | syncHandleAllowed: boolean; 16 | /** Whether writes are allowed, defaults to `read`. */ 17 | mode: 'read' | 'readwrite'; 18 | } 19 | -------------------------------------------------------------------------------- /src/node-to-fsa/util.ts: -------------------------------------------------------------------------------- 1 | import type { NodeFsaContext } from './types'; 2 | 3 | /** 4 | * Creates a new {@link NodeFsaContext}. 5 | */ 6 | export const ctx = (partial: Partial = {}): NodeFsaContext => { 7 | return { 8 | separator: '/', 9 | syncHandleAllowed: false, 10 | mode: 'read', 11 | ...partial, 12 | }; 13 | }; 14 | 15 | export const basename = (path: string, separator: string) => { 16 | if (path[path.length - 1] === separator) path = path.slice(0, -1); 17 | const lastSlashIndex = path.lastIndexOf(separator); 18 | return lastSlashIndex === -1 ? path : path.slice(lastSlashIndex + 1); 19 | }; 20 | 21 | const nameRegex = /^(\.{1,2})$|^(.*([\/\\]).*)$/; 22 | 23 | export const assertName = (name: string, method: string, klass: string) => { 24 | const isInvalid = !name || nameRegex.test(name); 25 | if (isInvalid) throw new TypeError(`Failed to execute '${method}' on '${klass}': Name is not allowed.`); 26 | }; 27 | 28 | export const assertCanWrite = (mode: 'read' | 'readwrite') => { 29 | if (mode !== 'readwrite') 30 | throw new DOMException( 31 | 'The request is not allowed by the user agent or the platform in the current context.', 32 | 'NotAllowedError', 33 | ); 34 | }; 35 | 36 | export const newNotFoundError = () => 37 | new DOMException( 38 | 'A requested file or directory could not be found at the time an operation was processed.', 39 | 'NotFoundError', 40 | ); 41 | 42 | export const newTypeMismatchError = () => 43 | new DOMException('The path supplied exists, but was not an entry of requested type.', 'TypeMismatchError'); 44 | 45 | export const newNotAllowedError = () => new DOMException('Permission not granted.', 'NotAllowedError'); 46 | -------------------------------------------------------------------------------- /src/node/FsPromises.ts: -------------------------------------------------------------------------------- 1 | import { isReadableStream, promisify, streamToBuffer } from './util'; 2 | import { constants } from '../constants'; 3 | import type * as opts from './types/options'; 4 | import type * as misc from './types/misc'; 5 | import type { FsCallbackApi, FsPromisesApi } from './types'; 6 | 7 | export class FsPromises implements FsPromisesApi { 8 | public readonly constants = constants; 9 | 10 | public constructor( 11 | protected readonly fs: FsCallbackApi, 12 | public readonly FileHandle: new (...args: unknown[]) => misc.IFileHandle, 13 | ) {} 14 | 15 | public readonly cp = promisify(this.fs, 'cp'); 16 | public readonly opendir = promisify(this.fs, 'opendir'); 17 | public readonly statfs = promisify(this.fs, 'statfs'); 18 | public readonly lutimes = promisify(this.fs, 'lutimes'); 19 | public readonly access = promisify(this.fs, 'access'); 20 | public readonly chmod = promisify(this.fs, 'chmod'); 21 | public readonly chown = promisify(this.fs, 'chown'); 22 | public readonly copyFile = promisify(this.fs, 'copyFile'); 23 | public readonly lchmod = promisify(this.fs, 'lchmod'); 24 | public readonly lchown = promisify(this.fs, 'lchown'); 25 | public readonly link = promisify(this.fs, 'link'); 26 | public readonly lstat = promisify(this.fs, 'lstat'); 27 | public readonly mkdir = promisify(this.fs, 'mkdir'); 28 | public readonly mkdtemp = promisify(this.fs, 'mkdtemp'); 29 | public readonly readdir = promisify(this.fs, 'readdir'); 30 | public readonly readlink = promisify(this.fs, 'readlink'); 31 | public readonly realpath = promisify(this.fs, 'realpath'); 32 | public readonly rename = promisify(this.fs, 'rename'); 33 | public readonly rmdir = promisify(this.fs, 'rmdir'); 34 | public readonly rm = promisify(this.fs, 'rm'); 35 | public readonly stat = promisify(this.fs, 'stat'); 36 | public readonly symlink = promisify(this.fs, 'symlink'); 37 | public readonly truncate = promisify(this.fs, 'truncate'); 38 | public readonly unlink = promisify(this.fs, 'unlink'); 39 | public readonly utimes = promisify(this.fs, 'utimes'); 40 | 41 | public readonly readFile = ( 42 | id: misc.TFileHandle, 43 | options?: opts.IReadFileOptions | string, 44 | ): Promise => { 45 | return promisify(this.fs, 'readFile')(id instanceof this.FileHandle ? id.fd : (id as misc.PathLike), options); 46 | }; 47 | 48 | public readonly appendFile = ( 49 | path: misc.TFileHandle, 50 | data: misc.TData, 51 | options?: opts.IAppendFileOptions | string, 52 | ): Promise => { 53 | return promisify(this.fs, 'appendFile')( 54 | path instanceof this.FileHandle ? path.fd : (path as misc.PathLike), 55 | data, 56 | options, 57 | ); 58 | }; 59 | 60 | public readonly open = (path: misc.PathLike, flags: misc.TFlags = 'r', mode?: misc.TMode) => { 61 | return promisify(this.fs, 'open', fd => new this.FileHandle(this.fs, fd))(path, flags, mode); 62 | }; 63 | 64 | public readonly writeFile = ( 65 | id: misc.TFileHandle, 66 | data: misc.TPromisesData, 67 | options?: opts.IWriteFileOptions, 68 | ): Promise => { 69 | const dataPromise = isReadableStream(data) ? streamToBuffer(data) : Promise.resolve(data); 70 | return dataPromise.then(data => 71 | promisify(this.fs, 'writeFile')(id instanceof this.FileHandle ? id.fd : (id as misc.PathLike), data, options), 72 | ); 73 | }; 74 | 75 | public readonly watch = () => { 76 | throw new Error('Not implemented'); 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/node/constants.ts: -------------------------------------------------------------------------------- 1 | import { constants } from '../constants'; 2 | 3 | // Default modes for opening files. 4 | export const enum MODE { 5 | FILE = 0o666, 6 | DIR = 0o777, 7 | DEFAULT = MODE.FILE, 8 | } 9 | 10 | export const ERRSTR = { 11 | PATH_STR: 'path must be a string, Buffer, or Uint8Array', 12 | // FD: 'file descriptor must be a unsigned 32-bit integer', 13 | FD: 'fd must be a file descriptor', 14 | MODE_INT: 'mode must be an int', 15 | CB: 'callback must be a function', 16 | UID: 'uid must be an unsigned int', 17 | GID: 'gid must be an unsigned int', 18 | LEN: 'len must be an integer', 19 | ATIME: 'atime must be an integer', 20 | MTIME: 'mtime must be an integer', 21 | PREFIX: 'filename prefix is required', 22 | BUFFER: 'buffer must be an instance of Buffer or StaticBuffer', 23 | OFFSET: 'offset must be an integer', 24 | LENGTH: 'length must be an integer', 25 | POSITION: 'position must be an integer', 26 | }; 27 | 28 | const { O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_EXCL, O_TRUNC, O_APPEND, O_SYNC } = constants; 29 | 30 | // List of file `flags` as defined by Node. 31 | export enum FLAGS { 32 | // Open file for reading. An exception occurs if the file does not exist. 33 | r = O_RDONLY, 34 | // Open file for reading and writing. An exception occurs if the file does not exist. 35 | 'r+' = O_RDWR, 36 | // Open file for reading in synchronous mode. Instructs the operating system to bypass the local file system cache. 37 | rs = O_RDONLY | O_SYNC, 38 | sr = FLAGS.rs, 39 | // Open file for reading and writing, telling the OS to open it synchronously. See notes for 'rs' about using this with caution. 40 | 'rs+' = O_RDWR | O_SYNC, 41 | 'sr+' = FLAGS['rs+'], 42 | // Open file for writing. The file is created (if it does not exist) or truncated (if it exists). 43 | w = O_WRONLY | O_CREAT | O_TRUNC, 44 | // Like 'w' but fails if path exists. 45 | wx = O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 46 | xw = FLAGS.wx, 47 | // Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists). 48 | 'w+' = O_RDWR | O_CREAT | O_TRUNC, 49 | // Like 'w+' but fails if path exists. 50 | 'wx+' = O_RDWR | O_CREAT | O_TRUNC | O_EXCL, 51 | 'xw+' = FLAGS['wx+'], 52 | // Open file for appending. The file is created if it does not exist. 53 | a = O_WRONLY | O_APPEND | O_CREAT, 54 | // Like 'a' but fails if path exists. 55 | ax = O_WRONLY | O_APPEND | O_CREAT | O_EXCL, 56 | xa = FLAGS.ax, 57 | // Open file for reading and appending. The file is created if it does not exist. 58 | 'a+' = O_RDWR | O_APPEND | O_CREAT, 59 | // Like 'a+' but fails if path exists. 60 | 'ax+' = O_RDWR | O_APPEND | O_CREAT | O_EXCL, 61 | 'xa+' = FLAGS['ax+'], 62 | } 63 | -------------------------------------------------------------------------------- /src/node/lists/fsCallbackApiList.ts: -------------------------------------------------------------------------------- 1 | import type { FsCallbackApi } from '../types'; 2 | 3 | export const fsCallbackApiList: Array = [ 4 | 'access', 5 | 'appendFile', 6 | 'chmod', 7 | 'chown', 8 | 'close', 9 | 'copyFile', 10 | 'createReadStream', 11 | 'createWriteStream', 12 | 'exists', 13 | 'fchmod', 14 | 'fchown', 15 | 'fdatasync', 16 | 'fstat', 17 | 'fsync', 18 | 'ftruncate', 19 | 'futimes', 20 | 'lchmod', 21 | 'lchown', 22 | 'link', 23 | 'lstat', 24 | 'mkdir', 25 | 'mkdtemp', 26 | 'open', 27 | 'read', 28 | 'readv', 29 | 'readdir', 30 | 'readFile', 31 | 'readlink', 32 | 'realpath', 33 | 'rename', 34 | 'rm', 35 | 'rmdir', 36 | 'stat', 37 | 'symlink', 38 | 'truncate', 39 | 'unlink', 40 | 'unwatchFile', 41 | 'utimes', 42 | 'lutimes', 43 | 'watch', 44 | 'watchFile', 45 | 'write', 46 | 'writev', 47 | 'writeFile', 48 | ]; 49 | -------------------------------------------------------------------------------- /src/node/lists/fsCommonObjectsList.ts: -------------------------------------------------------------------------------- 1 | import type { FsCommonObjects } from '../types/FsCommonObjects'; 2 | 3 | export const fsCommonObjectsList: Array = [ 4 | 'F_OK', 5 | 'R_OK', 6 | 'W_OK', 7 | 'X_OK', 8 | 'constants', 9 | 'Stats', 10 | 'StatFs', 11 | 'Dir', 12 | 'Dirent', 13 | 'StatsWatcher', 14 | 'FSWatcher', 15 | 'ReadStream', 16 | 'WriteStream', 17 | ]; 18 | -------------------------------------------------------------------------------- /src/node/lists/fsSynchronousApiList.ts: -------------------------------------------------------------------------------- 1 | import type { FsSynchronousApi } from '../types'; 2 | 3 | export const fsSynchronousApiList: Array = [ 4 | 'accessSync', 5 | 'appendFileSync', 6 | 'chmodSync', 7 | 'chownSync', 8 | 'closeSync', 9 | 'copyFileSync', 10 | 'existsSync', 11 | 'fchmodSync', 12 | 'fchownSync', 13 | 'fdatasyncSync', 14 | 'fstatSync', 15 | 'fsyncSync', 16 | 'ftruncateSync', 17 | 'futimesSync', 18 | 'lchmodSync', 19 | 'lchownSync', 20 | 'linkSync', 21 | 'lstatSync', 22 | 'mkdirSync', 23 | 'mkdtempSync', 24 | 'openSync', 25 | 'readdirSync', 26 | 'readFileSync', 27 | 'readlinkSync', 28 | 'readSync', 29 | 'readvSync', 30 | 'realpathSync', 31 | 'renameSync', 32 | 'rmdirSync', 33 | 'rmSync', 34 | 'statSync', 35 | 'symlinkSync', 36 | 'truncateSync', 37 | 'unlinkSync', 38 | 'utimesSync', 39 | 'lutimesSync', 40 | 'writeFileSync', 41 | 'writeSync', 42 | 'writevSync', 43 | 44 | // 'cpSync', 45 | // 'statfsSync', 46 | ]; 47 | -------------------------------------------------------------------------------- /src/node/types/FsCommonObjects.ts: -------------------------------------------------------------------------------- 1 | import type { constants } from '../../constants'; 2 | import type * as misc from './misc'; 3 | 4 | export interface FsCommonObjects { 5 | F_OK: number; 6 | R_OK: number; 7 | W_OK: number; 8 | X_OK: number; 9 | constants: typeof constants; 10 | Dir: new (...args: unknown[]) => misc.IDir; 11 | Dirent: new (...args: unknown[]) => misc.IDirent; 12 | FSWatcher: new (...args: unknown[]) => misc.IFSWatcher; 13 | ReadStream: new (...args: unknown[]) => misc.IReadStream; 14 | StatFs: new (...args: unknown[]) => misc.IStatFs; 15 | Stats: new (...args: unknown[]) => misc.IStats; 16 | StatsWatcher: new (...args: unknown[]) => misc.IStatWatcher; 17 | WriteStream: new (...args: unknown[]) => misc.IWriteStream; 18 | } 19 | -------------------------------------------------------------------------------- /src/node/types/FsPromisesApi.ts: -------------------------------------------------------------------------------- 1 | import type { constants } from '../../constants'; 2 | import * as misc from './misc'; 3 | import * as opts from './options'; 4 | 5 | export interface FsPromisesApi { 6 | constants: typeof constants; 7 | FileHandle: new (...args: unknown[]) => misc.IFileHandle; 8 | access(path: misc.PathLike, mode?: number): Promise; 9 | appendFile(path: misc.TFileHandle, data: misc.TData, options?: opts.IAppendFileOptions | string): Promise; 10 | chmod(path: misc.PathLike, mode: misc.TMode): Promise; 11 | chown(path: misc.PathLike, uid: number, gid: number): Promise; 12 | copyFile(src: misc.PathLike, dest: misc.PathLike, flags?: misc.TFlagsCopy): Promise; 13 | cp(src: string | URL, dest: string | URL, options?: opts.ICpOptions): Promise; 14 | lchmod(path: misc.PathLike, mode: misc.TMode): Promise; 15 | lchown(path: misc.PathLike, uid: number, gid: number): Promise; 16 | lutimes(path: misc.PathLike, atime: misc.TTime, mtime: misc.TTime): Promise; 17 | link(existingPath: misc.PathLike, newPath: misc.PathLike): Promise; 18 | lstat(path: misc.PathLike, options?: opts.IStatOptions): Promise; 19 | mkdir(path: misc.PathLike, options?: misc.TMode | opts.IMkdirOptions): Promise; 20 | mkdtemp(prefix: string, options?: opts.IOptions): Promise; 21 | open(path: misc.PathLike, flags?: misc.TFlags, mode?: misc.TMode): Promise; 22 | opendir(path: misc.PathLike, options?: opts.IOpendirOptions): Promise; 23 | readdir(path: misc.PathLike, options?: opts.IReaddirOptions | string): Promise; 24 | readFile(id: misc.TFileHandle, options?: opts.IReadFileOptions | string): Promise; 25 | readlink(path: misc.PathLike, options?: opts.IOptions): Promise; 26 | realpath(path: misc.PathLike, options?: opts.IRealpathOptions | string): Promise; 27 | rename(oldPath: misc.PathLike, newPath: misc.PathLike): Promise; 28 | rmdir(path: misc.PathLike, options?: opts.IRmdirOptions): Promise; 29 | rm(path: misc.PathLike, options?: opts.IRmOptions): Promise; 30 | stat(path: misc.PathLike, options?: opts.IStatOptions): Promise; 31 | statfs(path: misc.PathLike, options?: opts.IStatOptions): Promise; 32 | symlink(target: misc.PathLike, path: misc.PathLike, type?: misc.symlink.Type): Promise; 33 | truncate(path: misc.PathLike, len?: number): Promise; 34 | unlink(path: misc.PathLike): Promise; 35 | utimes(path: misc.PathLike, atime: misc.TTime, mtime: misc.TTime): Promise; 36 | watch( 37 | filename: misc.PathLike, 38 | options?: opts.IWatchOptions, 39 | ): AsyncIterableIterator<{ eventType: string; filename: string | Buffer }>; 40 | writeFile(id: misc.TFileHandle, data: misc.TPromisesData, options?: opts.IWriteFileOptions): Promise; 41 | } 42 | -------------------------------------------------------------------------------- /src/node/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { FsSynchronousApi } from './FsSynchronousApi'; 2 | import type { FsCallbackApi } from './FsCallbackApi'; 3 | import type { FsPromisesApi } from './FsPromisesApi'; 4 | 5 | export { FsSynchronousApi, FsCallbackApi, FsPromisesApi }; 6 | 7 | export interface FsApi extends FsCallbackApi, FsSynchronousApi { 8 | promises: FsPromisesApi; 9 | } 10 | -------------------------------------------------------------------------------- /src/print/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { toTreeSync } from '..'; 2 | import { memfs } from '../..'; 3 | 4 | test('can print a single file', () => { 5 | const { fs } = memfs({ 6 | '/file.txt': '...', 7 | }); 8 | expect(toTreeSync(fs, { dir: '/' })).toMatchInlineSnapshot(` 9 | "/ 10 | └─ file.txt" 11 | `); 12 | }); 13 | 14 | test('can a one level deep directory tree', () => { 15 | const { fs } = memfs({ 16 | '/file.txt': '...', 17 | '/foo/bar.txt': '...', 18 | '/foo/index.php': '...', 19 | }); 20 | expect(toTreeSync(fs, { dir: '/' })).toMatchInlineSnapshot(` 21 | "/ 22 | ├─ file.txt 23 | └─ foo/ 24 | ├─ bar.txt 25 | └─ index.php" 26 | `); 27 | }); 28 | 29 | test('can print two-levels of folders', () => { 30 | const { fs } = memfs({ 31 | '/level1/level2/file.txt': '...', 32 | }); 33 | expect(toTreeSync(fs, { dir: '/' })).toMatchInlineSnapshot(` 34 | "/ 35 | └─ level1/ 36 | └─ level2/ 37 | └─ file.txt" 38 | `); 39 | }); 40 | 41 | test('can stop recursion at specified depth', () => { 42 | const { fs } = memfs({ 43 | '/dir1/dir2/dir3/file.txt': '...', 44 | }); 45 | expect(toTreeSync(fs, { dir: '/', depth: 2 })).toMatchInlineSnapshot(` 46 | "/ 47 | └─ dir1/ 48 | └─ dir2/ (...)" 49 | `); 50 | }); 51 | 52 | test('can print symlinks', () => { 53 | const { fs } = memfs({ 54 | '/a/b/c/file.txt': '...', 55 | '/a/b/main.rb': '...', 56 | }); 57 | fs.symlinkSync('/a/b/c/file.txt', '/goto'); 58 | expect(toTreeSync(fs)).toMatchInlineSnapshot(` 59 | "/ 60 | ├─ a/ 61 | │ └─ b/ 62 | │ ├─ c/ 63 | │ │ └─ file.txt 64 | │ └─ main.rb 65 | └─ goto → /a/b/c/file.txt" 66 | `); 67 | }); 68 | 69 | test('can print starting from subfolder', () => { 70 | const { fs } = memfs({ 71 | '/a/b/c/file.txt': '...', 72 | '/a/b/main.rb': '...', 73 | }); 74 | expect(toTreeSync(fs, { dir: '/a/b' })).toMatchInlineSnapshot(` 75 | "b/ 76 | ├─ c/ 77 | │ └─ file.txt 78 | └─ main.rb" 79 | `); 80 | }); 81 | -------------------------------------------------------------------------------- /src/print/index.ts: -------------------------------------------------------------------------------- 1 | import { printTree } from 'tree-dump'; 2 | import { basename } from '../node-to-fsa/util'; 3 | import type { FsSynchronousApi } from '../node/types'; 4 | import type { IDirent } from '../node/types/misc'; 5 | 6 | export const toTreeSync = (fs: FsSynchronousApi, opts: ToTreeOptions = {}) => { 7 | const separator = opts.separator || '/'; 8 | let dir = opts.dir || separator; 9 | if (dir[dir.length - 1] !== separator) dir += separator; 10 | const tab = opts.tab || ''; 11 | const depth = opts.depth ?? 10; 12 | let subtree = ' (...)'; 13 | if (depth > 0) { 14 | const list = fs.readdirSync(dir, { withFileTypes: true }) as IDirent[]; 15 | subtree = printTree( 16 | tab, 17 | list.map(entry => tab => { 18 | if (entry.isDirectory()) { 19 | return toTreeSync(fs, { dir: dir + entry.name, depth: depth - 1, tab }); 20 | } else if (entry.isSymbolicLink()) { 21 | return '' + entry.name + ' → ' + fs.readlinkSync(dir + entry.name); 22 | } else { 23 | return '' + entry.name; 24 | } 25 | }), 26 | ); 27 | } 28 | const base = basename(dir, separator) + separator; 29 | return base + subtree; 30 | }; 31 | 32 | export interface ToTreeOptions { 33 | dir?: string; 34 | tab?: string; 35 | depth?: number; 36 | separator?: '/' | '\\'; 37 | } 38 | -------------------------------------------------------------------------------- /src/process.ts: -------------------------------------------------------------------------------- 1 | // Here we mock the global `process` variable in case we are not in Node's environment. 2 | 3 | export interface IProcess { 4 | getuid?(): number; 5 | 6 | getgid?(): number; 7 | 8 | cwd(): string; 9 | 10 | platform: string; 11 | emitWarning: (message: string, type: string) => void; 12 | env: {}; 13 | } 14 | 15 | /** 16 | * Looks to return a `process` object, if one is available. 17 | * 18 | * The global `process` is returned if defined; 19 | * otherwise `require('process')` is attempted. 20 | * 21 | * If that fails, `undefined` is returned. 22 | * 23 | * @return {IProcess | undefined} 24 | */ 25 | const maybeReturnProcess = (): IProcess | undefined => { 26 | if (typeof process !== 'undefined') { 27 | return process; 28 | } 29 | 30 | try { 31 | return require('process'); 32 | } catch { 33 | return undefined; 34 | } 35 | }; 36 | 37 | export function createProcess(): IProcess { 38 | const p: IProcess = maybeReturnProcess() || ({} as IProcess); 39 | 40 | if (!p.cwd) p.cwd = () => '/'; 41 | if (!p.emitWarning) 42 | p.emitWarning = (message, type) => { 43 | // tslint:disable-next-line:no-console 44 | console.warn(`${type}${type ? ': ' : ''}${message}`); 45 | }; 46 | if (!p.env) p.env = {}; 47 | return p; 48 | } 49 | 50 | export default createProcess(); 51 | -------------------------------------------------------------------------------- /src/queueMicrotask.ts: -------------------------------------------------------------------------------- 1 | export default typeof queueMicrotask === 'function' ? queueMicrotask : (cb => 2 | Promise.resolve() 3 | .then(() => cb()) 4 | .catch(() => {})); 5 | -------------------------------------------------------------------------------- /src/setTimeoutUnref.ts: -------------------------------------------------------------------------------- 1 | export type TSetTimeout = (callback: (...args) => void, time?: number, args?: any[]) => any; 2 | 3 | /** 4 | * `setTimeoutUnref` is just like `setTimeout`, 5 | * only in Node's environment it will "unref" its macro task. 6 | */ 7 | function setTimeoutUnref(callback, time?, args?): object { 8 | const ref = setTimeout.apply(typeof globalThis !== 'undefined' ? globalThis : global, arguments); 9 | if (ref && typeof ref === 'object' && typeof ref.unref === 'function') ref.unref(); 10 | return ref; 11 | } 12 | 13 | export default setTimeoutUnref; 14 | -------------------------------------------------------------------------------- /src/snapshot/__tests__/async.test.ts: -------------------------------------------------------------------------------- 1 | import { fromSnapshot, toSnapshot } from '../async'; 2 | import { memfs } from '../..'; 3 | import { SnapshotNodeType } from '../constants'; 4 | 5 | test('can snapshot a single file', async () => { 6 | const { fs } = memfs({ 7 | '/foo': 'bar', 8 | }); 9 | const snapshot = await toSnapshot({ fs: fs.promises, path: '/foo' }); 10 | expect(snapshot).toStrictEqual([SnapshotNodeType.File, expect.any(Object), new Uint8Array([98, 97, 114])]); 11 | }); 12 | 13 | test('can snapshot a single folder', async () => { 14 | const { fs } = memfs({ 15 | '/foo': null, 16 | }); 17 | const snapshot = await toSnapshot({ fs: fs.promises, path: '/foo' }); 18 | expect(snapshot).toStrictEqual([SnapshotNodeType.Folder, expect.any(Object), {}]); 19 | }); 20 | 21 | test('can snapshot a folder with a file and symlink', async () => { 22 | const { fs } = memfs({ 23 | '/foo': 'bar', 24 | }); 25 | fs.symlinkSync('/foo', '/baz'); 26 | const snapshot = await toSnapshot({ fs: fs.promises, path: '/' }); 27 | expect(snapshot).toStrictEqual([ 28 | SnapshotNodeType.Folder, 29 | expect.any(Object), 30 | { 31 | foo: [SnapshotNodeType.File, expect.any(Object), new Uint8Array([98, 97, 114])], 32 | baz: [SnapshotNodeType.Symlink, { target: '/foo' }], 33 | }, 34 | ]); 35 | }); 36 | 37 | test('can create a snapshot and un-snapshot a complex fs tree', async () => { 38 | const { fs } = memfs({ 39 | '/start': { 40 | file1: 'file1', 41 | file2: 'file2', 42 | 'empty-folder': null, 43 | '/folder1': { 44 | file3: 'file3', 45 | file4: 'file4', 46 | 'empty-folder': null, 47 | '/folder2': { 48 | file5: 'file5', 49 | file6: 'file6', 50 | 'empty-folder': null, 51 | 'empty-folde2': null, 52 | }, 53 | }, 54 | }, 55 | }); 56 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 57 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 58 | const snapshot = await toSnapshot({ fs: fs.promises, path: '/start' })!; 59 | const { fs: fs2, vol: vol2 } = memfs(); 60 | fs2.mkdirSync('/start', { recursive: true }); 61 | await fromSnapshot(snapshot, { fs: fs2.promises, path: '/start' }); 62 | expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3])); 63 | const snapshot2 = await toSnapshot({ fs: fs2.promises, path: '/start' })!; 64 | expect(snapshot2).toStrictEqual(snapshot); 65 | }); 66 | -------------------------------------------------------------------------------- /src/snapshot/__tests__/binary.test.ts: -------------------------------------------------------------------------------- 1 | import { memfs } from '../..'; 2 | import * as binary from '../binary'; 3 | 4 | const data = { 5 | '/start': { 6 | file1: 'file1', 7 | file2: 'file2', 8 | 'empty-folder': null, 9 | '/folder1': { 10 | file3: 'file3', 11 | file4: 'file4', 12 | 'empty-folder': null, 13 | '/folder2': { 14 | file5: 'file5', 15 | file6: 'file6', 16 | 'empty-folder': null, 17 | 'empty-folde2': null, 18 | }, 19 | }, 20 | }, 21 | }; 22 | 23 | test('sync and async snapshots are equivalent', async () => { 24 | const { fs } = memfs(data); 25 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 26 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 27 | const snapshot1 = binary.toBinarySnapshotSync({ fs: fs, path: '/start' })!; 28 | const snapshot2 = await binary.toBinarySnapshot({ fs: fs.promises, path: '/start' })!; 29 | expect(snapshot1).toStrictEqual(snapshot2); 30 | }); 31 | 32 | describe('synchronous', () => { 33 | test('can create a binary snapshot and un-snapshot it back', () => { 34 | const { fs } = memfs(data); 35 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 36 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 37 | const snapshot = binary.toBinarySnapshotSync({ fs, path: '/start' })!; 38 | const { fs: fs2, vol: vol2 } = memfs(); 39 | fs2.mkdirSync('/start', { recursive: true }); 40 | binary.fromBinarySnapshotSync(snapshot, { fs: fs2, path: '/start' }); 41 | expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3])); 42 | const snapshot2 = binary.toBinarySnapshotSync({ fs: fs2, path: '/start' })!; 43 | expect(snapshot2).toStrictEqual(snapshot); 44 | }); 45 | }); 46 | 47 | describe('asynchronous', () => { 48 | test('can create a binary snapshot and un-snapshot it back', async () => { 49 | const { fs } = memfs(data); 50 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 51 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 52 | const snapshot = await binary.toBinarySnapshot({ fs: fs.promises, path: '/start' })!; 53 | const { fs: fs2, vol: vol2 } = memfs(); 54 | fs2.mkdirSync('/start', { recursive: true }); 55 | await binary.fromBinarySnapshot(snapshot, { fs: fs2.promises, path: '/start' }); 56 | expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3])); 57 | const snapshot2 = await binary.toBinarySnapshot({ fs: fs2.promises, path: '/start' })!; 58 | expect(snapshot2).toStrictEqual(snapshot); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/snapshot/__tests__/json.test.ts: -------------------------------------------------------------------------------- 1 | import { memfs } from '../..'; 2 | import { SnapshotNodeType } from '../constants'; 3 | import * as json from '../json'; 4 | 5 | const data = { 6 | '/start': { 7 | file1: 'file1', 8 | file2: 'file2', 9 | 'empty-folder': null, 10 | '/folder1': { 11 | file3: 'file3', 12 | file4: 'file4', 13 | 'empty-folder': null, 14 | '/folder2': { 15 | file5: 'file5', 16 | file6: 'file6', 17 | 'empty-folder': null, 18 | 'empty-folde2': null, 19 | }, 20 | }, 21 | }, 22 | }; 23 | 24 | test('snapshot is a valid JSON', () => { 25 | const { fs } = memfs(data); 26 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 27 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 28 | const snapshot = json.toJsonSnapshotSync({ fs, path: '/start' })!; 29 | const pojo = JSON.parse(Buffer.from(snapshot).toString()); 30 | expect(Array.isArray(pojo)).toBe(true); 31 | expect(pojo[0]).toBe(SnapshotNodeType.Folder); 32 | }); 33 | 34 | test('sync and async snapshots are equivalent', async () => { 35 | const { fs } = memfs(data); 36 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 37 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 38 | const snapshot1 = await json.toJsonSnapshotSync({ fs: fs, path: '/start' })!; 39 | const snapshot2 = await json.toJsonSnapshot({ fs: fs.promises, path: '/start' })!; 40 | expect(snapshot1).toStrictEqual(snapshot2); 41 | }); 42 | 43 | describe('synchronous', () => { 44 | test('can create a binary snapshot and un-snapshot it back', () => { 45 | const { fs } = memfs(data); 46 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 47 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 48 | const snapshot = json.toJsonSnapshotSync({ fs, path: '/start' })!; 49 | const { fs: fs2, vol: vol2 } = memfs(); 50 | fs2.mkdirSync('/start', { recursive: true }); 51 | json.fromJsonSnapshotSync(snapshot, { fs: fs2, path: '/start' }); 52 | expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3])); 53 | const snapshot2 = json.toJsonSnapshotSync({ fs: fs2, path: '/start' })!; 54 | expect(snapshot2).toStrictEqual(snapshot); 55 | }); 56 | }); 57 | 58 | describe('asynchronous', () => { 59 | test('can create a binary snapshot and un-snapshot it back', async () => { 60 | const { fs } = memfs(data); 61 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 62 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 63 | const snapshot = await json.toJsonSnapshot({ fs: fs.promises, path: '/start' })!; 64 | const { fs: fs2, vol: vol2 } = memfs(); 65 | fs2.mkdirSync('/start', { recursive: true }); 66 | await json.fromJsonSnapshot(snapshot, { fs: fs2.promises, path: '/start' }); 67 | expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3])); 68 | const snapshot2 = await json.toJsonSnapshot({ fs: fs2.promises, path: '/start' })!; 69 | expect(snapshot2).toStrictEqual(snapshot); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/snapshot/__tests__/sync.test.ts: -------------------------------------------------------------------------------- 1 | import { toSnapshotSync, fromSnapshotSync } from '../sync'; 2 | import { memfs } from '../..'; 3 | import { SnapshotNodeType } from '../constants'; 4 | 5 | test('can snapshot a single file', () => { 6 | const { fs } = memfs({ 7 | '/foo': 'bar', 8 | }); 9 | const snapshot = toSnapshotSync({ fs, path: '/foo' }); 10 | expect(snapshot).toStrictEqual([SnapshotNodeType.File, expect.any(Object), new Uint8Array([98, 97, 114])]); 11 | }); 12 | 13 | test('can snapshot a single folder', () => { 14 | const { fs } = memfs({ 15 | '/foo': null, 16 | }); 17 | const snapshot = toSnapshotSync({ fs, path: '/foo' }); 18 | expect(snapshot).toStrictEqual([SnapshotNodeType.Folder, expect.any(Object), {}]); 19 | }); 20 | 21 | test('can snapshot a folder with a file and symlink', () => { 22 | const { fs } = memfs({ 23 | '/foo': 'bar', 24 | }); 25 | fs.symlinkSync('/foo', '/baz'); 26 | const snapshot = toSnapshotSync({ fs, path: '/' }); 27 | expect(snapshot).toStrictEqual([ 28 | SnapshotNodeType.Folder, 29 | expect.any(Object), 30 | { 31 | foo: [SnapshotNodeType.File, expect.any(Object), new Uint8Array([98, 97, 114])], 32 | baz: [SnapshotNodeType.Symlink, { target: '/foo' }], 33 | }, 34 | ]); 35 | }); 36 | 37 | test('can create a snapshot and un-snapshot a complex fs tree', () => { 38 | const { fs } = memfs({ 39 | '/start': { 40 | file1: 'file1', 41 | file2: 'file2', 42 | 'empty-folder': null, 43 | '/folder1': { 44 | file3: 'file3', 45 | file4: 'file4', 46 | 'empty-folder': null, 47 | '/folder2': { 48 | file5: 'file5', 49 | file6: 'file6', 50 | 'empty-folder': null, 51 | 'empty-folde2': null, 52 | }, 53 | }, 54 | }, 55 | }); 56 | fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); 57 | fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); 58 | const snapshot = toSnapshotSync({ fs, path: '/start' })!; 59 | const { fs: fs2, vol: vol2 } = memfs(); 60 | fs2.mkdirSync('/start', { recursive: true }); 61 | fromSnapshotSync(snapshot, { fs: fs2, path: '/start' }); 62 | expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3])); 63 | const snapshot2 = toSnapshotSync({ fs: fs2, path: '/start' })!; 64 | expect(snapshot2).toStrictEqual(snapshot); 65 | }); 66 | -------------------------------------------------------------------------------- /src/snapshot/async.ts: -------------------------------------------------------------------------------- 1 | import { SnapshotNodeType } from './constants'; 2 | import type { AsyncSnapshotOptions, SnapshotNode } from './types'; 3 | 4 | export const toSnapshot = async ({ fs, path = '/', separator = '/' }: AsyncSnapshotOptions): Promise => { 5 | const stats = await fs.lstat(path); 6 | if (stats.isDirectory()) { 7 | const list = await fs.readdir(path); 8 | const entries: { [child: string]: SnapshotNode } = {}; 9 | const dir = path.endsWith(separator) ? path : path + separator; 10 | for (const child of list) { 11 | const childSnapshot = await toSnapshot({ fs, path: `${dir}${child}`, separator }); 12 | if (childSnapshot) entries['' + child] = childSnapshot; 13 | } 14 | return [SnapshotNodeType.Folder, {}, entries]; 15 | } else if (stats.isFile()) { 16 | const buf = (await fs.readFile(path)) as Buffer; 17 | const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); 18 | return [SnapshotNodeType.File, {}, uint8]; 19 | } else if (stats.isSymbolicLink()) { 20 | return [ 21 | SnapshotNodeType.Symlink, 22 | { 23 | target: (await fs.readlink(path, { encoding: 'utf8' })) as string, 24 | }, 25 | ]; 26 | } 27 | return null; 28 | }; 29 | 30 | export const fromSnapshot = async ( 31 | snapshot: SnapshotNode, 32 | { fs, path = '/', separator = '/' }: AsyncSnapshotOptions, 33 | ): Promise => { 34 | if (!snapshot) return; 35 | switch (snapshot[0]) { 36 | case SnapshotNodeType.Folder: { 37 | if (!path.endsWith(separator)) path = path + separator; 38 | const [, , entries] = snapshot; 39 | await fs.mkdir(path, { recursive: true }); 40 | for (const [name, child] of Object.entries(entries)) 41 | await fromSnapshot(child, { fs, path: `${path}${name}`, separator }); 42 | break; 43 | } 44 | case SnapshotNodeType.File: { 45 | const [, , data] = snapshot; 46 | await fs.writeFile(path, data); 47 | break; 48 | } 49 | case SnapshotNodeType.Symlink: { 50 | const [, { target }] = snapshot; 51 | await fs.symlink(target, path); 52 | break; 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/snapshot/binary.ts: -------------------------------------------------------------------------------- 1 | import { CborEncoder } from '@jsonjoy.com/json-pack/lib/cbor/CborEncoder'; 2 | import { CborDecoder } from '@jsonjoy.com/json-pack/lib/cbor/CborDecoder'; 3 | import { fromSnapshotSync, toSnapshotSync } from './sync'; 4 | import { fromSnapshot, toSnapshot } from './async'; 5 | import { writer } from './shared'; 6 | import type { CborUint8Array } from '@jsonjoy.com/json-pack/lib/cbor/types'; 7 | import type { AsyncSnapshotOptions, SnapshotNode, SnapshotOptions } from './types'; 8 | 9 | const encoder = new CborEncoder(writer); 10 | const decoder = new CborDecoder(); 11 | 12 | export const toBinarySnapshotSync = (options: SnapshotOptions): CborUint8Array => { 13 | const snapshot = toSnapshotSync(options); 14 | return encoder.encode(snapshot) as CborUint8Array; 15 | }; 16 | 17 | export const fromBinarySnapshotSync = (uint8: CborUint8Array, options: SnapshotOptions): void => { 18 | const snapshot = decoder.decode(uint8) as SnapshotNode; 19 | fromSnapshotSync(snapshot, options); 20 | }; 21 | 22 | export const toBinarySnapshot = async (options: AsyncSnapshotOptions): Promise> => { 23 | const snapshot = await toSnapshot(options); 24 | return encoder.encode(snapshot) as CborUint8Array; 25 | }; 26 | 27 | export const fromBinarySnapshot = async ( 28 | uint8: CborUint8Array, 29 | options: AsyncSnapshotOptions, 30 | ): Promise => { 31 | const snapshot = decoder.decode(uint8) as SnapshotNode; 32 | await fromSnapshot(snapshot, options); 33 | }; 34 | -------------------------------------------------------------------------------- /src/snapshot/constants.ts: -------------------------------------------------------------------------------- 1 | export const enum SnapshotNodeType { 2 | Folder = 0, 3 | File = 1, 4 | Symlink = 2, 5 | } 6 | -------------------------------------------------------------------------------- /src/snapshot/index.ts: -------------------------------------------------------------------------------- 1 | export type * from './types'; 2 | export * from './constants'; 3 | export * from './sync'; 4 | export * from './binary'; 5 | export * from './json'; 6 | -------------------------------------------------------------------------------- /src/snapshot/json.ts: -------------------------------------------------------------------------------- 1 | import { JsonEncoder } from '@jsonjoy.com/json-pack/lib/json/JsonEncoder'; 2 | import { JsonDecoder } from '@jsonjoy.com/json-pack/lib/json/JsonDecoder'; 3 | import { fromSnapshotSync, toSnapshotSync } from './sync'; 4 | import { fromSnapshot, toSnapshot } from './async'; 5 | import { writer } from './shared'; 6 | import type { AsyncSnapshotOptions, SnapshotNode, SnapshotOptions } from './types'; 7 | 8 | /** @todo Import this type from `json-joy` once it is available. */ 9 | export type JsonUint8Array = Uint8Array & { __BRAND__: 'json'; __TYPE__: T }; 10 | 11 | const encoder = new JsonEncoder(writer); 12 | const decoder = new JsonDecoder(); 13 | 14 | export const toJsonSnapshotSync = (options: SnapshotOptions): JsonUint8Array => { 15 | const snapshot = toSnapshotSync(options); 16 | return encoder.encode(snapshot) as JsonUint8Array; 17 | }; 18 | 19 | export const fromJsonSnapshotSync = (uint8: JsonUint8Array, options: SnapshotOptions): void => { 20 | const snapshot = decoder.read(uint8) as SnapshotNode; 21 | fromSnapshotSync(snapshot, options); 22 | }; 23 | 24 | export const toJsonSnapshot = async (options: AsyncSnapshotOptions): Promise> => { 25 | const snapshot = await toSnapshot(options); 26 | return encoder.encode(snapshot) as JsonUint8Array; 27 | }; 28 | 29 | export const fromJsonSnapshot = async ( 30 | uint8: JsonUint8Array, 31 | options: AsyncSnapshotOptions, 32 | ): Promise => { 33 | const snapshot = decoder.read(uint8) as SnapshotNode; 34 | await fromSnapshot(snapshot, options); 35 | }; 36 | -------------------------------------------------------------------------------- /src/snapshot/shared.ts: -------------------------------------------------------------------------------- 1 | import { Writer } from '@jsonjoy.com/util/lib/buffers/Writer'; 2 | 3 | export const writer = new Writer(1024 * 32); 4 | -------------------------------------------------------------------------------- /src/snapshot/sync.ts: -------------------------------------------------------------------------------- 1 | import { SnapshotNodeType } from './constants'; 2 | import type { SnapshotNode, SnapshotOptions } from './types'; 3 | 4 | export const toSnapshotSync = ({ fs, path = '/', separator = '/' }: SnapshotOptions): SnapshotNode => { 5 | const stats = fs.lstatSync(path); 6 | if (stats.isDirectory()) { 7 | const list = fs.readdirSync(path); 8 | const entries: { [child: string]: SnapshotNode } = {}; 9 | const dir = path.endsWith(separator) ? path : path + separator; 10 | for (const child of list) { 11 | const childSnapshot = toSnapshotSync({ fs, path: `${dir}${child}`, separator }); 12 | if (childSnapshot) entries['' + child] = childSnapshot; 13 | } 14 | return [SnapshotNodeType.Folder, {}, entries]; 15 | } else if (stats.isFile()) { 16 | const buf = fs.readFileSync(path) as Buffer; 17 | const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); 18 | return [SnapshotNodeType.File, {}, uint8]; 19 | } else if (stats.isSymbolicLink()) { 20 | return [ 21 | SnapshotNodeType.Symlink, 22 | { 23 | target: fs.readlinkSync(path).toString(), 24 | }, 25 | ]; 26 | } 27 | return null; 28 | }; 29 | 30 | export const fromSnapshotSync = ( 31 | snapshot: SnapshotNode, 32 | { fs, path = '/', separator = '/' }: SnapshotOptions, 33 | ): void => { 34 | if (!snapshot) return; 35 | switch (snapshot[0]) { 36 | case SnapshotNodeType.Folder: { 37 | if (!path.endsWith(separator)) path = path + separator; 38 | const [, , entries] = snapshot; 39 | fs.mkdirSync(path, { recursive: true }); 40 | for (const [name, child] of Object.entries(entries)) 41 | fromSnapshotSync(child, { fs, path: `${path}${name}`, separator }); 42 | break; 43 | } 44 | case SnapshotNodeType.File: { 45 | const [, , data] = snapshot; 46 | fs.writeFileSync(path, data); 47 | break; 48 | } 49 | case SnapshotNodeType.Symlink: { 50 | const [, { target }] = snapshot; 51 | fs.symlinkSync(target, path); 52 | break; 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/snapshot/types.ts: -------------------------------------------------------------------------------- 1 | import type { FsPromisesApi, FsSynchronousApi } from '../node/types'; 2 | import type { SnapshotNodeType } from './constants'; 3 | 4 | export interface SnapshotOptions { 5 | fs: FsSynchronousApi; 6 | path?: string; 7 | separator?: '/' | '\\'; 8 | } 9 | 10 | export interface AsyncSnapshotOptions { 11 | fs: FsPromisesApi; 12 | path?: string; 13 | separator?: '/' | '\\'; 14 | } 15 | 16 | export type SnapshotNode = FolderNode | FileNode | SymlinkNode | UnknownNode; 17 | 18 | export type FolderNode = [ 19 | type: SnapshotNodeType.Folder, 20 | meta: FolderMetadata, 21 | entries: { [child: string]: SnapshotNode }, 22 | ]; 23 | 24 | export interface FolderMetadata {} 25 | 26 | export type FileNode = [type: SnapshotNodeType.File, meta: FileMetadata, data: Uint8Array]; 27 | 28 | export interface FileMetadata {} 29 | 30 | export type SymlinkNode = [type: SnapshotNodeType.Symlink, meta: SymlinkMetadata]; 31 | 32 | export interface SymlinkMetadata { 33 | target: string; 34 | } 35 | 36 | export type UnknownNode = null; 37 | -------------------------------------------------------------------------------- /src/thingies/Defer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An externally resolvable/rejectable "promise". Use it to resolve/reject 3 | * promise at any time. 4 | * 5 | * ```ts 6 | * const future = new Defer(); 7 | * 8 | * future.promise.then(value => console.log(value)); 9 | * 10 | * future.resolve(123); 11 | * ``` 12 | */ 13 | export class Defer { 14 | public readonly resolve!: (data: T) => void; 15 | public readonly reject!: (error: any) => void; 16 | public readonly promise: Promise = new Promise((resolve, reject) => { 17 | (this as any).resolve = resolve; 18 | (this as any).reject = reject; 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/thingies/concurrency.ts: -------------------------------------------------------------------------------- 1 | import {go} from './go'; 2 | import type {Code} from './types'; 3 | 4 | class Task { 5 | public readonly resolve!: (data: T) => void; 6 | public readonly reject!: (error: any) => void; 7 | public readonly promise = new Promise((resolve, reject) => { 8 | (this as any).resolve = resolve; 9 | (this as any).reject = reject; 10 | }); 11 | constructor(public readonly code: Code) {} 12 | } 13 | 14 | /** Limits concurrency of async code. */ 15 | export const concurrency = (limit: number) => { 16 | let workers = 0; 17 | const queue = new Set(); 18 | const work = async () => { 19 | const task = queue.values().next().value; 20 | if (task) queue.delete(task); 21 | else return; 22 | workers++; 23 | try { 24 | task.resolve(await task.code()); 25 | } catch (error) { 26 | task.reject(error); 27 | } finally { 28 | workers--, queue.size && go(work); 29 | } 30 | }; 31 | return async (code: Code): Promise => { 32 | const task = new Task(code); 33 | queue.add(task as Task); 34 | return workers < limit && go(work), task.promise; 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/thingies/go.ts: -------------------------------------------------------------------------------- 1 | import type {Code} from './types'; 2 | 3 | /** Executes code concurrently. */ 4 | export const go = (code: Code): void => { 5 | code().catch(() => {}); 6 | }; 7 | -------------------------------------------------------------------------------- /src/thingies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './concurrency'; 2 | export * from './Defer'; 3 | export * from './go'; 4 | export * from './of'; 5 | -------------------------------------------------------------------------------- /src/thingies/of.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Given a promise awaits it and returns a 3-tuple, with the following members: 3 | * 4 | * - First entry is either the resolved value of the promise or `undefined`. 5 | * - Second entry is either the error thrown by promise or `undefined`. 6 | * - Third entry is a boolean, truthy if promise was resolved and falsy if rejected. 7 | * 8 | * @param promise Promise to convert to 3-tuple. 9 | */ 10 | export const of = async (promise: Promise): Promise<[T | undefined, E | undefined, boolean]> => { 11 | try { 12 | return [await promise, undefined, true]; 13 | } catch (error: unknown) { 14 | return [undefined, error as E, false]; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/thingies/types.ts: -------------------------------------------------------------------------------- 1 | export type Code = () => Promise; 2 | -------------------------------------------------------------------------------- /src/volume-localstorage.ts: -------------------------------------------------------------------------------- 1 | import { Volume } from './volume'; 2 | import { Link, Node } from './node'; 3 | 4 | export interface IStore { 5 | setItem(key: string, json); 6 | getItem(key: string); 7 | removeItem(key: string); 8 | } 9 | 10 | export class ObjectStore { 11 | obj: object; 12 | 13 | constructor(obj) { 14 | this.obj = obj; 15 | } 16 | 17 | setItem(key: string, json) { 18 | this.obj[key] = JSON.stringify(json); 19 | } 20 | 21 | getItem(key: string) { 22 | const data = this.obj[key]; 23 | if (typeof data === void 0) return void 0; 24 | return JSON.parse(data); 25 | } 26 | 27 | removeItem(key: string) { 28 | delete this.obj[key]; 29 | } 30 | } 31 | 32 | export function createVolume(namespace: string, LS: IStore | object = localStorage): new (...args) => Volume { 33 | const store = new ObjectStore(LS); 34 | const key = (type, id) => `memfs.${namespace}.${type}.${id}`; 35 | 36 | class NodeLocalStorage extends Node { 37 | private _key: string; 38 | 39 | get Key(): string { 40 | if (!this._key) this._key = key('ino', this.ino); 41 | return this._key; 42 | } 43 | 44 | sync() { 45 | store.setItem(this.Key, this.toJSON()); 46 | } 47 | 48 | touch() { 49 | super.touch(); 50 | this.sync(); 51 | } 52 | 53 | del() { 54 | super.del(); 55 | store.removeItem(this.Key); 56 | } 57 | } 58 | 59 | class LinkLocalStorage extends Link { 60 | private _key: string; 61 | 62 | get Key(): string { 63 | if (!this._key) this._key = key('link', this.getPath()); 64 | return this._key; 65 | } 66 | 67 | sync() { 68 | store.setItem(this.Key, this.toJSON()); 69 | } 70 | } 71 | 72 | return class VolumeLocalStorage extends Volume { 73 | constructor() { 74 | super({ 75 | Node: NodeLocalStorage, 76 | Link: LinkLocalStorage, 77 | }); 78 | } 79 | 80 | createLink(parent?, name?, isDirectory?, perm?) { 81 | const link = super.createLink(parent, name, isDirectory, perm); 82 | store.setItem(key('link', link.getPath()), link.toJSON()); 83 | return link; 84 | } 85 | 86 | deleteLink(link) { 87 | store.removeItem(key('link', link.getPath())); 88 | return super.deleteLink(link); 89 | } 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/webfs/index.ts: -------------------------------------------------------------------------------- 1 | (self).process = require('process/browser'); 2 | 3 | import { FsaNodeFs, FsaNodeSyncAdapterWorker } from '../fsa-to-node'; 4 | import { FsaNodeSyncWorker } from '../../src/fsa-to-node/worker/FsaNodeSyncWorker'; 5 | import type { IFileSystemDirectoryHandle } from '../fsa/types'; 6 | 7 | if (typeof window === 'object') { 8 | const url = (document.currentScript).src; 9 | const dir = navigator.storage.getDirectory() as unknown as Promise; 10 | const fs = ((window).fs = new FsaNodeFs(dir)); 11 | if (url) { 12 | FsaNodeSyncAdapterWorker.start(url, dir) 13 | .then(adapter => { 14 | fs.syncAdapter = adapter; 15 | }) 16 | .catch(() => {}); 17 | } 18 | } else { 19 | const worker = new FsaNodeSyncWorker(); 20 | worker.start(); 21 | } 22 | -------------------------------------------------------------------------------- /src/webfs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const root = require('app-root-path'); 4 | 5 | const DEV = process.env.NODE_ENV !== 'production'; 6 | 7 | module.exports = { 8 | mode: DEV ? 'development' : 'production', 9 | devtool: DEV ? 'source-map' : false, 10 | entry: { 11 | bundle: __dirname + '/index', 12 | }, 13 | plugins: [ 14 | // new ForkTsCheckerWebpackPlugin(), 15 | new HtmlWebpackPlugin({ 16 | title: 'Development', 17 | }), 18 | ], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.tsx?$/, 23 | exclude: /node_modules/, 24 | loader: 'ts-loader', 25 | }, 26 | ], 27 | }, 28 | resolve: { 29 | extensions: ['.tsx', '.ts', '.js'], 30 | fallback: { 31 | assert: require.resolve('assert'), 32 | buffer: require.resolve('buffer'), 33 | path: require.resolve('path-browserify'), 34 | process: require.resolve('process/browser'), 35 | stream: require.resolve('readable-stream'), 36 | // url: require.resolve('url'), 37 | }, 38 | }, 39 | output: { 40 | filename: '[name].js', 41 | path: path.resolve(root.path, 'dist'), 42 | }, 43 | devServer: { 44 | // HTTPS is required for SharedArrayBuffer to work. 45 | https: true, 46 | headers: { 47 | // These two headers are required for SharedArrayBuffer to work. 48 | 'Cross-Origin-Opener-Policy': 'same-origin', 49 | 'Cross-Origin-Embedder-Policy': 'require-corp', 50 | }, 51 | port: 9876, 52 | hot: false, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["ES2017", "dom"], 5 | "module": "commonjs", 6 | "removeComments": false, 7 | "noImplicitAny": false, 8 | "strictNullChecks": true, 9 | "sourceMap": true, 10 | "outDir": "lib", 11 | "declaration": true, 12 | "skipLibCheck": true, 13 | "noEmitHelpers": true, 14 | "importHelpers": true 15 | }, 16 | "include": ["src"], 17 | "exclude": ["src/__tests__", "node_modules", "lib", "es6", "es2020", "esm", "docs", "README.md"], 18 | "typedocOptions": { 19 | "entryPoints": [ 20 | "src/index.ts", 21 | "src/cas/types.ts", 22 | "src/crud/types.ts", 23 | "src/crud-to-cas/index.ts", 24 | "src/fsa/types.ts", 25 | "src/fsa-to-crud/index.ts", 26 | "src/fsa-to-node/index.ts", 27 | "src/node-to-crud/index.ts", 28 | "src/node-to-fsa/index.ts" 29 | ], 30 | "out": "typedocs" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-common", 3 | "rules": { 4 | "no-invalid-this": false, 5 | "variable-name": false, 6 | "no-inferrable-types": false, 7 | "curly": false, 8 | "forin": false, 9 | "no-dynamic-delete": false, 10 | "unified-signatures": false 11 | } 12 | } 13 | --------------------------------------------------------------------------------