├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── .releaserc ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── index.d.ts ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── CacheFS.js ├── DefaultBackend.js ├── HttpBackend.js ├── IdbBackend.js ├── Mutex.js ├── Mutex2.js ├── PromisifiedFS.js ├── Stat.js ├── __tests__ │ ├── CacheFS.spec.js │ ├── __fixtures__ │ │ ├── test-folder │ │ │ ├── 0 │ │ │ │ ├── a.txt │ │ │ │ ├── b.txt │ │ │ │ └── c.txt │ │ │ ├── 1 │ │ │ │ ├── d.txt │ │ │ │ ├── e.txt │ │ │ │ └── f.txt │ │ │ ├── 2 │ │ │ │ └── a.txt │ │ │ ├── .superblock.txt │ │ │ ├── a.txt │ │ │ ├── b.txt │ │ │ ├── c.txt │ │ │ └── not-in-superblock.txt │ │ └── tree.txt.js │ ├── fallback.spec.js │ ├── fs.promises.spec.js │ ├── fs.spec.js │ ├── hotswap-backends-1.spec.js │ ├── hotswap-backends-2.spec.js │ ├── hotswap-backends-3.spec.js │ ├── hotswap-backends-4.spec.js │ ├── path.spec.js │ ├── threadsafety.spec.js │ └── threadsafety.worker.js ├── clock.js ├── errors.js ├── index.js ├── path.js └── superblocktxt.js └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json binary 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | # Cancel in-progress runs for the current workflow 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | name: Build (Node.js v18) 15 | runs-on: ubuntu-latest 16 | 17 | timeout-minutes: 10 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 18 26 | cache: npm 27 | 28 | - name: Install dependencies 29 | run: npm ci 30 | 31 | - name: Run build 32 | run: | 33 | npm run build 34 | 35 | - name: Run tests 36 | run: | 37 | npm test 38 | env: 39 | BROWSER_STACK_ACCESS_KEY: ${{ secrets.BROWSER_STACK_ACCESS_KEY }} 40 | BROWSER_STACK_USERNAME: ${{ secrets.BROWSER_STACK_USERNAME }} 41 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 42 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 43 | TEST_BROWSERS: 'ChromeHeadlessNoSandbox,FirefoxHeadless,sl_edge,sl_safari,sl_ios_safari,bs_android_chrome' 44 | 45 | - name: Save test results 46 | if: ${{ !cancelled() }} 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: test-results-jest 50 | path: junit/*.xml 51 | 52 | - name: Prepare installable tarball 53 | if: ${{ !cancelled() && !github.event.pull_request.head.repo.fork }} 54 | run: | 55 | npm pack 56 | 57 | - name: Save npm-tarball.tgz 58 | if: ${{ !cancelled() && !github.event.pull_request.head.repo.fork }} 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: npm-tarball.tgz 62 | path: isomorphic-git-lightning-fs-0.0.0-development.tgz 63 | 64 | - name: Publish to npm 65 | if: ${{ github.ref == 'refs/heads/beta'}} 66 | # if: ${{ github.ref == 'refs/heads/main' || github.ref_name == 'refs/heads/beta' }} 67 | run: | 68 | npm run semantic-release 69 | env: 70 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | junit 5 | *-0.0.0-development.tgz 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "trailingComma": "es5", 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | branches: 2 | - '+([0-9])?(.{+([0-9]),x}).x' 3 | - 'main' 4 | - 'next' 5 | - 'next-major' 6 | - name: 'beta' 7 | prerelease: true 8 | - name: 'alpha' 9 | prerelease: true 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wmhilton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @isomorphic-git/lightning-fs 2 | 3 | A lean and fast 'fs' for the browser 4 | 5 | ## Motivation 6 | 7 | I wanted to see if I could make something faster than [BrowserFS](https://github.com/jvilk/BrowserFS) or [filer](https://github.com/filerjs/filer) that still implements enough of the `fs` API to run the [`isomorphic-git`](https://github.com/isomorphic-git/isomorphic-git) test suite in browsers. 8 | 9 | ## Comparison with other libraries 10 | 11 | This library does not even come close to implementing the full [`fs`](https://nodejs.org/api/fs.html) API. 12 | Instead, it only implements [the subset used by isomorphic-git 'fs' plugin interface](https://isomorphic-git.org/docs/en/plugin_fs) plus the [`fs.promises`](https://nodejs.org/dist/latest-v10.x/docs/api/fs.html#fs_fs_promises_api) versions of those functions. 13 | 14 | Unlike BrowserFS, which has a dozen backends and is highly configurable, `lightning-fs` has a single configuration that should Just Work for most users. 15 | 16 | ## Philosophy 17 | 18 | ### Basic requirements: 19 | 20 | 1. needs to work in all modern browsers 21 | 2. needs to work with large-ish files and directories 22 | 3. needs to persist data 23 | 4. needs to enable performant web apps 24 | 25 | Req #3 excludes pure in-memory solutions. Req #4 excludes `localStorage` because it blocks the DOM and cannot be run in a webworker. Req #1 excludes WebSQL and Chrome's FileSystem API. So that leaves us with IndexedDB as the only usable storage technology. 26 | 27 | ### Optimization targets (in order of priority): 28 | 29 | 1. speed (time it takes to execute file system operations) 30 | 2. bundle size (time it takes to download the library) 31 | 3. memory usage (will it work on mobile) 32 | 33 | In order to get improve #1, I ended up making a hybrid in-memory / IndexedDB system: 34 | - `mkdir`, `rmdir`, `readdir`, `rename`, and `stat` are pure in-memory operations that take 0ms 35 | - `writeFile`, `readFile`, and `unlink` are throttled by IndexedDB 36 | 37 | The in-memory portion of the filesystem is persisted to IndexedDB with a debounce of 500ms. 38 | The files themselves are not currently cached in memory, because I don't want to waste a lot of memory. 39 | Applications can always *add* an LRU cache on top of `lightning-fs` - if I add one internally and it isn't tuned well for your application, it might be much harder to work around. 40 | 41 | ### Multi-threaded filesystem access 42 | 43 | Multiple tabs (and web workers) can share a filesystem. However, because SharedArrayBuffer is still not available in most browsers, the in-memory cache that makes LightningFS fast cannot be shared. If each thread was allowed to update its cache independently, then you'd have a complex distributed system and would need a fancy algorithm to resolve conflicts. Instead, I'm counting on the fact that your multi-threaded applications will NOT be IO bound, and thus a simpler strategy for sharing the filesystem will work. Filesystem access is bottlenecked by a mutex (implemented via polling and an atomic compare-and-replace operation in IndexedDB) to ensure that only one thread has access to the filesystem at a time. If the active thread is constantly using the filesystem, no other threads will get a chance. However if the active thread's filesystem goes idle - no operations are pending and no new operations are started - then after 500ms its in-memory cache is serialized and saved to IndexedDB and the mutex is released. (500ms was chosen experimentally such that an [isomorphic-git](https://github.com/isomorphic-git/isomorphic-git) `clone` operation didn't thrash the mutex.) 44 | 45 | While the mutex is being held by another thread, any fs operations will be stuck waiting until the mutex becomes available. If the mutex is not available even after ten minutes then the filesystem operations will fail with an error. This could happen if say, you are trying to write to a log file every 100ms. You can overcome this by making sure that the filesystem is allowed to go idle for >500ms every now and then. 46 | 47 | ## Usage 48 | 49 | ### `new FS(name, opts?)` 50 | First, create or open a "filesystem". (The name is used to determine the IndexedDb store name.) 51 | 52 | ```js 53 | import FS from '@isomorphic-git/lightning-fs'; 54 | 55 | const fs = new FS("testfs") 56 | ``` 57 | 58 | **Note: It is better not to create multiple `FS` instances using the same name in a single thread.** Memory usage will be higher as each instance maintains its own cache, and throughput may be lower as each instance will have to compete over the mutex for access to the IndexedDb store. 59 | 60 | Options object: 61 | 62 | | Param | Type [= default] | Description | 63 | | --------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 64 | | `wipe` | boolean = false | Delete the database and start with an empty filesystem | 65 | | `url` | string = undefined | Let `readFile` requests fall back to an HTTP request to this base URL | 66 | | `urlauto` | boolean = false | Fall back to HTTP for every read of a missing file, even if unbacked | 67 | | `fileDbName` | string | Customize the database name | 68 | | `fileStoreName` | string | Customize the store name | 69 | | `lockDbName` | string | Customize the database name for the lock mutex | 70 | | `lockStoreName` | string | Customize the store name for the lock mutex | 71 | | `defer` | boolean = false | If true, avoids mutex contention during initialization | 72 | | `db` | IDB | Replacement for DB object that hold Filesystem data. It's low level replacement for `backend` option. | 73 | | `backend` | IBackend | If present, none of the other arguments (except `defer`) have any effect, and instead of using the normal LightningFS stuff, LightningFS acts as a wrapper around the provided custom backend. | 74 | 75 | 76 | #### Advanced usage 77 | 78 | You can procrastinate initializing the FS object until later. 79 | And, if you're really adventurous, you can _re-initialize_ it with a different name to switch between IndexedDb databases. 80 | 81 | ```js 82 | import FS from '@isomorphic-git/lightning-fs'; 83 | 84 | const fs = new FS() 85 | 86 | // Some time later... 87 | fs.init(name, options) 88 | 89 | // Some time later... 90 | fs.init(different_name, different_options) 91 | ``` 92 | 93 | ### `fs.mkdir(filepath, opts?, cb)` 94 | 95 | Make directory 96 | 97 | Options object: 98 | 99 | | Param | Type [= default] | Description | 100 | | ------ | ---------------- | ---------------------- | 101 | | `mode` | number = 0o777 | Posix mode permissions | 102 | 103 | ### `fs.rmdir(filepath, opts?, cb)` 104 | 105 | Remove directory 106 | 107 | ### `fs.readdir(filepath, opts?, cb)` 108 | 109 | Read directory 110 | 111 | The callback return value is an Array of strings. NOTE: _To save time, it is NOT SORTED._ (Fun fact: Node.js' `readdir` output is not guaranteed to be sorted either. I learned that the hard way.) 112 | 113 | ### `fs.writeFile(filepath, data, opts?, cb)` 114 | 115 | `data` should be a string of a Uint8Array. 116 | 117 | If `opts` is a string, it is interpreted as `{ encoding: opts }`. 118 | 119 | Options object: 120 | 121 | | Param | Type [= default] | Description | 122 | | ---------- | ------------------ | -------------------------------- | 123 | | `mode` | number = 0o777 | Posix mode permissions | 124 | | `encoding` | string = undefined | Only supported value is `'utf8'` | 125 | 126 | ### `fs.readFile(filepath, opts?, cb)` 127 | 128 | The result value will be a Uint8Array or (if `encoding` is `'utf8'`) a string. 129 | 130 | If `opts` is a string, it is interpreted as `{ encoding: opts }`. 131 | 132 | Options object: 133 | 134 | | Param | Type [= default] | Description | 135 | | ---------- | ------------------ | -------------------------------- | 136 | | `encoding` | string = undefined | Only supported value is `'utf8'` | 137 | 138 | ### `fs.unlink(filepath, opts?, cb)` 139 | 140 | Delete a file 141 | 142 | ### `fs.rename(oldFilepath, newFilepath, cb)` 143 | 144 | Rename a file or directory 145 | 146 | ### `fs.stat(filepath, opts?, cb)` 147 | 148 | The result is a Stat object similar to the one used by Node but with fewer and slightly different properties and methods. 149 | The included properties are: 150 | 151 | - `type` ("file" or "dir") 152 | - `mode` 153 | - `size` 154 | - `ino` 155 | - `mtimeMs` 156 | - `ctimeMs` 157 | - `uid` (fixed value of 1) 158 | - `gid` (fixed value of 1) 159 | - `dev` (fixed value of 1) 160 | 161 | The included methods are: 162 | - `isFile()` 163 | - `isDirectory()` 164 | - `isSymbolicLink()` 165 | 166 | ### `fs.lstat(filepath, opts?, cb)` 167 | 168 | Like `fs.stat` except that paths to symlinks return the symlink stats not the file stats of the symlink's target. 169 | 170 | ### `fs.symlink(target, filepath, cb)` 171 | 172 | Create a symlink at `filepath` that points to `target`. 173 | 174 | ### `fs.readlink(filepath, opts?, cb)` 175 | 176 | Read the target of a symlink. 177 | 178 | ### `fs.backFile(filepath, opts?, cb)` 179 | 180 | Create or change the stat data for a file backed by HTTP. Size is fetched with a HEAD request. Useful when using an HTTP backend without `urlauto` set, as then files will only be readable if they have stat data. 181 | Note that stat data is made automatically from the file `/.superblock.txt` if found on the server. `/.superblock.txt` can be generated or updated with the [included standalone script](src/superblocktxt.js). 182 | 183 | Options object: 184 | 185 | | Param | Type [= default] | Description | 186 | | ------ | ---------------- | ---------------------- | 187 | | `mode` | number = 0o666 | Posix mode permissions | 188 | 189 | ### `fs.du(filepath, cb)` 190 | 191 | Returns the size of a file or directory in bytes. 192 | 193 | ### `fs.promises` 194 | 195 | All the same functions as above, but instead of passing a callback they return a promise. 196 | 197 | ## Providing a custom `backend` (advanced usage) 198 | 199 | There are only two reasons I can think of that you would want to do this: 200 | 201 | 1. The `fs` module is normally a singleton. LightningFS allows you to safely(ish) hotswap between various data sources by calling `init` multiple times with different options. (It keeps track of file system operations in flight and waits until there's an idle moment to do the switch.) 202 | 203 | 2. LightningFS normalizes all the lovely variations of node's `fs` arguments: 204 | 205 | - `fs.writeFile('filename.txt', 'Hello', cb)` 206 | - `fs.writeFile('filename.txt', 'Hello', 'utf8', cb)` 207 | - `fs.writeFile('filename.txt', 'Hello', { encoding: 'utf8' }, cb)` 208 | - `fs.promises.writeFile('filename.txt', 'Hello')` 209 | - `fs.promises.writeFile('filename.txt', 'Hello', 'utf8')` 210 | - `fs.promises.writeFile('filename.txt', 'Hello', { encoding: 'utf8' })` 211 | 212 | And it normalizes filepaths. And will convert plain `StatLike` objects into `Stat` objects with methods like `isFile`, `isDirectory`, etc. 213 | 214 | If that fits your needs, then you can provide a `backend` option and LightningFS will use that. Implement as few/many methods as you need for your application to work. 215 | 216 | **Note:** If you use a custom backend, you are responsible for managing multi-threaded access - there are no magic mutexes included by default. 217 | 218 | Note: throwing an error with the correct `.code` property for any given situation is often important for utilities like `mkdirp` and `rimraf` to work. 219 | 220 | ```tsx 221 | 222 | type EncodingOpts = { 223 | encoding?: 'utf8'; 224 | } 225 | 226 | type StatLike = { 227 | type: 'file' | 'dir' | 'symlink'; 228 | mode: number; 229 | size: number; 230 | ino: number | string | BigInt; 231 | mtimeMs: number; 232 | ctimeMs?: number; 233 | } 234 | 235 | interface IBackend { 236 | // highly recommended - usually necessary for apps to work 237 | readFile(filepath: string, opts: EncodingOpts): Awaited; // throws ENOENT 238 | writeFile(filepath: string, data: Uint8Array | string, opts: EncodingOpts): void; // throws ENOENT 239 | unlink(filepath: string, opts: any): void; // throws ENOENT 240 | readdir(filepath: string, opts: any): Awaited; // throws ENOENT, ENOTDIR 241 | mkdir(filepath: string, opts: any): void; // throws ENOENT, EEXIST 242 | rmdir(filepath: string, opts: any): void; // throws ENOENT, ENOTDIR, ENOTEMPTY 243 | 244 | // recommended - often necessary for apps to work 245 | stat(filepath: string, opts: any): Awaited; // throws ENOENT 246 | lstat(filepath: string, opts: any): Awaited; // throws ENOENT 247 | 248 | // suggested - used occasionally by apps 249 | rename(oldFilepath: string, newFilepath: string): void; // throws ENOENT 250 | readlink(filepath: string, opts: any): Awaited; // throws ENOENT 251 | symlink(target: string, filepath: string): void; // throws ENOENT 252 | 253 | // bonus - not part of the standard `fs` module 254 | backFile(filepath: string, opts: any): void; 255 | du(filepath: string): Awaited; 256 | 257 | // lifecycle - useful if your backend needs setup and teardown 258 | init?(name: string, opts: any): Awaited; // passes initialization options 259 | activate?(): Awaited; // called before fs operations are started 260 | deactivate?(): Awaited; // called after fs has been idle for a while 261 | destroy?(): Awaited; // called before hotswapping backends 262 | } 263 | 264 | interface IDB { 265 | saveSuperblock(superblock): void; 266 | loadSuperblock(): Awaited; 267 | readFile(inode): Awaited; 268 | writeFile(inode, data): Awaited; 269 | unlink(inode): Awaited; 270 | wipe(): void; 271 | close(): void; 272 | } 273 | 274 | ``` 275 | 276 | ## License 277 | 278 | MIT 279 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | system.debug: true 3 | 4 | jobs: 5 | - job: Linux 6 | 7 | pool: 8 | vmImage: 'ubuntu-latest' 9 | 10 | steps: 11 | - task: NodeTool@0 12 | inputs: 13 | versionSpec: '18.x' 14 | displayName: 'Install Node.js' 15 | 16 | - script: npm ci 17 | displayName: 'Install dependencies' 18 | 19 | - script: npm run build 20 | displayName: 'Webpack build' 21 | 22 | - script: npm test 23 | displayName: 'Run tests' 24 | env: 25 | BROWSER_STACK_ACCESS_KEY: $(BROWSER_STACK_ACCESS_KEY) 26 | BROWSER_STACK_USERNAME: $(BROWSER_STACK_USERNAME) 27 | BUNDLEWATCH_GITHUB_TOKEN: $(BUNDLEWATCH_GITHUB_TOKEN) 28 | SAUCE_ACCESS_KEY: $(SAUCE_ACCESS_KEY) 29 | SAUCE_USERNAME: $(SAUCE_USERNAME) 30 | TEST_BROWSERS: 'ChromeHeadlessNoSandbox,FirefoxHeadless,sl_edge,sl_safari,sl_ios_safari,bs_android_chrome' 31 | 32 | - task: PublishTestResults@2 33 | displayName: 'Save test results' 34 | condition: succeededOrFailed() 35 | inputs: 36 | testResultsFormat: JUnit 37 | testResultsFiles: '$(System.DefaultWorkingDirectory)/junit/*.xml' 38 | 39 | - task: PublishCodeCoverageResults@1 40 | displayName: 'Save code coverage' 41 | condition: and(succeededOrFailed(), ne(variables['system.pullrequest.isfork'], true)) 42 | inputs: 43 | codeCoverageTool: Cobertura 44 | summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml' 45 | reportDirectory: '$(System.DefaultWorkingDirectory)/coverage/lcov-report' 46 | 47 | - script: npm pack 48 | displayName: 'Prepare installable tarball' 49 | condition: succeededOrFailed() 50 | 51 | - task: PublishBuildArtifacts@1 52 | displayName: 'Save npm-tarball.tgz' 53 | condition: and(succeededOrFailed(), ne(variables['system.pullrequest.isfork'], true)) 54 | inputs: 55 | artifactName: 'npm-tarball.tgz' 56 | PathtoPublish: '$(System.DefaultWorkingDirectory)/isomorphic-git-lightning-fs-0.0.0-development.tgz' 57 | 58 | - script: npm run semantic-release 59 | displayName: 'Publish to npm' 60 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) 61 | env: 62 | GH_TOKEN: $(GITHUB_TOKEN) 63 | NPM_TOKEN: $(Npm.Token) 64 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@isomorphic-git/lightning-fs' { 2 | class FS { 3 | /** 4 | * You can procrastinate initializing the FS object until later. And, if you're really adventurous, you can re-initialize it with a different name to switch between IndexedDb databases. 5 | */ 6 | constructor() 7 | 8 | /** 9 | * First, create or open a "filesystem". 10 | * @param name This is used to determine the IndexedDb store name. 11 | * @param options The "filesystem" configuration. 12 | */ 13 | constructor(name: string, options?: FS.Options) 14 | 15 | /** 16 | * 17 | * @param name This is used to determine the IndexedDb store name. 18 | * @param options The "filesystem" configuration. 19 | */ 20 | init(name: string, options?: FS.Options): void 21 | 22 | /** 23 | * Make directory 24 | * @param filepath 25 | * @param options 26 | * @param cb 27 | */ 28 | mkdir(filepath: string, options: FS.MKDirOptions | undefined, cb: (err: Error) => void): void 29 | 30 | /** 31 | * Remove directory 32 | * @param filepath 33 | * @param options 34 | * @param cb 35 | */ 36 | rmdir(filepath: string, options: undefined, cb: (err: Error) => void): void 37 | 38 | /** 39 | * Read directory 40 | * 41 | * The callback return value is an Array of strings. NOTE: To save time, it is NOT SORTED. (Fun fact: Node.js' readdir output is not guaranteed to be sorted either. I learned that the hard way.) 42 | * @param filepath 43 | * @param options 44 | * @param cb 45 | */ 46 | readdir(filepath: string, options: undefined, cb: (err: Error, files: string[]) => void): void 47 | 48 | writeFile(filepath: string, data: Uint8Array | string, options: FS.WriteFileOptions | undefined | string, cb: (err: Error) => void): void 49 | 50 | readFile(filepath: string, options: 'utf8' | { encoding: 'utf8' }, cb: (err: Error, data: string) => void): void 51 | readFile(filepath: string, options: {} | void, cb: (err: Error, data: Uint8Array) => void): void 52 | 53 | /** 54 | * Delete a file 55 | * @param filepath 56 | * @param options 57 | * @param cb 58 | */ 59 | unlink(filepath: string, options: undefined, cb: (err: Error) => void): void 60 | 61 | /** 62 | * Rename a file or directory 63 | * @param oldFilepath 64 | * @param newFilepath 65 | * @param cb 66 | */ 67 | rename(oldFilepath: string, newFilepath: string, cb: (err: Error) => void): void 68 | 69 | /** 70 | * The result is a Stat object similar to the one used by Node but with fewer and slightly different properties and methods. 71 | * @param filepath 72 | * @param options 73 | * @param cb 74 | */ 75 | stat(filepath: string, options: undefined, cb: (err: Error, stats: FS.Stats) => void): void 76 | 77 | /** 78 | * Like fs.stat except that paths to symlinks return the symlink stats not the file stats of the symlink's target. 79 | * @param filepath 80 | * @param options 81 | * @param cb 82 | */ 83 | lstat(filepath: string, options: undefined, cb: (err: Error, stats: FS.Stats) => void): void 84 | 85 | /** 86 | * Create a symlink at filepath that points to target. 87 | * @param target 88 | * @param filepath 89 | * @param cb 90 | */ 91 | symlink(target: string, filepath: string, cb: (err: Error) => void): void 92 | 93 | /** 94 | * Read the target of a symlink. 95 | * @param filepath 96 | * @param options 97 | * @param cb 98 | */ 99 | readlink(filepath: string, options: undefined, cb: (err: Error, linkString: string) => void): void 100 | 101 | /** 102 | * Create or change the stat data for a file backed by HTTP. Size is fetched with a HEAD request. Useful when using an HTTP backend without urlauto set, as then files will only be readable if they have stat data. Note that stat data is made automatically from the file /.superblock.txt if found on the server. /.superblock.txt can be generated or updated with the included [standalone script](https://github.com/isomorphic-git/lightning-fs/blob/main/src/superblocktxt.js). 103 | * @param filepath 104 | * @param options 105 | * @param cb 106 | */ 107 | backFile(filepath: string, options: FS.BackFileOptions | undefined, cb: (err: Error) => void): void 108 | 109 | /** 110 | * Returns the size of a file or directory in bytes. 111 | * @param filepath 112 | * @param cb 113 | */ 114 | du(filepath: string, cb: (err: Error, size: number) => void): void 115 | 116 | readonly promises: FS.PromisifiedFS 117 | } 118 | namespace FS { 119 | export class PromisifiedFS { 120 | /** 121 | * 122 | * @param name This is used to determine the IndexedDb store name. 123 | * @param options The "filesystem" configuration. 124 | */ 125 | init(name: string, options?: FS.Options): void 126 | 127 | /** 128 | * Make directory 129 | * @param filepath 130 | * @param options 131 | */ 132 | mkdir(filepath: string, options?: FS.MKDirOptions): Promise 133 | 134 | /** 135 | * Remove directory 136 | * @param filepath 137 | * @param options 138 | */ 139 | rmdir(filepath: string, options?: undefined): Promise 140 | 141 | /** 142 | * Read directory 143 | * 144 | * The Promise return value is an Array of strings. NOTE: To save time, it is NOT SORTED. (Fun fact: Node.js' readdir output is not guaranteed to be sorted either. I learned that the hard way.) 145 | * @param filepath 146 | * @param options 147 | * @returns The file list. 148 | */ 149 | readdir(filepath: string, options?: undefined): Promise 150 | 151 | writeFile(filepath: string, data: Uint8Array | string, options?: FS.WriteFileOptions | string): Promise 152 | 153 | readFile(filepath: string, options: 'utf8' | { encoding: 'utf8' }): Promise 154 | readFile(filepath: string, options?: {}): Promise 155 | 156 | /** 157 | * Delete a file 158 | * @param filepath 159 | * @param options 160 | */ 161 | unlink(filepath: string, options?: undefined): Promise 162 | 163 | /** 164 | * Rename a file or directory 165 | * @param oldFilepath 166 | * @param newFilepath 167 | */ 168 | rename(oldFilepath: string, newFilepath: string): Promise 169 | 170 | /** 171 | * The result is a Stat object similar to the one used by Node but with fewer and slightly different properties and methods. 172 | * @param filepath 173 | * @param options 174 | */ 175 | stat(filepath: string, options?: undefined): Promise 176 | 177 | /** 178 | * Like fs.stat except that paths to symlinks return the symlink stats not the file stats of the symlink's target. 179 | * @param filepath 180 | * @param options 181 | */ 182 | lstat(filepath: string, options?: undefined): Promise 183 | 184 | /** 185 | * Create a symlink at filepath that points to target. 186 | * @param target 187 | * @param filepath 188 | */ 189 | symlink(target: string, filepath: string): Promise 190 | 191 | /** 192 | * Read the target of a symlink. 193 | * @param filepath 194 | * @param options 195 | * @returns The link string. 196 | */ 197 | readlink(filepath: string, options?: undefined): Promise 198 | 199 | /** 200 | * Create or change the stat data for a file backed by HTTP. Size is fetched with a HEAD request. Useful when using an HTTP backend without urlauto set, as then files will only be readable if they have stat data. Note that stat data is made automatically from the file /.superblock.txt if found on the server. /.superblock.txt can be generated or updated with the included [standalone script](https://github.com/isomorphic-git/lightning-fs/blob/main/src/superblocktxt.js). 201 | * @param filepath 202 | * @param options 203 | */ 204 | backFile(filepath: string, options?: FS.BackFileOptions): Promise 205 | 206 | /** 207 | * @param filepath 208 | * @returns The size of a file or directory in bytes. 209 | */ 210 | du(filepath: string): Promise 211 | /** 212 | * Function that saves anything that need to be saved in IndexedBD or custom IDB object. Right now it saves SuperBlock so it's save to dump the object as dump it into a file (e.g. from a Browser) 213 | * @returns Promise that resolves when super block is saved to file 214 | */ 215 | flush(): Promise 216 | } 217 | 218 | export interface Options { 219 | /** 220 | * Delete the database and start with an empty filesystem 221 | * @default false 222 | */ 223 | wipe?: boolean 224 | /** 225 | * Let readFile requests fall back to an HTTP request to this base URL 226 | * @default false 227 | */ 228 | url?: string 229 | /** 230 | * Fall back to HTTP for every read of a missing file, even if unbacked 231 | * @default false 232 | */ 233 | urlauto?: boolean 234 | /** 235 | * Customize the database name 236 | */ 237 | fileDbName?: string 238 | /** 239 | * Customize the store name 240 | */ 241 | fileStoreName?: string 242 | /** 243 | * Customize the database name for the lock mutex 244 | */ 245 | lockDbName?: string 246 | /** 247 | * Customize the store name for the lock mutex 248 | */ 249 | lockStoreName?: string 250 | /** 251 | * If true, avoids mutex contention during initialization 252 | * @default false 253 | */ 254 | defer?: boolean 255 | /** 256 | * Custom database instance 257 | * @default null 258 | */ 259 | db?: FS.IDB 260 | } 261 | export interface IDB { 262 | constructor(dbname: string, storename: string): IDB 263 | saveSuperblock(sb: Uint8Array): TypeOrPromise 264 | loadSuperblock(): TypeOrPromise 265 | loadFile(inode: number): TypeOrPromise 266 | writeFile(inode: number, data: Uint8Array): TypeOrPromise 267 | wipe(): TypeOrPromise 268 | close(): TypeOrPromise 269 | } 270 | type TypeOrPromise = T | Promise 271 | export type SuperBlock = Map 272 | export interface MKDirOptions { 273 | /** 274 | * Posix mode permissions 275 | * @default 0o777 276 | */ 277 | mode: number 278 | } 279 | export interface WriteFileOptions { 280 | /** 281 | * Posix mode permissions 282 | * @default 0o777 283 | */ 284 | mode: number 285 | encoding?: 'utf8' 286 | } 287 | export interface ReadFileOptions { 288 | encoding?: 'utf8' 289 | } 290 | export interface Stats { 291 | type: 'file' | 'dir' 292 | mode: any 293 | size: number 294 | ino: any 295 | mtimeMs: any 296 | ctimeMs: any 297 | uid: 1 298 | gid: 1 299 | dev: 1 300 | isFile(): boolean 301 | isDirectory(): boolean 302 | isSymbolicLink(): boolean 303 | } 304 | export interface BackFileOptions { 305 | /** 306 | * Posix mode permissions 307 | * @default 0o666 308 | */ 309 | mode: number 310 | } 311 | } 312 | export = FS 313 | } 314 | 315 | declare module '@isomorphic-git/lightning-fs/src/path' { 316 | namespace Path { 317 | function join(...parts: string[]): string 318 | function normalize(path: string): string 319 | function split(path: string): string[] 320 | function basename(path: string): string 321 | function dirname(path: string): string 322 | function resolve(...paths: string[]): string 323 | } 324 | export = Path 325 | } 326 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | process.env.CHROME_BIN = require('puppeteer').executablePath() 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | 6 | const REPO = process.env.BUILD_REPOSITORY_NAME 7 | const ISSUE = 8 | process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER || 9 | process.env.SYSTEM_PULLREQUEST_PULLREQUESTID 10 | const COMMIT = process.env.BUILD_SOURCEVERSION 11 | 12 | module.exports = function (config) { 13 | const options = { 14 | // start these browsers 15 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 16 | browsers: [], 17 | // base path that will be used to resolve all patterns (eg. files, exclude) 18 | basePath: '', 19 | // frameworks to use 20 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 21 | frameworks: ['jasmine'], 22 | // list of files / patterns to load in the browser 23 | files: [ 24 | 'src/**/*.spec.js', 25 | { 26 | pattern: 'src/__tests__/__fixtures__/test-folder/**/*', 27 | served: true, 28 | watched: false, 29 | included: false 30 | }, 31 | { 32 | pattern: 'src/__tests__/__fixtures__/test-folder/**/.*', 33 | served: true, 34 | watched: false, 35 | included: false 36 | }, 37 | { 38 | pattern: 'src/**/*.worker.js', 39 | served: true, 40 | watched: true, 41 | included: false 42 | }, 43 | { 44 | pattern: 'dist/**', 45 | served: true, 46 | watched: true, 47 | included: false 48 | }, 49 | ], 50 | // list of files to exclude 51 | // exclude: [ 52 | // '**/node_modules/**', 53 | // ], 54 | // preprocess matching files before serving them to the browser 55 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 56 | preprocessors: { 57 | 'src/**/*.spec.js': ['webpack'] 58 | }, 59 | // web server port 60 | port: 9876, 61 | // enable / disable colors in the output (reporters and logs) 62 | colors: true, 63 | // Increase timeouts since some actions take quite a while. 64 | browserNoActivityTimeout: 4 * 60 * 1000, // default 10000 65 | // https://support.saucelabs.com/hc/en-us/articles/225104707-Karma-Tests-Disconnect-Particularly-When-Running-Tests-on-Safari 66 | browserDisconnectTimeout: 10000, // default 2000 67 | browserDisconnectTolerance: 0, // default 0 68 | captureTimeout: 4 * 60 * 1000, // default 60000 69 | // SauceLabs browsers 70 | customLaunchers: { 71 | XXXsl_chrome: { 72 | base: 'SauceLabs', 73 | browserName: 'chrome', 74 | extendedDebugging: true, 75 | }, 76 | XXXsl_firefox: { 77 | base: 'SauceLabs', 78 | browserName: 'firefox', 79 | }, 80 | sl_edge: { 81 | base: 'SauceLabs', 82 | browserName: 'MicrosoftEdge', 83 | version: '79.0', 84 | }, 85 | sl_safari: { 86 | base: 'SauceLabs', 87 | browserName: 'safari', 88 | platform: 'macOS 11.00', 89 | version: '14', 90 | }, 91 | sl_ios_safari: { 92 | base: 'SauceLabs', 93 | deviceName: 'iPhone 11 Pro Max Simulator', 94 | platformName: 'iOS', 95 | platformVersion: '14.0', 96 | browserName: 'Safari', 97 | appiumVersion: '1.18.3', 98 | }, 99 | XXXsl_android_chrome: { 100 | base: 'SauceLabs', 101 | deviceOrientation: 'portrait', 102 | deviceName: 'Android GoogleAPI Emulator', 103 | platformName: 'Android', 104 | platformVersion: '7.1', 105 | browserName: 'Chrome', 106 | appiumVersion: '1.15.0', 107 | }, 108 | bs_android_chrome: { 109 | base: 'BrowserStack', 110 | os: 'android', 111 | os_version: '10.0', 112 | browser: 'android', 113 | device: 'Google Pixel 4', 114 | real_mobile: true, 115 | }, 116 | FirefoxHeadless: { 117 | base: 'Firefox', 118 | flags: ['-headless'], 119 | }, 120 | ChromeHeadlessNoSandbox: { 121 | base: 'ChromeHeadless', 122 | flags: ['--no-sandbox'], 123 | }, 124 | ChromeCanaryHeadlessNoSandbox: { 125 | base: 'ChromeCanaryHeadless', 126 | flags: ['--no-sandbox'], 127 | }, 128 | }, 129 | sauceLabs: { 130 | // Since tags aren't being sent correctly, I'm going to stick the branch name in here. 131 | testName: `${REPO} / ${ISSUE} / ${COMMIT}`, 132 | // Note: I added the Date.now() bit so that when I can click "Restart" on a Travis job, 133 | // Sauce Labs does not simply append new test results to the old set that failed, which 134 | // convinces karma that it failed again and always. 135 | build: process.env.BUILD_BUILDID + '-' + Date.now(), 136 | // Note: it does not appear that tags are being sent correctly. 137 | tags: [ISSUE], 138 | recordScreenshots: false, 139 | recordVideo: false, 140 | public: 'public restricted', 141 | }, 142 | concurrency: 5, 143 | // Continuous Integration mode 144 | // if true, Karma captures browsers, runs the tests and exits 145 | singleRun: true, 146 | // test results reporter to use 147 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 148 | reporters: ['verbose', 'junit'], 149 | junitReporter: { 150 | outputDir: './junit' 151 | }, 152 | webpack: { 153 | mode: 'development', 154 | devtool: 'inline-source-map', 155 | }, 156 | plugins: [ 157 | 'karma-browserstack-launcher', 158 | 'karma-chrome-launcher', 159 | 'karma-edge-launcher', 160 | 'karma-ie-launcher', 161 | 'karma-safari-launcher', 162 | 'karma-firefox-launcher', 163 | 'karma-jasmine', 164 | 'karma-junit-reporter', 165 | 'karma-sauce-launcher', 166 | 'karma-verbose-reporter', 167 | 'karma-webpack', 168 | ] 169 | } 170 | 171 | if (!process.env.SAUCE_USERNAME) { 172 | console.log( 173 | 'Skipping SauceLabs tests because SAUCE_USERNAME environment variable is not set.' 174 | ) 175 | } else if (!process.env.SAUCE_ACCESS_KEY) { 176 | console.log( 177 | 'Skipping SauceLabs tests because SAUCE_ACCESS_KEY environment variable is not set.' 178 | ) 179 | } else { 180 | options.reporters.push('saucelabs') 181 | } 182 | 183 | if (process.env.TEST_BROWSERS) { 184 | options.browsers = process.env.TEST_BROWSERS.split(',') 185 | } else { 186 | options.browsers.push('ChromeHeadlessNoSandbox') 187 | options.browsers.push('FirefoxHeadless') 188 | } 189 | 190 | console.log('running with browsers:', options.browsers) 191 | 192 | if (!process.env.CI) { 193 | // Continuous Integration mode 194 | // if true, Karma captures browsers, runs the tests and exits 195 | options.singleRun = false 196 | // enable / disable watching file and executing tests whenever any file changes 197 | options.autoWatch = true 198 | } 199 | 200 | config.set(options) 201 | } 202 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publishConfig": { 3 | "access": "public" 4 | }, 5 | "name": "@isomorphic-git/lightning-fs", 6 | "version": "0.0.0-development", 7 | "description": "A lean and fast 'fs' for the browser", 8 | "main": "src/index.js", 9 | "unpkg": "dist/lightning-fs.min.js", 10 | "bin": { 11 | "superblocktxt": "src/superblocktxt.js" 12 | }, 13 | "scripts": { 14 | "test": "karma start --single-run", 15 | "build": "webpack", 16 | "semantic-release": "semantic-release" 17 | }, 18 | "dependencies": { 19 | "@isomorphic-git/idb-keyval": "3.3.2", 20 | "isomorphic-textencoder": "1.0.1", 21 | "just-debounce-it": "1.1.0", 22 | "just-once": "1.1.0" 23 | }, 24 | "devDependencies": { 25 | "karma": "^6.4.2", 26 | "karma-browserstack-launcher": "^1.5.1", 27 | "karma-chrome-launcher": "3.1.0", 28 | "karma-edge-launcher": "^0.4.2", 29 | "karma-fail-fast-reporter": "1.0.5", 30 | "karma-firefox-launcher": "1.2.0", 31 | "karma-ie-launcher": "1.0.0", 32 | "karma-jasmine": "2.0.1", 33 | "karma-junit-reporter": "1.2.0", 34 | "karma-safari-launcher": "^1.0.0", 35 | "karma-sauce-launcher": "^1.2.0", 36 | "karma-verbose-reporter": "0.0.6", 37 | "karma-webpack": "^5.0.0", 38 | "prettier": "^1.15.3", 39 | "puppeteer": "^1.10.0", 40 | "semantic-release": "^21.0.9", 41 | "webpack": "^5.88.2", 42 | "webpack-cli": "^5.1.4" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/isomorphic-git/lightning-fs.git" 47 | }, 48 | "files": [ 49 | "**/*.js", 50 | "index.d.ts", 51 | "!__tests__", 52 | "!coverage" 53 | ], 54 | "keywords": [ 55 | "browser", 56 | "fs", 57 | "indexeddb", 58 | "idb" 59 | ], 60 | "author": "William Hilton ", 61 | "license": "MIT", 62 | "bugs": { 63 | "url": "https://github.com/isomorphic-git/lightning-fs/issues" 64 | }, 65 | "homepage": "https://github.com/isomorphic-git/lightning-fs#readme" 66 | } 67 | -------------------------------------------------------------------------------- /src/CacheFS.js: -------------------------------------------------------------------------------- 1 | const path = require("./path.js"); 2 | const { EEXIST, ENOENT, ENOTDIR, ENOTEMPTY, EISDIR } = require("./errors.js"); 3 | 4 | const STAT = 0; 5 | 6 | module.exports = class CacheFS { 7 | constructor() { 8 | } 9 | _makeRoot(root = new Map()) { 10 | root.set(STAT, { mode: 0o777, type: "dir", size: 0, ino: 0, mtimeMs: Date.now() }); 11 | return root 12 | } 13 | activate(superblock = null) { 14 | if (superblock === null) { 15 | this._root = new Map([["/", this._makeRoot()]]); 16 | } else if (typeof superblock === 'string') { 17 | this._root = new Map([["/", this._makeRoot(this.parse(superblock))]]); 18 | } else { 19 | this._root = superblock 20 | } 21 | } 22 | get activated () { 23 | return !!this._root 24 | } 25 | deactivate () { 26 | this._root = void 0 27 | } 28 | size () { 29 | // subtract 1 to ignore the root directory itself from the count. 30 | return this._countInodes(this._root.get("/")) - 1; 31 | } 32 | _countInodes(map) { 33 | let count = 1; 34 | for (let [key, val] of map) { 35 | if (key === STAT) continue; 36 | count += this._countInodes(val); 37 | } 38 | return count; 39 | } 40 | autoinc () { 41 | let val = this._maxInode(this._root.get("/")) + 1; 42 | return val; 43 | } 44 | _maxInode(map) { 45 | let max = map.get(STAT).ino; 46 | for (let [key, val] of map) { 47 | if (key === STAT) continue; 48 | max = Math.max(max, this._maxInode(val)); 49 | } 50 | return max; 51 | } 52 | print(root = this._root.get("/")) { 53 | let str = ""; 54 | const printTree = (root, indent) => { 55 | for (let [file, node] of root) { 56 | if (file === 0) continue; 57 | let stat = node.get(STAT); 58 | let mode = stat.mode.toString(8); 59 | str += `${"\t".repeat(indent)}${file}\t${mode}` 60 | if (stat.type === "file") { 61 | str += `\t${stat.size}\t${stat.mtimeMs}\n`; 62 | } else { 63 | str += `\n` 64 | printTree(node, indent + 1); 65 | } 66 | } 67 | }; 68 | printTree(root, 0); 69 | return str; 70 | } 71 | parse(print) { 72 | let autoinc = 0; 73 | 74 | function mk(stat) { 75 | const ino = ++autoinc; 76 | // TODO: Use a better heuristic for determining whether file or dir 77 | const type = stat.length === 1 ? "dir" : "file" 78 | let [mode, size, mtimeMs] = stat; 79 | mode = parseInt(mode, 8); 80 | size = size ? parseInt(size) : 0; 81 | mtimeMs = mtimeMs ? parseInt(mtimeMs) : Date.now(); 82 | return new Map([[STAT, { mode, type, size, mtimeMs, ino }]]); 83 | } 84 | 85 | let lines = print.trim().split("\n"); 86 | let _root = this._makeRoot(); 87 | let stack = [ 88 | { indent: -1, node: _root }, 89 | { indent: 0, node: null } 90 | ]; 91 | for (let line of lines) { 92 | let prefix = line.match(/^\t*/)[0]; 93 | let indent = prefix.length; 94 | line = line.slice(indent); 95 | let [filename, ...stat] = line.split("\t"); 96 | let node = mk(stat); 97 | if (indent <= stack[stack.length - 1].indent) { 98 | while (indent <= stack[stack.length - 1].indent) { 99 | stack.pop(); 100 | } 101 | } 102 | stack.push({ indent, node }); 103 | let cd = stack[stack.length - 2].node; 104 | cd.set(filename, node); 105 | } 106 | return _root; 107 | } 108 | _lookup(filepath, follow = true) { 109 | let dir = this._root; 110 | let partialPath = '/' 111 | let parts = path.split(filepath) 112 | for (let i = 0; i < parts.length; ++ i) { 113 | let part = parts[i]; 114 | dir = dir.get(part); 115 | if (!dir) throw new ENOENT(filepath); 116 | // Follow symlinks 117 | if (follow || i < parts.length - 1) { 118 | const stat = dir.get(STAT) 119 | if (stat.type === 'symlink') { 120 | let target = path.resolve(partialPath, stat.target) 121 | dir = this._lookup(target) 122 | } 123 | if (!partialPath) { 124 | partialPath = part 125 | } else { 126 | partialPath = path.join(partialPath, part) 127 | } 128 | } 129 | } 130 | return dir; 131 | } 132 | mkdir(filepath, { mode }) { 133 | if (filepath === "/") throw new EEXIST(); 134 | let dir = this._lookup(path.dirname(filepath)); 135 | let basename = path.basename(filepath); 136 | if (dir.has(basename)) { 137 | throw new EEXIST(); 138 | } 139 | let entry = new Map(); 140 | let stat = { 141 | mode, 142 | type: "dir", 143 | size: 0, 144 | mtimeMs: Date.now(), 145 | ino: this.autoinc(), 146 | }; 147 | entry.set(STAT, stat); 148 | dir.set(basename, entry); 149 | } 150 | rmdir(filepath) { 151 | let dir = this._lookup(filepath); 152 | if (dir.get(STAT).type !== 'dir') throw new ENOTDIR(); 153 | // check it's empty (size should be 1 for just StatSym) 154 | if (dir.size > 1) throw new ENOTEMPTY(); 155 | // remove from parent 156 | let parent = this._lookup(path.dirname(filepath)); 157 | let basename = path.basename(filepath); 158 | parent.delete(basename); 159 | } 160 | readdir(filepath) { 161 | let dir = this._lookup(filepath); 162 | if (dir.get(STAT).type !== 'dir') throw new ENOTDIR(); 163 | return [...dir.keys()].filter(key => typeof key === "string"); 164 | } 165 | writeStat(filepath, size, { mode }) { 166 | let ino; 167 | let oldStat; 168 | try { 169 | oldStat = this.stat(filepath); 170 | } catch (err) {} 171 | 172 | if (oldStat !== undefined) { 173 | if (oldStat.type === 'dir') { 174 | throw new EISDIR(); 175 | } 176 | 177 | if (mode == null) { 178 | mode = oldStat.mode; 179 | } 180 | ino = oldStat.ino; 181 | } 182 | 183 | if (mode == null) { 184 | mode = 0o666; 185 | } 186 | if (ino == null) { 187 | ino = this.autoinc(); 188 | } 189 | let dir = this._lookup(path.dirname(filepath)); 190 | let basename = path.basename(filepath); 191 | let stat = { 192 | mode, 193 | type: "file", 194 | size, 195 | mtimeMs: Date.now(), 196 | ino, 197 | }; 198 | let entry = new Map(); 199 | entry.set(STAT, stat); 200 | dir.set(basename, entry); 201 | return stat; 202 | } 203 | unlink(filepath) { 204 | // remove from parent 205 | let parent = this._lookup(path.dirname(filepath)); 206 | let basename = path.basename(filepath); 207 | parent.delete(basename); 208 | } 209 | rename(oldFilepath, newFilepath) { 210 | let basename = path.basename(newFilepath); 211 | // Note: do both lookups before making any changes 212 | // so if lookup throws, we don't lose data (issue #23) 213 | // grab references 214 | let entry = this._lookup(oldFilepath); 215 | let destDir = this._lookup(path.dirname(newFilepath)); 216 | // insert into new parent directory 217 | destDir.set(basename, entry); 218 | // remove from old parent directory 219 | this.unlink(oldFilepath) 220 | } 221 | stat(filepath) { 222 | return this._lookup(filepath).get(STAT); 223 | } 224 | lstat(filepath) { 225 | return this._lookup(filepath, false).get(STAT); 226 | } 227 | readlink(filepath) { 228 | return this._lookup(filepath, false).get(STAT).target; 229 | } 230 | symlink(target, filepath) { 231 | let ino, mode; 232 | try { 233 | let oldStat = this.stat(filepath); 234 | if (mode === null) { 235 | mode = oldStat.mode; 236 | } 237 | ino = oldStat.ino; 238 | } catch (err) {} 239 | if (mode == null) { 240 | mode = 0o120000; 241 | } 242 | if (ino == null) { 243 | ino = this.autoinc(); 244 | } 245 | let dir = this._lookup(path.dirname(filepath)); 246 | let basename = path.basename(filepath); 247 | let stat = { 248 | mode, 249 | type: "symlink", 250 | target, 251 | size: 0, 252 | mtimeMs: Date.now(), 253 | ino, 254 | }; 255 | let entry = new Map(); 256 | entry.set(STAT, stat); 257 | dir.set(basename, entry); 258 | return stat; 259 | } 260 | _du (dir) { 261 | let size = 0; 262 | for (const [name, entry] of dir.entries()) { 263 | if (name === STAT) { 264 | size += entry.size; 265 | } else { 266 | size += this._du(entry); 267 | } 268 | } 269 | return size; 270 | } 271 | du (filepath) { 272 | let dir = this._lookup(filepath); 273 | return this._du(dir); 274 | } 275 | }; 276 | -------------------------------------------------------------------------------- /src/DefaultBackend.js: -------------------------------------------------------------------------------- 1 | const { encode, decode } = require("isomorphic-textencoder"); 2 | const debounce = require("just-debounce-it"); 3 | 4 | const CacheFS = require("./CacheFS.js"); 5 | const { ENOENT, ENOTEMPTY, ETIMEDOUT } = require("./errors.js"); 6 | const IdbBackend = require("./IdbBackend.js"); 7 | const HttpBackend = require("./HttpBackend.js") 8 | const Mutex = require("./Mutex.js"); 9 | const Mutex2 = require("./Mutex2.js"); 10 | 11 | const path = require("./path.js"); 12 | 13 | module.exports = class DefaultBackend { 14 | constructor() { 15 | this.saveSuperblock = debounce(() => { 16 | this.flush(); 17 | }, 500); 18 | } 19 | async init (name, { 20 | wipe, 21 | url, 22 | urlauto, 23 | fileDbName = name, 24 | db = null, 25 | fileStoreName = name + "_files", 26 | lockDbName = name + "_lock", 27 | lockStoreName = name + "_lock", 28 | } = {}) { 29 | this._name = name 30 | this._idb = db || new IdbBackend(fileDbName, fileStoreName); 31 | this._mutex = navigator.locks ? new Mutex2(name) : new Mutex(lockDbName, lockStoreName); 32 | this._cache = new CacheFS(name); 33 | this._opts = { wipe, url }; 34 | this._needsWipe = !!wipe; 35 | if (url) { 36 | this._http = new HttpBackend(url) 37 | this._urlauto = !!urlauto 38 | } 39 | } 40 | async activate() { 41 | if (this._cache.activated) return 42 | // Wipe IDB if requested 43 | if (this._needsWipe) { 44 | this._needsWipe = false; 45 | await this._idb.wipe() 46 | await this._mutex.release({ force: true }) 47 | } 48 | if (!(await this._mutex.has())) await this._mutex.wait() 49 | // Attempt to load FS from IDB backend 50 | const root = await this._idb.loadSuperblock() 51 | if (root) { 52 | this._cache.activate(root); 53 | } else if (this._http) { 54 | // If that failed, attempt to load FS from HTTP backend 55 | const text = await this._http.loadSuperblock() 56 | this._cache.activate(text) 57 | await this._saveSuperblock(); 58 | } else { 59 | // If there is no HTTP backend, start with an empty filesystem 60 | this._cache.activate() 61 | } 62 | if (await this._mutex.has()) { 63 | return 64 | } else { 65 | throw new ETIMEDOUT() 66 | } 67 | } 68 | async deactivate() { 69 | if (await this._mutex.has()) { 70 | await this._saveSuperblock() 71 | } 72 | this._cache.deactivate() 73 | try { 74 | await this._mutex.release() 75 | } catch (e) { 76 | console.log(e) 77 | } 78 | await this._idb.close() 79 | } 80 | async _saveSuperblock() { 81 | if (this._cache.activated) { 82 | this._lastSavedAt = Date.now() 83 | await this._idb.saveSuperblock(this._cache._root); 84 | } 85 | } 86 | _writeStat(filepath, size, opts) { 87 | let dirparts = path.split(path.dirname(filepath)) 88 | let dir = dirparts.shift() 89 | for (let dirpart of dirparts) { 90 | dir = path.join(dir, dirpart) 91 | try { 92 | this._cache.mkdir(dir, { mode: 0o777 }) 93 | } catch (e) {} 94 | } 95 | return this._cache.writeStat(filepath, size, opts) 96 | } 97 | async readFile(filepath, opts) { 98 | const encoding = typeof opts === "string" ? opts : opts && opts.encoding; 99 | if (encoding && encoding !== 'utf8') throw new Error('Only "utf8" encoding is supported in readFile'); 100 | let data = null, stat = null 101 | try { 102 | stat = this._cache.stat(filepath); 103 | data = await this._idb.readFile(stat.ino) 104 | } catch (e) { 105 | if (!this._urlauto) throw e 106 | } 107 | if (!data && this._http) { 108 | let lstat = this._cache.lstat(filepath) 109 | while (lstat.type === 'symlink') { 110 | filepath = path.resolve(path.dirname(filepath), lstat.target) 111 | lstat = this._cache.lstat(filepath) 112 | } 113 | data = await this._http.readFile(filepath) 114 | } 115 | if (data) { 116 | if (!stat || stat.size != data.byteLength) { 117 | stat = await this._writeStat(filepath, data.byteLength, { mode: stat ? stat.mode : 0o666 }) 118 | this.saveSuperblock() // debounced 119 | } 120 | if (encoding === "utf8") { 121 | data = decode(data); 122 | } else { 123 | data.toString = () => decode(data); 124 | } 125 | } 126 | if (!stat) throw new ENOENT(filepath) 127 | return data; 128 | } 129 | async writeFile(filepath, data, opts) { 130 | const { mode, encoding = "utf8" } = opts; 131 | if (typeof data === "string") { 132 | if (encoding !== "utf8") { 133 | throw new Error('Only "utf8" encoding is supported in writeFile'); 134 | } 135 | data = encode(data); 136 | } 137 | const stat = await this._cache.writeStat(filepath, data.byteLength, { mode }); 138 | await this._idb.writeFile(stat.ino, data) 139 | } 140 | async unlink(filepath, opts) { 141 | const stat = this._cache.lstat(filepath); 142 | this._cache.unlink(filepath); 143 | if (stat.type !== 'symlink') { 144 | await this._idb.unlink(stat.ino) 145 | } 146 | } 147 | readdir(filepath, opts) { 148 | return this._cache.readdir(filepath); 149 | } 150 | mkdir(filepath, opts) { 151 | const { mode = 0o777 } = opts; 152 | this._cache.mkdir(filepath, { mode }); 153 | } 154 | rmdir(filepath, opts) { 155 | // Never allow deleting the root directory. 156 | if (filepath === "/") { 157 | throw new ENOTEMPTY(); 158 | } 159 | this._cache.rmdir(filepath); 160 | } 161 | rename(oldFilepath, newFilepath) { 162 | this._cache.rename(oldFilepath, newFilepath); 163 | } 164 | stat(filepath, opts) { 165 | return this._cache.stat(filepath); 166 | } 167 | lstat(filepath, opts) { 168 | return this._cache.lstat(filepath); 169 | } 170 | readlink(filepath, opts) { 171 | return this._cache.readlink(filepath); 172 | } 173 | symlink(target, filepath) { 174 | this._cache.symlink(target, filepath); 175 | } 176 | async backFile(filepath, opts) { 177 | let size = await this._http.sizeFile(filepath) 178 | await this._writeStat(filepath, size, opts) 179 | } 180 | du(filepath) { 181 | return this._cache.du(filepath); 182 | } 183 | flush() { 184 | return this._saveSuperblock(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/HttpBackend.js: -------------------------------------------------------------------------------- 1 | module.exports = class HttpBackend { 2 | constructor(url) { 3 | this._url = url; 4 | } 5 | loadSuperblock() { 6 | return fetch(this._url + '/.superblock.txt').then(res => res.ok ? res.text() : null) 7 | } 8 | async readFile(filepath) { 9 | const res = await fetch(this._url + filepath) 10 | if (res.status === 200) { 11 | return res.arrayBuffer() 12 | } else { 13 | throw new Error('ENOENT') 14 | } 15 | } 16 | async sizeFile(filepath) { 17 | const res = await fetch(this._url + filepath, { method: 'HEAD' }) 18 | if (res.status === 200) { 19 | return res.headers.get('content-length') 20 | } else { 21 | throw new Error('ENOENT') 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/IdbBackend.js: -------------------------------------------------------------------------------- 1 | const idb = require("@isomorphic-git/idb-keyval"); 2 | 3 | module.exports = class IdbBackend { 4 | constructor(dbname, storename) { 5 | this._database = dbname; 6 | this._storename = storename; 7 | this._store = new idb.Store(this._database, this._storename); 8 | } 9 | saveSuperblock(superblock) { 10 | return idb.set("!root", superblock, this._store); 11 | } 12 | loadSuperblock() { 13 | return idb.get("!root", this._store); 14 | } 15 | readFile(inode) { 16 | return idb.get(inode, this._store) 17 | } 18 | writeFile(inode, data) { 19 | return idb.set(inode, data, this._store) 20 | } 21 | unlink(inode) { 22 | return idb.del(inode, this._store) 23 | } 24 | wipe() { 25 | return idb.clear(this._store) 26 | } 27 | close() { 28 | return idb.close(this._store) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Mutex.js: -------------------------------------------------------------------------------- 1 | const idb = require("@isomorphic-git/idb-keyval"); 2 | 3 | const sleep = ms => new Promise(r => setTimeout(r, ms)) 4 | 5 | module.exports = class Mutex { 6 | constructor(dbname, storename) { 7 | this._id = Math.random() 8 | this._database = dbname 9 | this._storename = storename 10 | this._store = new idb.Store(this._database, this._storename) 11 | this._lock = null 12 | } 13 | async has ({ margin = 2000 } = {}) { 14 | if (this._lock && this._lock.holder === this._id) { 15 | const now = Date.now() 16 | if (this._lock.expires > now + margin) { 17 | return true 18 | } else { 19 | return await this.renew() 20 | } 21 | } else { 22 | return false 23 | } 24 | } 25 | // Returns true if successful 26 | async renew ({ ttl = 5000 } = {}) { 27 | let success 28 | await idb.update("lock", (current) => { 29 | const now = Date.now() 30 | const expires = now + ttl 31 | success = current && current.holder === this._id 32 | this._lock = success ? { holder: this._id, expires } : current 33 | return this._lock 34 | }, this._store) 35 | return success 36 | } 37 | // Returns true if successful 38 | async acquire ({ ttl = 5000 } = {}) { 39 | let success 40 | let expired 41 | let doubleLock 42 | await idb.update("lock", (current) => { 43 | const now = Date.now() 44 | const expires = now + ttl 45 | expired = current && current.expires < now 46 | success = current === undefined || expired 47 | doubleLock = current && current.holder === this._id 48 | this._lock = success ? { holder: this._id, expires } : current 49 | return this._lock 50 | }, this._store) 51 | if (doubleLock) { 52 | throw new Error('Mutex double-locked') 53 | } 54 | return success 55 | } 56 | // check at 10Hz, give up after 10 minutes 57 | async wait ({ interval = 100, limit = 6000, ttl } = {}) { 58 | while (limit--) { 59 | if (await this.acquire({ ttl })) return true 60 | await sleep(interval) 61 | } 62 | throw new Error('Mutex timeout') 63 | } 64 | // Returns true if successful 65 | async release ({ force = false } = {}) { 66 | let success 67 | let doubleFree 68 | let someoneElseHasIt 69 | await idb.update("lock", (current) => { 70 | success = force || (current && current.holder === this._id) 71 | doubleFree = current === void 0 72 | someoneElseHasIt = current && current.holder !== this._id 73 | this._lock = success ? void 0 : current 74 | return this._lock 75 | }, this._store) 76 | await idb.close(this._store) 77 | if (!success && !force) { 78 | if (doubleFree) throw new Error('Mutex double-freed') 79 | if (someoneElseHasIt) throw new Error('Mutex lost ownership') 80 | } 81 | return success 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Mutex2.js: -------------------------------------------------------------------------------- 1 | module.exports = class Mutex { 2 | constructor(name) { 3 | this._id = Math.random() 4 | this._database = name 5 | this._has = false 6 | this._release = null 7 | } 8 | async has () { 9 | return this._has 10 | } 11 | // Returns true if successful 12 | async acquire () { 13 | return new Promise(resolve => { 14 | navigator.locks.request(this._database + "_lock", {ifAvailable: true}, lock => { 15 | this._has = !!lock 16 | resolve(!!lock) 17 | return new Promise(resolve => { 18 | this._release = resolve 19 | }) 20 | }); 21 | }) 22 | } 23 | // Returns true if successful, gives up after 10 minutes 24 | async wait ({ timeout = 600000 } = {}) { 25 | return new Promise((resolve, reject) => { 26 | const controller = new AbortController(); 27 | setTimeout(() => { 28 | controller.abort(); 29 | reject(new Error('Mutex timeout')) 30 | }, timeout); 31 | navigator.locks.request(this._database + "_lock", {signal: controller.signal}, lock => { 32 | this._has = !!lock 33 | resolve(!!lock) 34 | return new Promise(resolve => { 35 | this._release = resolve 36 | }) 37 | }); 38 | }) 39 | } 40 | // Returns true if successful 41 | async release ({ force = false } = {}) { 42 | this._has = false 43 | if (this._release) { 44 | this._release() 45 | } else if (force) { 46 | navigator.locks.request(this._database + "_lock", {steal: true}, lock => true) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/PromisifiedFS.js: -------------------------------------------------------------------------------- 1 | const DefaultBackend = require("./DefaultBackend.js"); 2 | const Stat = require("./Stat.js"); 3 | 4 | const path = require("./path.js"); 5 | 6 | function cleanParamsFilepathOpts(filepath, opts, ...rest) { 7 | // normalize paths 8 | filepath = path.normalize(filepath); 9 | // strip out callbacks 10 | if (typeof opts === "undefined" || typeof opts === "function") { 11 | opts = {}; 12 | } 13 | // expand string options to encoding options 14 | if (typeof opts === "string") { 15 | opts = { 16 | encoding: opts, 17 | }; 18 | } 19 | return [filepath, opts, ...rest]; 20 | } 21 | 22 | function cleanParamsFilepathDataOpts(filepath, data, opts, ...rest) { 23 | // normalize paths 24 | filepath = path.normalize(filepath); 25 | // strip out callbacks 26 | if (typeof opts === "undefined" || typeof opts === "function") { 27 | opts = {}; 28 | } 29 | // expand string options to encoding options 30 | if (typeof opts === "string") { 31 | opts = { 32 | encoding: opts, 33 | }; 34 | } 35 | return [filepath, data, opts, ...rest]; 36 | } 37 | 38 | function cleanParamsFilepathFilepath(oldFilepath, newFilepath, ...rest) { 39 | // normalize paths 40 | return [path.normalize(oldFilepath), path.normalize(newFilepath), ...rest]; 41 | } 42 | 43 | module.exports = class PromisifiedFS { 44 | constructor(name, options = {}) { 45 | this.init = this.init.bind(this) 46 | this.readFile = this._wrap(this.readFile, cleanParamsFilepathOpts, false) 47 | this.writeFile = this._wrap(this.writeFile, cleanParamsFilepathDataOpts, true) 48 | this.unlink = this._wrap(this.unlink, cleanParamsFilepathOpts, true) 49 | this.readdir = this._wrap(this.readdir, cleanParamsFilepathOpts, false) 50 | this.mkdir = this._wrap(this.mkdir, cleanParamsFilepathOpts, true) 51 | this.rmdir = this._wrap(this.rmdir, cleanParamsFilepathOpts, true) 52 | this.rename = this._wrap(this.rename, cleanParamsFilepathFilepath, true) 53 | this.stat = this._wrap(this.stat, cleanParamsFilepathOpts, false) 54 | this.lstat = this._wrap(this.lstat, cleanParamsFilepathOpts, false) 55 | this.readlink = this._wrap(this.readlink, cleanParamsFilepathOpts, false) 56 | this.symlink = this._wrap(this.symlink, cleanParamsFilepathFilepath, true) 57 | this.backFile = this._wrap(this.backFile, cleanParamsFilepathOpts, true) 58 | this.du = this._wrap(this.du, cleanParamsFilepathOpts, false); 59 | 60 | this._deactivationPromise = null 61 | this._deactivationTimeout = null 62 | this._activationPromise = null 63 | 64 | this._operations = new Set() 65 | 66 | if (name) { 67 | this.init(name, options) 68 | } 69 | } 70 | async init (...args) { 71 | if (this._initPromiseResolve) await this._initPromise; 72 | this._initPromise = this._init(...args) 73 | return this._initPromise 74 | } 75 | async _init (name, options = {}) { 76 | await this._gracefulShutdown(); 77 | if (this._activationPromise) await this._deactivate() 78 | 79 | if (this._backend && this._backend.destroy) { 80 | await this._backend.destroy(); 81 | } 82 | this._backend = options.backend || new DefaultBackend(); 83 | if (this._backend.init) { 84 | await this._backend.init(name, options); 85 | } 86 | 87 | if (this._initPromiseResolve) { 88 | this._initPromiseResolve(); 89 | this._initPromiseResolve = null; 90 | } 91 | // The next comment starting with the "fs is initially activated when constructed"? 92 | // That can create contention for the mutex if two threads try to init at the same time 93 | // so I've added an option to disable that behavior. 94 | if (!options.defer) { 95 | // The fs is initially activated when constructed (in order to wipe/save the superblock) 96 | // This is not awaited, because that would create a cycle. 97 | this.stat('/') 98 | } 99 | } 100 | async _gracefulShutdown () { 101 | if (this._operations.size > 0) { 102 | this._isShuttingDown = true 103 | await new Promise(resolve => this._gracefulShutdownResolve = resolve); 104 | this._isShuttingDown = false 105 | this._gracefulShutdownResolve = null 106 | } 107 | } 108 | _wrap (fn, paramCleaner, mutating) { 109 | return async (...args) => { 110 | args = paramCleaner(...args) 111 | let op = { 112 | name: fn.name, 113 | args, 114 | } 115 | this._operations.add(op) 116 | try { 117 | await this._activate() 118 | return await fn.apply(this, args) 119 | } finally { 120 | this._operations.delete(op) 121 | if (mutating) this._backend.saveSuperblock() // this is debounced 122 | if (this._operations.size === 0) { 123 | if (!this._deactivationTimeout) clearTimeout(this._deactivationTimeout) 124 | this._deactivationTimeout = setTimeout(this._deactivate.bind(this), 500) 125 | } 126 | } 127 | } 128 | } 129 | async _activate() { 130 | if (!this._initPromise) console.warn(new Error(`Attempted to use LightningFS ${this._name} before it was initialized.`)) 131 | await this._initPromise 132 | if (this._deactivationTimeout) { 133 | clearTimeout(this._deactivationTimeout) 134 | this._deactivationTimeout = null 135 | } 136 | if (this._deactivationPromise) await this._deactivationPromise 137 | this._deactivationPromise = null 138 | if (!this._activationPromise) { 139 | this._activationPromise = this._backend.activate ? this._backend.activate() : Promise.resolve(); 140 | } 141 | await this._activationPromise 142 | } 143 | async _deactivate() { 144 | if (this._activationPromise) await this._activationPromise 145 | 146 | if (!this._deactivationPromise) { 147 | this._deactivationPromise = this._backend.deactivate ? this._backend.deactivate() : Promise.resolve(); 148 | } 149 | this._activationPromise = null 150 | if (this._gracefulShutdownResolve) this._gracefulShutdownResolve() 151 | return this._deactivationPromise 152 | } 153 | async readFile(filepath, opts) { 154 | return this._backend.readFile(filepath, opts); 155 | } 156 | async writeFile(filepath, data, opts) { 157 | await this._backend.writeFile(filepath, data, opts); 158 | return null 159 | } 160 | async unlink(filepath, opts) { 161 | await this._backend.unlink(filepath, opts); 162 | return null 163 | } 164 | async readdir(filepath, opts) { 165 | return this._backend.readdir(filepath, opts); 166 | } 167 | async mkdir(filepath, opts) { 168 | await this._backend.mkdir(filepath, opts); 169 | return null 170 | } 171 | async rmdir(filepath, opts) { 172 | await this._backend.rmdir(filepath, opts); 173 | return null; 174 | } 175 | async rename(oldFilepath, newFilepath) { 176 | await this._backend.rename(oldFilepath, newFilepath); 177 | return null; 178 | } 179 | async stat(filepath, opts) { 180 | const data = await this._backend.stat(filepath, opts); 181 | return new Stat(data); 182 | } 183 | async lstat(filepath, opts) { 184 | const data = await this._backend.lstat(filepath, opts); 185 | return new Stat(data); 186 | } 187 | async readlink(filepath, opts) { 188 | return this._backend.readlink(filepath, opts); 189 | } 190 | async symlink(target, filepath) { 191 | await this._backend.symlink(target, filepath); 192 | return null; 193 | } 194 | async backFile(filepath, opts) { 195 | await this._backend.backFile(filepath, opts); 196 | return null 197 | } 198 | async du(filepath) { 199 | return this._backend.du(filepath); 200 | } 201 | async flush() { 202 | return this._backend.flush(); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Stat.js: -------------------------------------------------------------------------------- 1 | module.exports = class Stat { 2 | constructor(stats) { 3 | this.type = stats.type; 4 | this.mode = stats.mode; 5 | this.size = stats.size; 6 | this.ino = stats.ino; 7 | this.mtimeMs = stats.mtimeMs; 8 | this.ctimeMs = stats.ctimeMs || stats.mtimeMs; 9 | this.uid = 1; 10 | this.gid = 1; 11 | this.dev = 1; 12 | } 13 | isFile() { 14 | return this.type === "file"; 15 | } 16 | isDirectory() { 17 | return this.type === "dir"; 18 | } 19 | isSymbolicLink() { 20 | return this.type === "symlink"; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/__tests__/CacheFS.spec.js: -------------------------------------------------------------------------------- 1 | import CacheFS from "../CacheFS"; 2 | 3 | const treeText = require('./__fixtures__/tree.txt.js'); 4 | 5 | describe("CacheFS module", () => { 6 | it("print ∘ parse == id", () => { 7 | const fs = new CacheFS(); 8 | let parsed = fs.parse(treeText) 9 | let text = fs.print(parsed) 10 | expect(text).toEqual(treeText) 11 | }); 12 | it("size()", () => { 13 | const fs = new CacheFS(); 14 | fs.activate() 15 | expect(fs.size()).toEqual(0) 16 | fs.activate(treeText) 17 | let inodeCount = treeText.trim().split('\n').length 18 | expect(fs.size()).toEqual(inodeCount) 19 | }); 20 | it("autoinc()", () => { 21 | const fs = new CacheFS(); 22 | fs.activate() 23 | expect(fs.autoinc()).toEqual(1) 24 | fs.writeStat('/foo', 3, {}) 25 | expect(fs.autoinc()).toEqual(2) 26 | fs.mkdir('/bar', {}) 27 | expect(fs.autoinc()).toEqual(3) 28 | fs.unlink('/foo') 29 | expect(fs.autoinc()).toEqual(3) 30 | fs.mkdir('/bar/baz', {}) 31 | expect(fs.autoinc()).toEqual(4) 32 | fs.rmdir('/bar/baz') 33 | expect(fs.autoinc()).toEqual(3) 34 | fs.mkdir('/bar/bar', {}) 35 | expect(fs.autoinc()).toEqual(4) 36 | fs.writeStat('/bar/bar/boo', 3, {}) 37 | expect(fs.autoinc()).toEqual(5) 38 | fs.unlink('/bar/bar/boo') 39 | expect(fs.autoinc()).toEqual(4) 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/.superblock.txt: -------------------------------------------------------------------------------- 1 | 0 40755 2 | a.txt 100644 14 1545621303427.1333 3 | b.txt 100644 14 1545621303427.2192 4 | c.txt 100644 14 1545621303427.2607 5 | 1 40755 6 | d.txt 100644 14 1545621340683.4724 7 | e.txt 100644 14 1545621349775.4539 8 | f.txt 100644 14 1545621356008.1582 9 | 2 40775 10 | a.txt 100664 14 1572953161955.4033 11 | a.txt 100644 14 1545621375109 12 | b.txt 100644 14 1545621255760.9512 13 | c.txt 100644 14 1545621290070.7742 14 | -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/0/a.txt: -------------------------------------------------------------------------------- 1 | Hello from "a" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/0/b.txt: -------------------------------------------------------------------------------- 1 | Hello from "b" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/0/c.txt: -------------------------------------------------------------------------------- 1 | Hello from "c" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/1/d.txt: -------------------------------------------------------------------------------- 1 | Hello from "d" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/1/e.txt: -------------------------------------------------------------------------------- 1 | Hello from "e" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/1/f.txt: -------------------------------------------------------------------------------- 1 | Hello from "f" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/2/a.txt: -------------------------------------------------------------------------------- 1 | Hello from "a" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/a.txt: -------------------------------------------------------------------------------- 1 | Hello from "a" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/b.txt: -------------------------------------------------------------------------------- 1 | Hello from "b" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/c.txt: -------------------------------------------------------------------------------- 1 | Hello from "c" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/test-folder/not-in-superblock.txt: -------------------------------------------------------------------------------- 1 | Hello from "not-in-superblock" -------------------------------------------------------------------------------- /src/__tests__/__fixtures__/tree.txt.js: -------------------------------------------------------------------------------- 1 | module.exports = `.git 777 2 | hooks 777 3 | info 777 4 | objects 777 5 | info 777 6 | pack 777 7 | pack-afb2f8b14c85b0fd93a1a77ba1518de1fd4e5b8c.pack 666 4975988 1545535030142 8 | pack-afb2f8b14c85b0fd93a1a77ba1518de1fd4e5b8c.idx 666 33804 1545535032217 9 | refs 777 10 | heads 777 11 | master 666 41 1545535032282 12 | tags 777 13 | remotes 777 14 | origin 777 15 | HEAD 666 32 1545535029386 16 | master 666 41 1545535029425 17 | config 666 363 1545535032279 18 | HEAD 666 23 1545535038216 19 | shallow 666 41 1545535029345 20 | index 666 148368 1545535038212 21 | .extra 777 22 | git index.grammar 666 3989 1545535032919 23 | .github 777 24 | ISSUE_TEMPLATE 777 25 | encounter-an-error-.md 666 254 1545535033390 26 | suggest-a-feature-.md 666 179 1545535033390 27 | PULL_REQUEST_TEMPLATE.md 666 1925 1545535032980 28 | __tests__ 777 29 | __fixtures__ 777 30 | test-GitIndex 777 31 | index 666 2972 1545535034089 32 | simple-index 666 118 1545535034089 33 | test-GitObjectManager.git 777 34 | objects 777 35 | 03 777 36 | 3417a0c0d27e31ee2402838910c3de49b10d8c 666 886 1545535035732 37 | 3417ae18b174f078f2f44232cb7a374f4c60ce 666 886 1545535035733 38 | 1e 777 39 | 40fdfba1cf17f3c9f9f3d6b392b1865e5147b9 666 780 1545535035733 40 | e1 777 41 | 0ebb90d03eaacca84de1af0a59b444232da99e 666 850 1545535035733 42 | refs 777 43 | heads 777 44 | master 666 41 1545535035734 45 | test-branch 666 41 1545535035735 46 | remotes 777 47 | origin 777 48 | master 666 41 1545535036142 49 | test-branch 666 41 1545535036143 50 | tags 777 51 | test-tag 666 41 1545535035735 52 | HEAD 666 23 1545535034090 53 | packed-refs 666 97 1545535034090 54 | shallow 666 41 1545535034091 55 | test-GitPackIndex.git 777 56 | objects 777 57 | pack 777 58 | pack-1a1e70d2f116e8cb0cb42d26019e5c7d0eb01888.idx 666 22604 1545535035735 59 | pack-1a1e70d2f116e8cb0cb42d26019e5c7d0eb01888.pack 666 338009 1545535035736 60 | test-GitRefManager.git 777 61 | refs 777 62 | tags 777 63 | local-tag 666 41 1545535035736 64 | packed-refs 666 3915 1545535034091 65 | test-add 777 66 | a-copy.txt 666 6 1545535034091 67 | a.txt 666 6 1545535034091 68 | b.txt 666 6 1545535034092 69 | test-addRemote.git 777 70 | config 666 286 1545535034092 71 | test-annotatedTag.git 777 72 | objects 777 73 | 98 777 74 | 4082c29b02a7ab31c34c7a8a52df9a706d7987 666 57 1545535035736 75 | cf 777 76 | c039a0acb68bee8bb4f3b13b6b211dbb8c1a69 666 130 1545535035736 77 | d6 777 78 | 70460b4b4aece5915caf5c68d12f560a9fe3e4 666 29 1545535035736 79 | refs 777 80 | heads 777 81 | master 666 41 1545535035737 82 | HEAD 666 23 1545535034092 83 | config 666 111 1545535034092 84 | description 666 73 1545535034092 85 | index 666 145 1545535034093 86 | test-branch.git 777 87 | objects 777 88 | 06 777 89 | 74da21ba278014a243d1e5ae58df828c819181 666 122 1545535035737 90 | 4b 777 91 | 825dc642cb6eb9a060e54bf8d69288fbee4904 666 15 1545535035737 92 | refs 777 93 | heads 777 94 | master 666 41 1545535035738 95 | COMMIT_EDITMSG 666 13 1545535034094 96 | HEAD 666 23 1545535034094 97 | config 666 137 1545535034094 98 | index 666 65 1545535034094 99 | test-checkout.git 777 100 | objects 777 101 | 04 777 102 | 0826471df5c588322c6f423f856c18ec91138c 666 405 1545535035738 103 | 10 777 104 | 43d2cea9a100886c53e78d534b3fa2b6d95874 666 249 1545535035738 105 | 12 777 106 | 915911dfafda54f7f1886ec9992a2c6bd82f86 666 327 1545535035738 107 | 1d 777 108 | b939d41956405f755e69ab570296c7ed3cec99 666 154 1545535035739 109 | bbf594d09fbc0fe257a0ed2fecca95b65bebe9 666 460 1545535035739 110 | 1e 777 111 | d33a9505ae4fe1389966e57873c8fbad681cb4 666 279 1545535035740 112 | 26 777 113 | 9a24ee09863165bcd7361a0a198fb8b8acf868 666 114 1545535035740 114 | 33 777 115 | 3191f116d625ba492527ce790fcabe12af33e5 666 450 1545535035740 116 | 3f 777 117 | 5bda22052d1c75d0242cba24989e8dc3b1acf0 666 646 1545535035740 118 | 40 777 119 | 6eba01347a2d646b3b9cdf3521791a9b0cb9b4 666 118 1545535035741 120 | a464b28dac22220694f42d99538f494fc9df40 666 237 1545535035741 121 | 41 777 122 | 87b0591eb99fe023a5ead08af217901b938d72 666 146 1545535035742 123 | 48 777 124 | 447164fc125691a3e77899190fcb8ab296b20a 666 514 1545535035742 125 | 4a 777 126 | 58bdcdef3eb91264dfca0279959d98c16568d5 666 48 1545535035742 127 | 59 777 128 | 7a460f8dd83f7dfefd3b4f6773e86cee88b996 666 124 1545535035743 129 | 63 777 130 | f0cdb57239207b88b5660cc9545d84399373b4 666 267 1545535035743 131 | 69 777 132 | 6dcb8b85a4c54c72b20e1ff451d0226f40a711 666 1970 1545535035743 133 | 6e 777 134 | 9d67f2a308ca3d580b78c9f5894dde12fe981d 666 21850 1545535035744 135 | 74 777 136 | 98c4a43de5dab36c4d706a457c6e6412d3da9d 666 453 1545535035744 137 | 75 777 138 | e2e020d9ae60a0251e3d143bdca333a770b658 666 148 1545535035744 139 | 7c 777 140 | b32b9028480456f01f612098124ac60dfebc54 666 89 1545535035745 141 | 86 777 142 | 57d296d07b4d76302519ac608752397c38cd39 666 264 1545535035745 143 | 96 777 144 | 9f51d6c43a9fb840e6f4edacae13e43d29213c 666 143 1545535035745 145 | 97 777 146 | 492c52a3b93b89497cf17aeeb78b6d7329eae8 666 395 1545535035745 147 | 98 777 148 | 4d4a53158896387a7f7e58810f134ec8070d28 666 282 1545535035746 149 | 9d 777 150 | a7c2fb380f39d8e46796e341aa9aff253b1819 666 233 1545535035746 151 | ab 777 152 | 3403ab7d9ea3ff3263600c368f7a685b0853c0 666 1956 1545535035746 153 | af 777 154 | bfb23abbecae4529f636f467f4ab928dbd68ea 666 145 1545535035747 155 | b3 777 156 | f16d527a5d80b8893da03e1bf397689ebc06c3 666 202 1545535035747 157 | b4 777 158 | 39f8a7e95cd1c180238b1f2d1593ee329448ee 666 341 1545535035747 159 | bb 777 160 | f3e21f43fa4fe25eb925bfcb7c0434f7c2dc7d 666 46 1545535035747 161 | c4 777 162 | 871ff92daa357edf895d2a6485ae7f5a5dde48 666 305 1545535035747 163 | c6 777 164 | 75a17ccb1578bca836decf90205fdad743827d 666 712 1545535035748 165 | c8 777 166 | 86b0680e2f6015fddb73b76cbca3ccc162ce01 666 95 1545535035749 167 | d4 777 168 | fb3e2dccabdd793a7cd17b9b85bbe1e617214a 666 774 1545535035749 169 | e0 777 170 | b8f3574060ee24e03e4af3896f65dd208a60cc 666 348 1545535035749 171 | e1 777 172 | 0ebb90d03eaacca84de1af0a59b444232da99e 666 850 1545535035749 173 | e6 777 174 | 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 666 15 1545535035749 175 | refs 777 176 | heads 777 177 | missing-branch 666 41 1545535035750 178 | test-branch 666 40 1545535035750 179 | HEAD 666 28 1545535034095 180 | config 666 179 1545535034095 181 | shallow 666 41 1545535034096 182 | test-commit.git 777 183 | objects 777 184 | 13 777 185 | 86e77b0a7afa8333663a9e4cbf8e6158e625c1 666 780 1545535035750 186 | refs 777 187 | heads 777 188 | master 666 41 1545535035751 189 | HEAD 666 23 1545535034096 190 | config 666 129 1545535034096 191 | index 666 104 1545535034096 192 | test-config.git 777 193 | config 666 600 1545535034097 194 | test-currentBranch.git 777 195 | refs 777 196 | heads 777 197 | master 666 41 1545535035751 198 | HEAD 666 23 1545535034097 199 | test-deleteBranch.git 777 200 | objects 777 201 | 06 777 202 | 74da21ba278014a243d1e5ae58df828c819181 666 122 1545535035751 203 | 46 777 204 | 938a722feab415225be56085fe08f233301211 666 154 1545535035752 205 | 4b 777 206 | 825dc642cb6eb9a060e54bf8d69288fbee4904 666 15 1545535035752 207 | refs 777 208 | heads 777 209 | master 666 41 1545535035752 210 | test 666 41 1545535035753 211 | COMMIT_EDITMSG 666 14 1545535034098 212 | HEAD 666 23 1545535034098 213 | config 666 137 1545535034098 214 | index 666 65 1545535034098 215 | test-deleteRef.git 777 216 | objects 777 217 | 4c 777 218 | 4c5cdd3f7e76632060ade0602df7ed76deaea7 666 30 1545535035753 219 | 85 777 220 | a4a30ffe48e8f7eeb837e758323269dffd77dd 666 83 1545535035753 221 | 98 777 222 | 4082c29b02a7ab31c34c7a8a52df9a706d7987 666 57 1545535035753 223 | cf 777 224 | c039a0acb68bee8bb4f3b13b6b211dbb8c1a69 666 130 1545535035753 225 | d6 777 226 | 70460b4b4aece5915caf5c68d12f560a9fe3e4 666 29 1545535035753 227 | e5 777 228 | 788e2a76a5ab6f84df776df153d5220c7b0fd5 666 151 1545535035754 229 | refs 777 230 | heads 777 231 | master 666 41 1545535035755 232 | tags 777 233 | latest 666 41 1545535035755 234 | packed-and-loose 666 41 1545535035755 235 | prev 666 41 1545535035756 236 | HEAD 666 23 1545535034099 237 | config 666 111 1545535034099 238 | description 666 73 1545535034099 239 | index 666 225 1545535034099 240 | packed-refs 666 169 1545535034099 241 | test-deleteRemote.git 777 242 | config 666 286 1545535034100 243 | test-deleteTag.git 777 244 | objects 777 245 | 4c 777 246 | 4c5cdd3f7e76632060ade0602df7ed76deaea7 666 30 1545535035756 247 | 85 777 248 | a4a30ffe48e8f7eeb837e758323269dffd77dd 666 83 1545535035756 249 | 98 777 250 | 4082c29b02a7ab31c34c7a8a52df9a706d7987 666 57 1545535035756 251 | cf 777 252 | c039a0acb68bee8bb4f3b13b6b211dbb8c1a69 666 130 1545535035757 253 | d6 777 254 | 70460b4b4aece5915caf5c68d12f560a9fe3e4 666 29 1545535035757 255 | e5 777 256 | 788e2a76a5ab6f84df776df153d5220c7b0fd5 666 151 1545535035757 257 | refs 777 258 | heads 777 259 | master 666 41 1545535035757 260 | tags 777 261 | latest 666 41 1545535035757 262 | prev 666 41 1545535035758 263 | HEAD 666 23 1545535034100 264 | config 666 111 1545535034100 265 | description 666 73 1545535034100 266 | index 666 225 1545535034101 267 | test-detachedHead.git 777 268 | objects 777 269 | 24 777 270 | fc0a9596d825432f06f0c7e3286898b89a04f2 666 119 1545535035758 271 | 4b 777 272 | 825dc642cb6eb9a060e54bf8d69288fbee4904 666 15 1545535035758 273 | 8f 777 274 | 7a1ba0e8eb33743c8c7fa9920b6da809c99107 666 50 1545535035759 275 | cc 777 276 | 628ccd10742baea8241c5924df992b5c019f71 666 21 1545535035759 277 | d4 777 278 | ddffa3db6f52466e09076e70fbefa0641b4cea 666 146 1545535035759 279 | refs 777 280 | heads 777 281 | master 666 41 1545535035759 282 | HEAD 666 41 1545535034101 283 | config 666 130 1545535034102 284 | index 666 104 1545535034102 285 | test-dumb-http-server.git 777 286 | hooks 777 287 | applypatch-msg.sample 666 478 1545535034964 288 | commit-msg.sample 666 896 1545535034964 289 | post-update.sample 666 189 1545535034965 290 | pre-applypatch.sample 666 424 1545535034965 291 | pre-commit.sample 666 1642 1545535034965 292 | pre-push.sample 666 1348 1545535034965 293 | pre-rebase.sample 666 4951 1545535034966 294 | pre-receive.sample 666 544 1545535034966 295 | prepare-commit-msg.sample 666 1239 1545535034966 296 | update.sample 666 3610 1545535034967 297 | info 777 298 | exclude 666 240 1545535034967 299 | refs 666 116 1545535034968 300 | objects 777 301 | 02 777 302 | d7e1758463e18f93ec3f1cb0151601c8c0ac31 666 76 1545535035760 303 | 08 777 304 | faabdc782b92e1e8d371fdd13b30c0a3f54676 666 18 1545535035760 305 | 5a 777 306 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035760 307 | 97 777 308 | c024f73eaab2781bf3691597bc7c833cb0e22f 666 158 1545535035761 309 | a6 777 310 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035762 311 | a7 777 312 | ab08ac7277588e8ccb9b22047d6ebb751dee0f 666 102 1545535035762 313 | c8 777 314 | 2587c97be8f9a10088590e06c9d0f767ed5c4a 666 158 1545535035762 315 | c9 777 316 | 44ebc28f05731ef588ac6298485ba5e8bf3704 666 21 1545535035762 317 | e9 777 318 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035762 319 | info 777 320 | packs 666 1 1545535035762 321 | refs 777 322 | heads 777 323 | master 666 41 1545535035762 324 | HEAD 666 23 1545535034103 325 | description 666 73 1545535034103 326 | git-daemon-export-ok 666 0 1545535034103 327 | packed-refs 666 155 1545535034104 328 | config 666 168 1545535034348 329 | test-empty.git 777 330 | HEAD 666 23 1545535034104 331 | config 666 92 1545535034104 332 | test-expandOid.git 777 333 | objects 777 334 | 03 777 335 | 3417a0c0d27e31ee2402838910c3de49b10d8c 666 886 1545535035763 336 | 3417ae18b174f078f2f44232cb7a374f4c60ce 666 886 1545535035764 337 | 1e 777 338 | 40fdfba1cf17f3c9f9f3d6b392b1865e5147b9 666 780 1545535035764 339 | e1 777 340 | 0ebb90d03eaacca84de1af0a59b444232da99e 666 850 1545535035764 341 | pack 777 342 | pack-1a1e70d2f116e8cb0cb42d26019e5c7d0eb01888.idx 666 22604 1545535035764 343 | pack-1a1e70d2f116e8cb0cb42d26019e5c7d0eb01888.pack 666 338009 1545535035764 344 | refs 777 345 | heads 777 346 | master 666 41 1545535035765 347 | test-branch 666 41 1545535035765 348 | remotes 777 349 | origin 777 350 | master 666 41 1545535036144 351 | test-branch 666 41 1545535036144 352 | tags 777 353 | test-tag 666 41 1545535035768 354 | HEAD 666 23 1545535034105 355 | packed-refs 666 97 1545535034105 356 | shallow 666 41 1545535034105 357 | test-fetch-cors.git 777 358 | HEAD 666 23 1545535034105 359 | config 666 265 1545535034236 360 | test-fetch.git 777 361 | HEAD 666 23 1545535034105 362 | config 666 223 1545535034352 363 | test-findMergeBase.git 777 364 | info 777 365 | refs 666 655 1545535034968 366 | objects 777 367 | 4c 777 368 | 658ff41121ddada50c47e4c72c092a9f7bf2be 666 202 1545535035768 369 | 85 777 370 | 303393b9fd415d48913dfec47d42db184dc4d8 666 202 1545535035768 371 | info 777 372 | packs 666 54 1545535035769 373 | pack 777 374 | pack-d939071faf14bfd9941fb7d995ae2d768df42a27.idx 666 1576 1545535035769 375 | pack-d939071faf14bfd9941fb7d995ae2d768df42a27.pack 666 1211 1545535035770 376 | refs 777 377 | heads 777 378 | Z1 666 41 1545535035770 379 | Z2 666 41 1545535035770 380 | HEAD 666 18 1545535034105 381 | packed-refs 666 701 1545535034106 382 | test-findRoot 777 383 | foobar 777 384 | bar 777 385 | baz 777 386 | buzz 777 387 | .gitkeep 666 0 1545535036235 388 | test-getRemoteInfo 777 389 | isomorphic-git.git 777 390 | hooks 777 391 | applypatch-msg.sample 666 478 1545535035770 392 | commit-msg.sample 666 896 1545535035770 393 | post-update.sample 666 189 1545535035771 394 | pre-applypatch.sample 666 424 1545535035771 395 | pre-commit.sample 666 1642 1545535035772 396 | pre-push.sample 666 1348 1545535035772 397 | pre-rebase.sample 666 4898 1545535035772 398 | pre-receive.sample 666 544 1545535035773 399 | prepare-commit-msg.sample 666 1239 1545535035773 400 | update.sample 666 3610 1545535035773 401 | info 777 402 | exclude 666 240 1545535035773 403 | objects 777 404 | pack 777 405 | pack-b5a80dec9ec3c8cf05444ea51f090349a22e28a8.idx 666 134492 1545535036144 406 | pack-b5a80dec9ec3c8cf05444ea51f090349a22e28a8.pack 666 3825027 1545535036144 407 | refs 777 408 | heads 777 409 | develop 666 41 1545535036144 410 | dist 666 41 1545535036144 411 | master 666 41 1545535036145 412 | test-branch 666 41 1545535036145 413 | test-branch-shallow-clone 666 41 1545535036145 414 | pull 777 415 | 20 777 416 | head 666 41 1545535036236 417 | 21 777 418 | head 666 41 1545535036236 419 | 23 777 420 | head 666 41 1545535036236 421 | merge 666 41 1545535036236 422 | 24 777 423 | head 666 41 1545535036237 424 | 28 777 425 | head 666 41 1545535036237 426 | 30 777 427 | head 666 41 1545535036237 428 | 31 777 429 | head 666 41 1545535036238 430 | 33 777 431 | head 666 41 1545535036238 432 | merge 666 41 1545535036238 433 | 35 777 434 | head 666 41 1545535036238 435 | 36 777 436 | head 666 41 1545535036239 437 | 49 777 438 | head 666 41 1545535036239 439 | 5 777 440 | head 666 41 1545535036239 441 | 51 777 442 | head 666 41 1545535036239 443 | 52 777 444 | head 666 41 1545535036239 445 | 53 777 446 | head 666 41 1545535036240 447 | 54 777 448 | head 666 41 1545535036240 449 | 55 777 450 | head 666 41 1545535036240 451 | merge 666 41 1545535036241 452 | 56 777 453 | head 666 41 1545535036241 454 | 57 777 455 | head 666 41 1545535036241 456 | 58 777 457 | head 666 41 1545535036242 458 | 59 777 459 | head 666 41 1545535036242 460 | 60 777 461 | head 666 41 1545535036243 462 | 61 777 463 | head 666 41 1545535036243 464 | 62 777 465 | head 666 41 1545535036243 466 | 63 777 467 | head 666 41 1545535036243 468 | 64 777 469 | head 666 41 1545535036243 470 | 65 777 471 | head 666 41 1545535036243 472 | 66 777 473 | head 666 41 1545535036244 474 | 67 777 475 | head 666 41 1545535036244 476 | 68 777 477 | head 666 41 1545535036244 478 | 69 777 479 | head 666 41 1545535036245 480 | 70 777 481 | head 666 41 1545535036246 482 | tags 777 483 | test-tag 666 41 1545535036150 484 | v0.0.1 666 41 1545535036151 485 | v0.0.10 666 41 1545535036152 486 | v0.0.11 666 41 1545535036153 487 | v0.0.12 666 41 1545535036153 488 | v0.0.13 666 41 1545535036153 489 | v0.0.14 666 41 1545535036153 490 | v0.0.15 666 41 1545535036153 491 | v0.0.16 666 41 1545535036153 492 | v0.0.17 666 41 1545535036154 493 | v0.0.18 666 41 1545535036155 494 | v0.0.19 666 41 1545535036155 495 | v0.0.2 666 41 1545535036155 496 | v0.0.20 666 41 1545535036155 497 | v0.0.21 666 41 1545535036156 498 | v0.0.22 666 41 1545535036156 499 | v0.0.23 666 41 1545535036156 500 | v0.0.24 666 41 1545535036156 501 | v0.0.25 666 41 1545535036157 502 | v0.0.26 666 41 1545535036157 503 | v0.0.27 666 41 1545535036157 504 | v0.0.28 666 41 1545535036157 505 | v0.0.29 666 41 1545535036158 506 | v0.0.3 666 41 1545535036158 507 | v0.0.30 666 41 1545535036159 508 | v0.0.31 666 41 1545535036159 509 | v0.0.32 666 41 1545535036159 510 | v0.0.33 666 41 1545535036159 511 | v0.0.34 666 41 1545535036159 512 | v0.0.35 666 41 1545535036160 513 | v0.0.36 666 41 1545535036160 514 | v0.0.37 666 41 1545535036161 515 | v0.0.38 666 41 1545535036161 516 | v0.0.4 666 41 1545535036161 517 | v0.0.5 666 41 1545535036162 518 | v0.0.6 666 41 1545535036162 519 | v0.0.7 666 41 1545535036162 520 | v0.0.8 666 41 1545535036163 521 | v0.0.9 666 41 1545535036163 522 | v0.1.0 666 41 1545535036163 523 | v0.2.0 666 41 1545535036163 524 | v0.2.1 666 41 1545535036164 525 | v0.3.0 666 41 1545535036164 526 | v0.4.0 666 41 1545535036164 527 | v0.5.0 666 41 1545535036164 528 | v0.5.1 666 41 1545535036164 529 | v0.6.0 666 41 1545535036165 530 | v0.6.1 666 41 1545535036166 531 | FETCH_HEAD 666 9732 1545535034969 532 | HEAD 666 23 1545535034969 533 | description 666 73 1545535034969 534 | git-daemon-export-ok 666 0 1545535034969 535 | shallow 666 41 1545535034970 536 | config 666 178 1545535035353 537 | test-issue-84.git 777 538 | HEAD 666 23 1545535034106 539 | config 666 266 1545535034356 540 | test-listBranches.git 777 541 | refs 777 542 | heads 777 543 | feature 777 544 | supercool 666 41 1545535036166 545 | master 666 41 1545535035775 546 | test-branch 666 41 1545535035775 547 | packed-refs 666 169 1545535034106 548 | test-listCommitsAndTags.git 777 549 | info 777 550 | exclude 666 240 1545535034970 551 | objects 777 552 | 01 777 553 | 8cb07aa9747e34bbd6b6f614a32f72d6e82d55 666 365 1545535035775 554 | 02 777 555 | 1beda55a84b562e68610fedfae3b137e22416c 666 944 1545535035775 556 | 04 777 557 | 4b49d6fffc5cbb411784d6baa8b990d3856189 666 717 1545535035775 558 | 5d9aa4358916d42ab896547ab21b6014606eb2 666 590 1545535035776 559 | 9041e4f5c2216806a69c78b3405d8882a6ce7a 666 436 1545535035776 560 | 05 777 561 | 18502faba1c63489562641c36a989e0f574d95 666 828 1545535035776 562 | 09 777 563 | 9fcd63a9a0387c1455717f9eaf50de058bdb5b 666 590 1545535035776 564 | 14 777 565 | be9ce5a0358b0c002e3147e1fefa4c0d513926 666 194 1545535035777 566 | 1a 777 567 | 874199bec2f87134661592b628fb73a65c02de 666 1992 1545535035777 568 | 1b 777 569 | c495d4e55ace48ae281ae1ec640e3a17eb443f 666 82 1545535035777 570 | 1f 777 571 | 00cbefb9a1fafb3b3368d5dcc062b19e38db71 666 444 1545535035778 572 | 3134056c4ec8e09e12f8ce01853cb1ac697a06 666 713 1545535035779 573 | 20 777 574 | b7372348daee8900e17c62ac44e0a8ad448ea1 666 206 1545535035779 575 | 23 777 576 | 9bccdf526f72fd3e50993c91bac022d069e5ae 666 2275 1545535035779 577 | 29 777 578 | 1e89bcd9d3bdd72f11a5a53196151ceb063ba4 666 287 1545535035779 579 | 2d 777 580 | 1486375a20fd5d88290048f94e6f591f198dfc 666 853 1545535035779 581 | 34 777 582 | d583663baf662b192a248adc8cca9702dc6dc5 666 943 1545535035779 583 | 38 777 584 | e18cba8c86ec3a412c5d1360118e9d19f4042f 666 973 1545535035780 585 | f6f3fe2ad90ede508cfdbd4c256e13d6bbfa1d 666 206 1545535035780 586 | 46 777 587 | 5d59659c40563765956e57d427262acbdc8485 666 287 1545535035781 588 | 4f 777 589 | 95b6947580a4673c51453c5b5836785d987db5 666 343 1545535035781 590 | 51 777 591 | 3b4a3e27ebf880868824240f0d294e7e128bdf 666 82 1545535035781 592 | 54 777 593 | 6e375adc31e156ed6b497c944b7d48da5693bc 666 316 1545535035782 594 | 9a20c0b0149a2a1a06ae433d7a29c95b800718 666 526 1545535035782 595 | d31620146af42375e0974c2ac2864218d2f197 666 444 1545535035783 596 | 55 777 597 | 158d43140549daee24c7d3534107723c0c056a 666 112 1545535035783 598 | 58 777 599 | 2558dc77d80d429fa473b67cedc6d56f1c201d 666 83 1545535035783 600 | 59 777 601 | 8b7d6806c264d36776ff80635f95a0188d730d 666 223 1545535035783 602 | 61 777 603 | 107b1197fa6d0852f3460b496e945d7e4a9d83 666 83 1545535035784 604 | 64 777 605 | 280b806c9760fb2313b29cc66dabbd0fe18e26 666 97 1545535035784 606 | 6d 777 607 | 925adc4818a513ae2683a12fe381269f45a7c7 666 328 1545535035784 608 | 75 777 609 | 63df69fe82c944d0f6cf38890a4438af3ab8ee 666 82 1545535035784 610 | 76 777 611 | 5de32d0e29d7fdc1c7ee597b90e942b9af23d6 666 83 1545535035784 612 | 79 777 613 | b998ff69792a63fa08d1f4c81bb8797c51d325 666 50527 1545535035785 614 | 85 777 615 | 26dac4a73c678fb3d77f2bbbe9dba7c9bc73dd 666 178 1545535035785 616 | 86 777 617 | b9eff27444689c422fa9d3803c574dc402d0d0 666 444 1545535035785 618 | 8d 777 619 | c4edaecb96a628b0d99e6e6ccc9bb2e13f2f54 666 82 1545535035785 620 | 8f 777 621 | dacc19e1252859a4dabec095a88596e1ae3840 666 444 1545535035786 622 | 94 777 623 | 9af990f79e1bc45f4c5fbe83985cb81eebe6df 666 82 1545535035786 624 | 98 777 625 | 8c291502fc1acafede7f1ac3e913625ce665de 666 83 1545535035787 626 | 9d 777 627 | 88857c845dcecdf5628abbfdfb72c25bab9a1f 666 872 1545535035787 628 | 9f 777 629 | 5295803b5e1e8ac460656cb8ba57fef77099de 666 179 1545535035788 630 | a1 777 631 | eb6dd53e364e03d2f51588decc4d64be7d6caa 666 1045 1545535035788 632 | a4 777 633 | b2efea01e274f56744342bc9021fbab838f5fc 666 194 1545535035788 634 | a6 777 635 | a2b7186bf8001b510128be269870aac023271a 666 83 1545535035788 636 | a7 777 637 | 975f328539ded81f4453665bc578a5bd3e10a1 666 205 1545535035788 638 | a8 777 639 | 103169a3cb5fb457e5232a0e36728511e736b4 666 443 1545535035788 640 | b4 777 641 | 8ad49aac46081a38b501384b0364d10af9e2db 666 333 1545535035789 642 | c3d831107de93e90405a275230aca61ac05099 666 391 1545535035789 643 | bd 777 644 | 8f4c8e205694750d9f0c467accf0b9bb6b17eb 666 654 1545535035790 645 | c0 777 646 | ef15bf5e38ba0221662569ee5a9aacc4cf6e2d 666 607 1545535035790 647 | c2 777 648 | ff4cb3f08a522ad4df96a664c39a697353e28e 666 80 1545535035791 649 | c3 777 650 | eb866fa2762e6abc36d226f2bb75017afb03e7 666 206 1545535035791 651 | c6 777 652 | 0bbbe99e96578105c57c4b3f2b6ebdf863edbc 666 838 1545535035791 653 | c7 777 654 | 7052f99c33dbe3d2a120805fcebe9e2194b6f9 666 809 1545535035791 655 | cf 777 656 | 19438b68da32ad4966266b01b2982546fc75c0 666 255 1545535035792 657 | d9 777 658 | b13df014743e24012795fe3ee07c57ce23326d 666 83 1545535035792 659 | df 777 660 | 2a3bdb4911011eddf4fa94c6f58857e0f120c7 666 391 1545535035793 661 | e0 777 662 | 5547ea87ea55eff079de295ff56f483e5b4439 666 830 1545535035793 663 | eb 777 664 | dedf722a3ec938da3fd53eb74fdea55c48a19d 666 814 1545535035793 665 | f2 777 666 | c6495f30d02e90ae465da3e1f5c48649dc77c4 666 694 1545535035793 667 | fc 777 668 | ba58ea4011b37731bba8b142b5d4a515f17839 666 226 1545535035793 669 | fe 777 670 | 9b7fecc60887755883b5d68fb706985d1f8667 666 495 1545535035794 671 | ff 777 672 | 53907ca9bd6d5f376188c57bcf1dd4650574c9 666 463 1545535035795 673 | HEAD 666 23 1545535034107 674 | config 666 104 1545535034108 675 | description 666 73 1545535034108 676 | test-listFiles.git 777 677 | index 666 2972 1545535034108 678 | test-listObjects.git 777 679 | info 777 680 | exclude 666 240 1545535034971 681 | objects 777 682 | 01 777 683 | 8cb07aa9747e34bbd6b6f614a32f72d6e82d55 666 365 1545535035795 684 | 02 777 685 | 1beda55a84b562e68610fedfae3b137e22416c 666 944 1545535035795 686 | 04 777 687 | 4b49d6fffc5cbb411784d6baa8b990d3856189 666 717 1545535035795 688 | 5d9aa4358916d42ab896547ab21b6014606eb2 666 590 1545535035795 689 | 9041e4f5c2216806a69c78b3405d8882a6ce7a 666 436 1545535035795 690 | 05 777 691 | 18502faba1c63489562641c36a989e0f574d95 666 828 1545535035796 692 | 06 777 693 | 81d1f146a390a27dcc628e982cadb5d7b500bc 666 191 1545535035796 694 | 09 777 695 | 653a52077d5d747a9dfe64b3a8889c1c095c4d 666 779 1545535035796 696 | 9fcd63a9a0387c1455717f9eaf50de058bdb5b 666 590 1545535035797 697 | df7e67ab764a5cb65a0826a3a0b2a3b3ebf621 666 172 1545535035797 698 | 0b 777 699 | 583a1bd52bd7e5579a4f7e47624d990f6ba282 666 1954 1545535035797 700 | 0d 777 701 | d30ed7ff2c698dec130d3451c99cdd9bbf1633 666 1951 1545535035798 702 | 0f 777 703 | 9a58bfaf1f7808815bd9528d7af2de50dd1455 666 409 1545535035799 704 | 14 777 705 | be9ce5a0358b0c002e3147e1fefa4c0d513926 666 194 1545535035799 706 | 17 777 707 | 4ca973d3b327276c73d0cf76698e7acb0f6bee 666 461 1545535035799 708 | 1a 777 709 | 874199bec2f87134661592b628fb73a65c02de 666 1992 1545535035799 710 | 1b 777 711 | 1b826853b6152b7e7ec4b39f47ecfb60143372 666 168 1545535035800 712 | c495d4e55ace48ae281ae1ec640e3a17eb443f 666 82 1545535035800 713 | 1d 777 714 | b939d41956405f755e69ab570296c7ed3cec99 666 154 1545535035800 715 | 1f 777 716 | 00cbefb9a1fafb3b3368d5dcc062b19e38db71 666 444 1545535035800 717 | 01bbfeddb009fb49b1db0f192cb73d39c8f36d 666 56 1545535035800 718 | 3134056c4ec8e09e12f8ce01853cb1ac697a06 666 713 1545535035801 719 | 20 777 720 | a1a5802c7abda8c76fb2771dbbe53458c8b37e 666 633 1545535035802 721 | 23 777 722 | 9bccdf526f72fd3e50993c91bac022d069e5ae 666 2275 1545535035802 723 | 25 777 724 | 58e35095ac9323ff381c4488a9bb9f7df21486 666 209 1545535035802 725 | 9426f84740e41182bbb63b7a2bb99f914c1bb6 666 83 1545535035802 726 | 29 777 727 | 1e89bcd9d3bdd72f11a5a53196151ceb063ba4 666 287 1545535035802 728 | d2fa6dbccd06134eb8832d1d942f4b29495f3d 666 609 1545535035802 729 | 2c 777 730 | 634e11f873c36e72e8415ceb5085de3d9ec507 666 57 1545535035803 731 | 2d 777 732 | 1486375a20fd5d88290048f94e6f591f198dfc 666 853 1545535035804 733 | 2e 777 734 | 957a173810564d93efd6cc48f4149602e8dab0 666 114 1545535035804 735 | 30 777 736 | 2013a7292bac15dd19b1d29049d69f4862561d 666 51 1545535035804 737 | 32 777 738 | 8e74b65839f7e5a8ae3b54e0b49180a5b7b82b 666 475 1545535035804 739 | c1b1592175ea226ca22482fe7d32e1593d432b 666 415 1545535035805 740 | 34 777 741 | 347c8ae54b8e84947f901d86761875364a9c5a 666 1018 1545535035805 742 | d583663baf662b192a248adc8cca9702dc6dc5 666 943 1545535035805 743 | 35 777 744 | cd3a23c15e845849ebc397dd53a2f5692c52bf 666 106 1545535035806 745 | 38 777 746 | 30b74f75e13b734e117ac65d04fbd92d7ffa30 666 101 1545535035806 747 | 863b2030eae29b3efc81115f035a4c4cc95122 666 632 1545535035806 748 | e18cba8c86ec3a412c5d1360118e9d19f4042f 666 973 1545535035807 749 | f6f3fe2ad90ede508cfdbd4c256e13d6bbfa1d 666 206 1545535035807 750 | 39 777 751 | 46e7b3cfb83c71718dc4fab8d50ce6171f2443 666 390 1545535035807 752 | 652652308646ebbd3f4291821a47754b1d6297 666 1331 1545535035807 753 | 989d7eed8a07022d06f3e5ea435ca55100b679 666 1348 1545535035807 754 | 3b 777 755 | fe1b6d98d0e263ba3bdf53a30766dac50a49a0 666 1939 1545535035808 756 | 3d 777 757 | ebcd9ed0f762dd757d0426c1b7f41c7875e378 666 79 1545535035809 758 | 45 777 759 | 10b2a983dc2253eb5f48a2bbe382b77a7a9be5 666 50263 1545535035809 760 | 165512f94f2cc9fb0259b6d1b8311cfda3928d 666 1297 1545535035809 761 | 46 777 762 | 05e706ad65f35fcc31b09d2d0d331eb6ae9a2a 666 164 1545535035809 763 | 5d59659c40563765956e57d427262acbdc8485 666 287 1545535035809 764 | 47 777 765 | 967151a5ff9366ca5d86e261c9ceb835d7b722 666 45 1545535035810 766 | 4a 777 767 | 58bdcdef3eb91264dfca0279959d98c16568d5 666 48 1545535035810 768 | 4b 777 769 | 3d47c6f13cceb2c789ae2c5d867b114d9e7fd3 666 150 1545535035810 770 | 4f 777 771 | 179be70e391c4b1b681dbdbd8126e10afd9800 666 48 1545535035811 772 | 95b6947580a4673c51453c5b5836785d987db5 666 343 1545535035811 773 | 51 777 774 | 3b4a3e27ebf880868824240f0d294e7e128bdf 666 82 1545535035811 775 | 71f8a8291d7edc31a6670800d5967cfd6be830 666 953 1545535035811 776 | e632299486ed66ac253a467599cfbdfeb6b635 666 142 1545535035812 777 | 54 777 778 | 6e375adc31e156ed6b497c944b7d48da5693bc 666 316 1545535035812 779 | 9a20c0b0149a2a1a06ae433d7a29c95b800718 666 526 1545535035812 780 | d31620146af42375e0974c2ac2864218d2f197 666 444 1545535035812 781 | 55 777 782 | 158d43140549daee24c7d3534107723c0c056a 666 112 1545535035812 783 | 7574c5a4e2d6378c3b44e954790e2903a066b7 666 1389 1545535035813 784 | 86cc035bd67b62124e24fcd0b60d7414909c9f 666 310 1545535035813 785 | 57 777 786 | 8c0a3a1363260575f1b1273eb28bd2c1b7dbac 666 517 1545535035814 787 | 58 777 788 | 2558dc77d80d429fa473b67cedc6d56f1c201d 666 83 1545535035814 789 | 59 777 790 | 2075707f47b954553198633f0bfd57ae50e9e6 666 190 1545535035814 791 | 8b7d6806c264d36776ff80635f95a0188d730d 666 223 1545535035815 792 | 5d 777 793 | 7ed63f6b79c3cd9f134f48be2521abfb9c6de1 666 108 1545535035815 794 | a715b5521d883f4f67e39811c7ee392a28d31a 666 870 1545535035815 795 | 60 777 796 | b63f5f7298f29033ec8b5fa19b689062282363 666 119 1545535035815 797 | 61 777 798 | 107b1197fa6d0852f3460b496e945d7e4a9d83 666 83 1545535035815 799 | 2ee65b07c8a0e3d4f29be1ecc5f93ae26b6938 666 111 1545535035817 800 | 64 777 801 | 280b806c9760fb2313b29cc66dabbd0fe18e26 666 97 1545535035817 802 | 65 777 803 | 1bd64f6c3716fc8a6e8ba6dcbad2d93337f279 666 8209 1545535035817 804 | ea841dbbff2244f144032ddd78d65d96093cb1 666 57 1545535035817 805 | 68 777 806 | be1ea418b8a032adac85a15bffe15493be4607 666 906 1545535035818 807 | 69 777 808 | 9e0fe71ff254b1bec2320356657b024bb88657 666 800 1545535035818 809 | 6a 777 810 | 41d6d49eec3593bee14ad19d0a0c0ac08be937 666 474 1545535035818 811 | 8cb808568e67faf2f6d675a8738eb45131f787 666 858 1545535035818 812 | 6c 777 813 | ea33858a30385a387c0704031c8be70b92cb4f 666 254 1545535035818 814 | 6d 777 815 | 925adc4818a513ae2683a12fe381269f45a7c7 666 328 1545535035818 816 | 6e 777 817 | 31e029d051eb7bacfd86e93f7eab43d4730054 666 1372 1545535035820 818 | 9d67f2a308ca3d580b78c9f5894dde12fe981d 666 21850 1545535035820 819 | 72 777 820 | ed82e1d1ddb86e6577c80b8a4dfeb4f99c1975 666 140 1545535035820 821 | 73 777 822 | 003f80fc868def28485d20180318454614e9b5 666 45 1545535035820 823 | 75 777 824 | 63df69fe82c944d0f6cf38890a4438af3ab8ee 666 82 1545535035821 825 | 76 777 826 | 5de32d0e29d7fdc1c7ee597b90e942b9af23d6 666 83 1545535035821 827 | ea0cb6ca47b3a49622d23037e8e7de24281ccf 666 53 1545535035822 828 | 79 777 829 | b998ff69792a63fa08d1f4c81bb8797c51d325 666 50527 1545535035822 830 | 7a 777 831 | 0268b317ebc8deec82ff2481bd12c1d9640d05 666 970 1545535035822 832 | 7b 777 833 | 7571cec88e81dbb74d9d923961d12a8a6e6fde 666 952 1545535035822 834 | 7c 777 835 | 9c3ffd4d4a85bc927e7ea1165c48ab79a4fc81 666 312 1545535035822 836 | 81 777 837 | dc4f14a323823a2a3a160a3d1e93de210b45ea 666 82 1545535035822 838 | 85 777 839 | 26dac4a73c678fb3d77f2bbbe9dba7c9bc73dd 666 178 1545535035823 840 | 86 777 841 | b9eff27444689c422fa9d3803c574dc402d0d0 666 444 1545535035823 842 | 87 777 843 | 5870fdb5b991aa654783f1b2e6bd2b10b557e3 666 252 1545535035824 844 | 96b53ec4f56869fb579d63ba88b369a8784d54 666 86 1545535035824 845 | 8b 777 846 | 3b108bb2f15275072b2479896d425700434754 666 325 1545535035824 847 | 8d 777 848 | 37248c1e0b669d1f35d8cc23072a315eac51dc 666 800 1545535035825 849 | c4edaecb96a628b0d99e6e6ccc9bb2e13f2f54 666 82 1545535035825 850 | 90 777 851 | 695c16a4f16741ddc792cf98d35fbafe0b5915 666 96 1545535035825 852 | 92 777 853 | af21f1dc2c6568d473934950b59ae670c82e41 666 234 1545535035825 854 | 94 777 855 | 9af990f79e1bc45f4c5fbe83985cb81eebe6df 666 82 1545535035826 856 | 95 777 857 | ca9a4a8527d48de831b077dc3114173f125f62 666 343 1545535035827 858 | 98 777 859 | 6815d07b173ab7e8a9b40c843464cb52f0d86f 666 415 1545535035827 860 | 8c291502fc1acafede7f1ac3e913625ce665de 666 83 1545535035827 861 | 99 777 862 | 2bede683598137b91ef6dae75cc125137c37e0 666 636 1545535035827 863 | 9a 777 864 | c4f57c7d98020514ca2baebff511841baa9d93 666 215 1545535035828 865 | 9d 777 866 | 2fb91440d05039be549cd3ba21adad2823608a 666 284 1545535035828 867 | 88857c845dcecdf5628abbfdfb72c25bab9a1f 666 872 1545535035828 868 | 9e 777 869 | 922d7be75abba592701aec08117c1e023945ac 666 82 1545535035828 870 | 9f 777 871 | 5295803b5e1e8ac460656cb8ba57fef77099de 666 179 1545535035828 872 | a0 777 873 | 03a3623c66cf6262db282d769574c556dafe1d 666 50 1545535035829 874 | 42f9c1c6de833686ae4a45f92515d028925df7 666 3113 1545535035829 875 | a1 777 876 | eb6dd53e364e03d2f51588decc4d64be7d6caa 666 1045 1545535035829 877 | a3 777 878 | e4b3692cca9dad58987ed92792d283b0a0237d 666 184 1545535035830 879 | a4 777 880 | b2efea01e274f56744342bc9021fbab838f5fc 666 194 1545535035830 881 | a6 777 882 | a2b7186bf8001b510128be269870aac023271a 666 83 1545535035831 883 | a7 777 884 | 975f328539ded81f4453665bc578a5bd3e10a1 666 205 1545535035831 885 | a8 777 886 | 103169a3cb5fb457e5232a0e36728511e736b4 666 443 1545535035831 887 | aa 777 888 | 0a764f20a691959a981c60de223717d3ddeaa1 666 81 1545535035831 889 | af 777 890 | 2500de57708675419c4db313e654bf462e2d67 666 786 1545535035831 891 | b0 777 892 | 1f344da89c62977b1b4dc7d36cfaa546c630c8 666 360 1545535035832 893 | b4 777 894 | 8ad49aac46081a38b501384b0364d10af9e2db 666 333 1545535035832 895 | c3d831107de93e90405a275230aca61ac05099 666 391 1545535035832 896 | ba 777 897 | 3a429b8bc8054427ee8209f1d725cc9bd0de3a 666 784 1545535035833 898 | bb 777 899 | cce901fa961f9df12681ba834c2f91ad4ba7b0 666 414 1545535035833 900 | f3e21f43fa4fe25eb925bfcb7c0434f7c2dc7d 666 46 1545535035833 901 | bc 777 902 | 9cf585a68cf44e2c5115ec3e9519c37d2c2e85 666 244 1545535035834 903 | bd 777 904 | 8f4c8e205694750d9f0c467accf0b9bb6b17eb 666 654 1545535035834 905 | be 777 906 | 395f63b878f28a86a418ae0d6a291e6fa51de9 666 124 1545535035834 907 | bf 777 908 | 45c6a17356154e11e14325624e170201f3f875 666 2750 1545535035835 909 | c0 777 910 | 585e66fee320a6d69c25248487464f8fe24dc6 666 256 1545535035835 911 | ef15bf5e38ba0221662569ee5a9aacc4cf6e2d 666 607 1545535035835 912 | c2 777 913 | ff4cb3f08a522ad4df96a664c39a697353e28e 666 80 1545535035836 914 | c3 777 915 | eb866fa2762e6abc36d226f2bb75017afb03e7 666 206 1545535035836 916 | c5 777 917 | 059df27e646eca3c538fcb435bb5d3a5335e3d 666 82 1545535035836 918 | c6 777 919 | 0bbbe99e96578105c57c4b3f2b6ebdf863edbc 666 838 1545535035837 920 | 75a17ccb1578bca836decf90205fdad743827d 666 712 1545535035837 921 | cb 777 922 | 089cd89a7d7686d284d8761201649346b5aa1c 666 37 1545535035838 923 | cc 777 924 | 4322b0f6cbf8586f13a6bb8f8f12c51ce795e8 666 378 1545535035838 925 | 8e98a7cfe36a74850eb52fbd2e0ca07a80a508 666 311 1545535035838 926 | ce 777 927 | 9f7fb173dc5846e37396df70003b389d311d1b 666 208 1545535035838 928 | cbde44129b2b2537546d252176b49e986e9fda 666 2702 1545535035839 929 | e0a86071305d3440f89da9fc57691ac1e934c9 666 1080 1545535035839 930 | cf 777 931 | 19438b68da32ad4966266b01b2982546fc75c0 666 255 1545535035839 932 | d5 777 933 | 45cdabdbddafca3501d2114506fc86e50e9824 666 113 1545535035839 934 | d6 777 935 | a0dd9c1c5e4df22b72979a097974ea3b7ba87b 666 151 1545535035840 936 | d7 777 937 | b25aab38d0772c4e0da5a88c84f369241e230e 666 3005 1545535035841 938 | e5d2d48a2ddf316d000fd6ff82b039a18e4489 666 184 1545535035841 939 | d8 777 940 | db70567effb326c173b17b2b5b6df22f1c4cc6 666 637 1545535035841 941 | d9 777 942 | 38addbdc76f7de47775442ca045a2720d48be0 666 642 1545535035841 943 | 69fd286799af572eacb3b49c0881e1ea445ed3 666 79 1545535035841 944 | b13df014743e24012795fe3ee07c57ce23326d 666 83 1545535035842 945 | df 777 946 | 2a3bdb4911011eddf4fa94c6f58857e0f120c7 666 391 1545535035842 947 | e0 777 948 | 5547ea87ea55eff079de295ff56f483e5b4439 666 830 1545535035842 949 | e5 777 950 | b8f9cece335aca583406109216173174068c73 666 21 1545535035842 951 | e8 777 952 | 54a11f6df60bc01c75bebf046f01a0954ebafc 666 290 1545535035843 953 | 7c4acef861687deb2728c9f3667a0dc03a8958 666 84 1545535035843 954 | e9 777 955 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035844 956 | eb 777 957 | 1a0c5bb07374f97343010b019aeb3e6be472ee 666 523 1545535035844 958 | dedf722a3ec938da3fd53eb74fdea55c48a19d 666 814 1545535035844 959 | f0 777 960 | 3ae7b490022507f83729b9227e723ab1587a38 666 2254 1545535035845 961 | f2 777 962 | c6495f30d02e90ae465da3e1f5c48649dc77c4 666 694 1545535035845 963 | f7 777 964 | 9f954746fe5542f951401a43233e57ba959047 666 346 1545535035845 965 | fa 777 966 | 03bdcc920bdad46598cabd249a1f629cef010c 666 261 1545535035845 967 | fc 777 968 | ba58ea4011b37731bba8b142b5d4a515f17839 666 226 1545535035846 969 | fd 777 970 | ba2ad440c231d15a2179f729b4b50ab5860df2 666 158 1545535035846 971 | fe 777 972 | 9b7fecc60887755883b5d68fb706985d1f8667 666 495 1545535035846 973 | ff 777 974 | 53907ca9bd6d5f376188c57bcf1dd4650574c9 666 463 1545535035846 975 | a7d2f123658ad92b52d0f2e5198458b5d854d4 666 1371 1545535035846 976 | HEAD 666 23 1545535034109 977 | config 666 104 1545535034109 978 | description 666 73 1545535034110 979 | test-listRemotes.git 777 980 | config 666 286 1545535034111 981 | test-listTags.git 777 982 | refs 777 983 | tags 777 984 | local-tag 666 41 1545535035847 985 | packed-refs 666 3915 1545535034111 986 | test-log-complex.git 777 987 | objects 777 988 | 1c 777 989 | e759dd468c1ea830e8befbbdcf79e591346153 666 164 1545535035847 990 | 4a 777 991 | cc58cd881f48c4662c4554ab268e77bcd34b71 666 132 1545535035848 992 | 4b 777 993 | 825dc642cb6eb9a060e54bf8d69288fbee4904 666 15 1545535035848 994 | 69 777 995 | a23c077590c6fe45a0b7a35442751ca359edf1 666 163 1545535035848 996 | 6c 777 997 | abb8ab77d3fc40858db84416dfd1a41fe1c2fd 666 163 1545535035848 998 | 8b 777 999 | b702b66d8def74b2a9642309eb23a5f76779dc 666 167 1545535035849 1000 | 9e 777 1001 | eab5143ea4a6dde4ede004e4882e2467dde340 666 233 1545535035849 1002 | ad 777 1003 | 5f1992b8ff758bc9fe457acf905093dd75b7b1 666 163 1545535035849 1004 | b5 777 1005 | 129e2726d68c93ed09a3eaec9dda5e76fd4a87 666 169 1545535035850 1006 | c4 777 1007 | e447f61fcaf49032265bfe3dea32383339d910 666 163 1545535035850 1008 | cc 777 1009 | c9ef071f1b27210fa0df2f8665f4ad550358e8 666 203 1545535035850 1010 | ec 777 1011 | 2db34cd04249ea6c31ed6d367656b0f2ab25c6 666 167 1545535035851 1012 | f1 777 1013 | eca35203ee2b578f23e0e7c8b8c2c48927d597 666 163 1545535035851 1014 | refs 777 1015 | heads 777 1016 | bar 666 41 1545535035851 1017 | baz 666 41 1545535035851 1018 | foo 666 41 1545535035852 1019 | master 666 41 1545535035852 1020 | HEAD 666 23 1545535034111 1021 | config 666 156 1545535034111 1022 | test-log.git 777 1023 | objects 777 1024 | 1c 777 1025 | 04ba2c3b7c61cdfc0ddc3f9515116bc0e06863 666 832 1545535035852 1026 | 1e 777 1027 | 40fdfba1cf17f3c9f9f3d6b392b1865e5147b9 666 780 1545535035853 1028 | 3c 777 1029 | 945912219e6fc27a9100bf099687c69c88afed 666 818 1545535035853 1030 | 3e 777 1031 | 80cede3c2a753a5272ed4d93496b67bb65cb0d 666 821 1545535035853 1032 | ae 777 1033 | 054080bcfd04c84e0820e0cf74b31f4a422d7c 666 879 1545535035853 1034 | e1 777 1035 | 0ebb90d03eaacca84de1af0a59b444232da99e 666 850 1545535035853 1036 | refs 777 1037 | heads 777 1038 | master 666 41 1545535035854 1039 | remotes 777 1040 | origin 777 1041 | test-branch 666 41 1545535036166 1042 | HEAD 666 23 1545535034112 1043 | config 666 104 1545535034112 1044 | description 666 73 1545535034112 1045 | test-pack 777 1046 | foobar-76178ca22ef818f971fca371d84bce571d474b1d.idx 666 1408 1545535034113 1047 | foobar-76178ca22ef818f971fca371d84bce571d474b1d.pack 666 8188 1545535034113 1048 | test-pack.git 777 1049 | objects 777 1050 | 0b 777 1051 | fe8fa3764089465235461624f2ede1533e74ec 666 443 1545535035854 1052 | 32 777 1053 | 8e74b65839f7e5a8ae3b54e0b49180a5b7b82b 666 475 1545535035854 1054 | 41 777 1055 | 4a0afa7e20452d90ab52de1c024182531c5c52 666 206 1545535035855 1056 | 51 777 1057 | 71f8a8291d7edc31a6670800d5967cfd6be830 666 953 1545535035855 1058 | 54 777 1059 | 77471ab5a6a8f2c217023532475044117a8f2c 666 634 1545535035856 1060 | 5a 777 1061 | 9da3272badb2d3c8dbab463aed5741acb15a33 666 833 1545535035856 1062 | 79 777 1063 | 83b4770a894a068152dfe6f347ea9b5ae561c5 666 235 1545535035856 1064 | 97 777 1065 | b32c43e96acc7873a1990e409194cb92421522 666 296 1545535035857 1066 | a5 777 1067 | 9efbcd7640e659ec81887a2599711f8d9ef801 666 1971 1545535035857 1068 | e5 777 1069 | abf40a5b37382c700f51ac5c2aeefdadb8e184 666 323 1545535035857 1070 | f0 777 1071 | 3ae7b490022507f83729b9227e723ab1587a38 666 2254 1545535035858 1072 | fd 777 1073 | ba2ad440c231d15a2179f729b4b50ab5860df2 666 158 1545535035858 1074 | HEAD 666 23 1545535034113 1075 | config 666 104 1545535034113 1076 | test-packfile.git 777 1077 | objects 777 1078 | pack 777 1079 | pack-1a1e70d2f116e8cb0cb42d26019e5c7d0eb01888.idx 666 22604 1545535035858 1080 | pack-1a1e70d2f116e8cb0cb42d26019e5c7d0eb01888.pack 666 338009 1545535035859 1081 | test-pull-client.git 777 1082 | hooks 777 1083 | applypatch-msg.sample 666 478 1545535034972 1084 | commit-msg.sample 666 896 1545535034973 1085 | post-update.sample 666 189 1545535034973 1086 | pre-applypatch.sample 666 424 1545535034974 1087 | pre-commit.sample 666 1642 1545535034974 1088 | pre-push.sample 666 1348 1545535034974 1089 | pre-rebase.sample 666 4951 1545535034975 1090 | pre-receive.sample 666 544 1545535034975 1091 | prepare-commit-msg.sample 666 1239 1545535034975 1092 | update.sample 666 3610 1545535034975 1093 | info 777 1094 | exclude 666 240 1545535034976 1095 | objects 777 1096 | 02 777 1097 | d7e1758463e18f93ec3f1cb0151601c8c0ac31 666 76 1545535035859 1098 | 08 777 1099 | faabdc782b92e1e8d371fdd13b30c0a3f54676 666 18 1545535035859 1100 | 5a 777 1101 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035859 1102 | 97 777 1103 | c024f73eaab2781bf3691597bc7c833cb0e22f 666 158 1545535035859 1104 | a6 777 1105 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035860 1106 | a7 777 1107 | ab08ac7277588e8ccb9b22047d6ebb751dee0f 666 102 1545535035860 1108 | c8 777 1109 | 2587c97be8f9a10088590e06c9d0f767ed5c4a 666 158 1545535035861 1110 | c9 777 1111 | 44ebc28f05731ef588ac6298485ba5e8bf3704 666 21 1545535035861 1112 | e9 777 1113 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035861 1114 | refs 777 1115 | heads 777 1116 | master 666 41 1545535035862 1117 | HEAD 666 23 1545535034114 1118 | config 666 283 1545535034115 1119 | description 666 73 1545535034115 1120 | packed-refs 666 155 1545535034115 1121 | test-pull-server.git 777 1122 | hooks 777 1123 | applypatch-msg.sample 666 478 1545535034976 1124 | commit-msg.sample 666 896 1545535034976 1125 | post-update.sample 666 189 1545535034977 1126 | pre-applypatch.sample 666 424 1545535034977 1127 | pre-commit.sample 666 1642 1545535034977 1128 | pre-push.sample 666 1348 1545535034978 1129 | pre-rebase.sample 666 4951 1545535034978 1130 | pre-receive.sample 666 544 1545535034979 1131 | prepare-commit-msg.sample 666 1239 1545535034979 1132 | update.sample 666 3610 1545535034979 1133 | info 777 1134 | exclude 666 240 1545535034979 1135 | objects 777 1136 | 02 777 1137 | d7e1758463e18f93ec3f1cb0151601c8c0ac31 666 76 1545535035862 1138 | 08 777 1139 | faabdc782b92e1e8d371fdd13b30c0a3f54676 666 18 1545535035862 1140 | 5a 777 1141 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035862 1142 | 97 777 1143 | c024f73eaab2781bf3691597bc7c833cb0e22f 666 158 1545535035863 1144 | a6 777 1145 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035868 1146 | a7 777 1147 | ab08ac7277588e8ccb9b22047d6ebb751dee0f 666 102 1545535035868 1148 | c8 777 1149 | 2587c97be8f9a10088590e06c9d0f767ed5c4a 666 158 1545535035869 1150 | c9 777 1151 | 44ebc28f05731ef588ac6298485ba5e8bf3704 666 21 1545535035869 1152 | e9 777 1153 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035869 1154 | refs 777 1155 | heads 777 1156 | master 666 41 1545535035870 1157 | HEAD 666 23 1545535034115 1158 | description 666 73 1545535034115 1159 | git-daemon-export-ok 666 0 1545535034116 1160 | packed-refs 666 155 1545535034117 1161 | config 666 168 1545535034405 1162 | test-push 777 1163 | foo.git 777 1164 | info 777 1165 | exclude 666 240 1545535035870 1166 | objects 777 1167 | 5a 777 1168 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535036166 1169 | a6 777 1170 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535036166 1171 | e9 777 1172 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535036166 1173 | refs 777 1174 | heads 777 1175 | master 666 41 1545535036167 1176 | HEAD 666 23 1545535034980 1177 | config 666 315 1545535034981 1178 | description 666 73 1545535034981 1179 | git-daemon-export-ok 666 0 1545535034981 1180 | test-push-server-auth.git 777 1181 | info 777 1182 | exclude 666 240 1545535034982 1183 | objects 777 1184 | 5a 777 1185 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035871 1186 | a6 777 1187 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035871 1188 | e9 777 1189 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035871 1190 | refs 777 1191 | heads 777 1192 | master 666 41 1545535035871 1193 | .htpasswd 666 47 1545535034117 1194 | HEAD 666 23 1545535034117 1195 | config 666 103 1545535034118 1196 | description 666 73 1545535034118 1197 | git-daemon-export-ok 666 0 1545535034118 1198 | test-push-server.git 777 1199 | hooks 777 1200 | post-receive 777 67 1545535034982 1201 | update 777 214 1545535034982 1202 | info 777 1203 | exclude 666 240 1545535034982 1204 | objects 777 1205 | 5a 777 1206 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035872 1207 | a6 777 1208 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035872 1209 | e9 777 1210 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035873 1211 | refs 777 1212 | heads 777 1213 | master 666 41 1545535035873 1214 | HEAD 666 23 1545535034118 1215 | config 666 103 1545535034119 1216 | description 666 73 1545535034119 1217 | git-daemon-export-ok 666 0 1545535034119 1218 | test-push.git 777 1219 | info 777 1220 | exclude 666 240 1545535034983 1221 | objects 777 1222 | 18 777 1223 | df7980ddf987c2e3e20eb8007727c659b37216 666 22 1545535035873 1224 | 48 777 1225 | af7b9b559ef89fc4a41d492282c927d63f306b 666 76 1545535035873 1226 | 5a 777 1227 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035873 1228 | a6 777 1229 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035874 1230 | c0 777 1231 | 3e131196f43a78888415924bcdcbf3090f3316 666 801 1545535035874 1232 | db 777 1233 | 13ba6f4e969df956fab620f5735281aef832a2 666 136 1545535035875 1234 | e9 777 1235 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035875 1236 | refs 777 1237 | heads 777 1238 | master 666 41 1545535035875 1239 | remotes 777 1240 | pseudo 777 1241 | master 666 41 1545535036167 1242 | tags 777 1243 | annotated-tag 666 41 1545535035875 1244 | lightweight-tag 666 41 1545535035875 1245 | HEAD 666 23 1545535034119 1246 | config 666 553 1545535034120 1247 | description 666 73 1545535034120 1248 | test-readObject.git 777 1249 | objects 777 1250 | 03 777 1251 | 3417ae18b174f078f2f44232cb7a374f4c60ce 666 886 1545535035876 1252 | 1e 777 1253 | 40fdfba1cf17f3c9f9f3d6b392b1865e5147b9 666 780 1545535035876 1254 | e1 777 1255 | 0ebb90d03eaacca84de1af0a59b444232da99e 666 850 1545535035877 1256 | pack 777 1257 | pack-1a1e70d2f116e8cb0cb42d26019e5c7d0eb01888.idx 666 22604 1545535035877 1258 | pack-1a1e70d2f116e8cb0cb42d26019e5c7d0eb01888.pack 666 338009 1545535035877 1259 | refs 777 1260 | heads 777 1261 | master 666 41 1545535035877 1262 | test-branch 666 41 1545535035878 1263 | remotes 777 1264 | origin 777 1265 | master 666 41 1545535036167 1266 | test-branch 666 41 1545535036167 1267 | tags 777 1268 | test-tag 666 41 1545535035878 1269 | HEAD 666 23 1545535034121 1270 | packed-refs 666 97 1545535034121 1271 | shallow 666 41 1545535034121 1272 | test-remove.git 777 1273 | index 666 2048 1545535034121 1274 | test-resetIndex 777 1275 | a.txt 666 6 1545535034122 1276 | b.txt 666 8 1545535034122 1277 | d.txt 666 11 1545535034122 1278 | test-resetIndex.git 777 1279 | objects 777 1280 | 3c 777 1281 | eb96f2cac30550faa17088d52b38ca161cb946 666 76 1545535035878 1282 | 65 777 1283 | 78b57ec768162a8a61efa52cf58bd927a38ebd 666 115 1545535035878 1284 | 77 777 1285 | 787b8f756d76b1d470f0dbb919d5d35dc55ef8 666 23 1545535035879 1286 | 88 777 1287 | ed47e7926fc9b0a27894cac0882d341c23335e 666 23 1545535035879 1288 | 89 777 1289 | 5a23b41a53a99670b5fd4092e4199e3a328e02 666 27 1545535035880 1290 | e9 777 1291 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035880 1292 | refs 777 1293 | heads 777 1294 | master 666 41 1545535035881 1295 | HEAD 666 23 1545535034122 1296 | config 666 92 1545535034123 1297 | index 666 262 1545535034123 1298 | test-resolveRef.git 777 1299 | objects 777 1300 | 03 777 1301 | 3417ae18b174f078f2f44232cb7a374f4c60ce 666 886 1545535035881 1302 | 1e 777 1303 | 40fdfba1cf17f3c9f9f3d6b392b1865e5147b9 666 780 1545535035881 1304 | e1 777 1305 | 0ebb90d03eaacca84de1af0a59b444232da99e 666 850 1545535035882 1306 | refs 777 1307 | heads 777 1308 | master 666 41 1545535035882 1309 | test-branch 666 41 1545535035882 1310 | remotes 777 1311 | origin 777 1312 | master 666 41 1545535036168 1313 | test-branch 666 41 1545535036169 1314 | tags 777 1315 | test-tag 666 41 1545535035883 1316 | HEAD 666 23 1545535034124 1317 | packed-refs 666 97 1545535034124 1318 | shallow 666 41 1545535034124 1319 | test-status 777 1320 | g 777 1321 | g.txt 666 0 1545535034984 1322 | h 777 1323 | h.txt 666 0 1545535034984 1324 | i 777 1325 | .gitignore 666 6 1545535034984 1326 | i.txt 666 0 1545535034984 1327 | .gitignore 666 19 1545535034125 1328 | a.txt 666 6 1545535034125 1329 | b.txt 666 8 1545535034125 1330 | d.txt 666 11 1545535034125 1331 | test-status.git 777 1332 | hooks 777 1333 | applypatch-msg.sample 666 478 1545535034984 1334 | commit-msg.sample 666 896 1545535034985 1335 | post-update.sample 666 189 1545535034985 1336 | pre-applypatch.sample 666 424 1545535034985 1337 | pre-commit.sample 666 1642 1545535034985 1338 | pre-push.sample 666 1348 1545535034986 1339 | pre-rebase.sample 666 4951 1545535034986 1340 | pre-receive.sample 666 544 1545535034986 1341 | prepare-commit-msg.sample 666 1239 1545535034987 1342 | update.sample 666 3610 1545535034988 1343 | info 777 1344 | exclude 666 240 1545535034988 1345 | objects 777 1346 | 02 777 1347 | d7e1758463e18f93ec3f1cb0151601c8c0ac31 666 76 1545535035883 1348 | 08 777 1349 | faabdc782b92e1e8d371fdd13b30c0a3f54676 666 18 1545535035883 1350 | 5a 777 1351 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035883 1352 | 97 777 1353 | c024f73eaab2781bf3691597bc7c833cb0e22f 666 158 1545535035883 1354 | a6 777 1355 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035885 1356 | a7 777 1357 | ab08ac7277588e8ccb9b22047d6ebb751dee0f 666 102 1545535035885 1358 | c8 777 1359 | 2587c97be8f9a10088590e06c9d0f767ed5c4a 666 158 1545535035885 1360 | c9 777 1361 | 44ebc28f05731ef588ac6298485ba5e8bf3704 666 21 1545535035886 1362 | e9 777 1363 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035886 1364 | refs 777 1365 | heads 777 1366 | master 666 41 1545535035886 1367 | HEAD 666 23 1545535034125 1368 | description 666 73 1545535034126 1369 | git-daemon-export-ok 666 0 1545535034126 1370 | index 666 281 1545535034127 1371 | packed-refs 666 155 1545535034128 1372 | config 666 168 1545535034415 1373 | test-statusMatrix 777 1374 | a.txt 666 6 1545535034128 1375 | b.txt 666 8 1545535034133 1376 | d.txt 666 11 1545535034133 1377 | test-statusMatrix.git 777 1378 | hooks 777 1379 | applypatch-msg.sample 666 478 1545535034988 1380 | commit-msg.sample 666 896 1545535034989 1381 | post-update.sample 666 189 1545535034989 1382 | pre-applypatch.sample 666 424 1545535034989 1383 | pre-commit.sample 666 1642 1545535034989 1384 | pre-push.sample 666 1348 1545535034989 1385 | pre-rebase.sample 666 4951 1545535034989 1386 | pre-receive.sample 666 544 1545535034990 1387 | prepare-commit-msg.sample 666 1239 1545535034991 1388 | update.sample 666 3610 1545535034991 1389 | info 777 1390 | exclude 666 240 1545535034991 1391 | objects 777 1392 | 02 777 1393 | d7e1758463e18f93ec3f1cb0151601c8c0ac31 666 76 1545535035886 1394 | 08 777 1395 | faabdc782b92e1e8d371fdd13b30c0a3f54676 666 18 1545535035886 1396 | 5a 777 1397 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035886 1398 | 97 777 1399 | c024f73eaab2781bf3691597bc7c833cb0e22f 666 158 1545535035887 1400 | a6 777 1401 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035887 1402 | a7 777 1403 | ab08ac7277588e8ccb9b22047d6ebb751dee0f 666 102 1545535035887 1404 | c8 777 1405 | 2587c97be8f9a10088590e06c9d0f767ed5c4a 666 158 1545535035888 1406 | c9 777 1407 | 44ebc28f05731ef588ac6298485ba5e8bf3704 666 21 1545535035888 1408 | e9 777 1409 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035888 1410 | refs 777 1411 | heads 777 1412 | master 666 41 1545535035890 1413 | HEAD 666 23 1545535034134 1414 | description 666 73 1545535034134 1415 | git-daemon-export-ok 666 0 1545535034135 1416 | index 666 281 1545535034136 1417 | packed-refs 666 155 1545535034136 1418 | config 666 168 1545535034417 1419 | test-tag.git 777 1420 | objects 777 1421 | 98 777 1422 | 4082c29b02a7ab31c34c7a8a52df9a706d7987 666 57 1545535035890 1423 | cf 777 1424 | c039a0acb68bee8bb4f3b13b6b211dbb8c1a69 666 130 1545535035890 1425 | d6 777 1426 | 70460b4b4aece5915caf5c68d12f560a9fe3e4 666 29 1545535035890 1427 | refs 777 1428 | heads 777 1429 | master 666 41 1545535035890 1430 | tags 777 1431 | existing-tag 666 41 1545535035890 1432 | HEAD 666 23 1545535034137 1433 | config 666 111 1545535034137 1434 | description 666 73 1545535034137 1435 | index 666 145 1545535034137 1436 | packed-refs 666 107 1545535034137 1437 | test-unicode-paths 777 1438 | 日本語 666 1 1545535034138 1439 | test-uploadPack.git 777 1440 | objects 777 1441 | 5a 777 1442 | 8905a02e181fe1821068b8c0f48cb6633d5b81 666 781 1545535035891 1443 | a6 777 1444 | 9b2f53db7b7ba59f43ee15f5c42166297c4262 666 49 1545535035891 1445 | e9 777 1446 | 65047ad7c57865823c7d992b1d046ea66edf78 666 21 1545535035891 1447 | refs 777 1448 | heads 777 1449 | master 666 41 1545535035892 1450 | HEAD 666 23 1545535034138 1451 | config 666 103 1545535034138 1452 | description 666 73 1545535034138 1453 | git-daemon-export-ok 666 0 1545535034139 1454 | test-writeRef.git 777 1455 | objects 777 1456 | 98 777 1457 | 4082c29b02a7ab31c34c7a8a52df9a706d7987 666 57 1545535035892 1458 | cf 777 1459 | c039a0acb68bee8bb4f3b13b6b211dbb8c1a69 666 130 1545535035893 1460 | d6 777 1461 | 70460b4b4aece5915caf5c68d12f560a9fe3e4 666 29 1545535035893 1462 | refs 777 1463 | heads 777 1464 | master 666 41 1545535035894 1465 | HEAD 666 23 1545535034139 1466 | config 666 111 1545535034139 1467 | description 666 73 1545535034140 1468 | index 666 145 1545535034140 1469 | test-merge.git 777 1470 | objects 777 1471 | 1c 777 1472 | 04ba2c3b7c61cdfc0ddc3f9515116bc0e06863 666 832 1545535035971 1473 | 1e 777 1474 | 40fdfba1cf17f3c9f9f3d6b392b1865e5147b9 666 780 1545535035971 1475 | 3c 777 1476 | 945912219e6fc27a9100bf099687c69c88afed 666 818 1545535035971 1477 | 3e 777 1478 | 80cede3c2a753a5272ed4d93496b67bb65cb0d 666 821 1545535035972 1479 | ae 777 1480 | 054080bcfd04c84e0820e0cf74b31f4a422d7c 666 879 1545535035972 1481 | e1 777 1482 | 0ebb90d03eaacca84de1af0a59b444232da99e 666 850 1545535035973 1483 | refs 777 1484 | heads 777 1485 | master 666 41 1545535035973 1486 | medium 666 41 1545535035973 1487 | newest 666 41 1545535035973 1488 | oldest 666 41 1545535035974 1489 | HEAD 666 23 1545535034211 1490 | config 666 104 1545535034211 1491 | description 666 73 1545535034212 1492 | pgp-keys.js 666 2963 1545535033390 1493 | __helpers__ 777 1494 | FixtureFS.js 666 2365 1545535033393 1495 | assertSnapshot.js 666 557 1545535033393 1496 | can-i-skip-tests.js 666 435 1545535033393 1497 | fix-version-number.js 666 248 1545535033393 1498 | jasmine-snapshots.js 666 1904 1545535033393 1499 | karma-load-successful-browsers.js 666 1232 1545535033394 1500 | karma-pr-comment-reporter.js 666 2204 1545535033394 1501 | karma-successful-browsers-reporter.js 666 1215 1545535033395 1502 | karma-translate-browser.js 666 583 1545535033395 1503 | make_http_index.js 666 1165 1545535033396 1504 | set-TRAVIS_PULL_REQUEST_SHA.js 666 411 1545535033397 1505 | set-test-timeout.js 666 221 1545535033398 1506 | tweet.js 666 653 1545535033398 1507 | __nockbacks__ 777 1508 | GitRemoteHTTP - preparePull (Github response).json 666 9211 1545535033398 1509 | GitRemoteHTTP - preparePush (mock response).json 666 688 1545535033398 1510 | getRemoteInfo - getRemoteInfo.json 666 9151 1545535033398 1511 | pull - basic pull.json 666 3976 1545535033399 1512 | GitRemoteHTTP - preparePull (mock response).json 666 528 1545535033498 1513 | __snapshots__ 777 1514 | server-only.test-getRemoteInfo.js.snap 666 8552 1545535033399 1515 | test-GitError.js.snap 666 259 1545535033399 1516 | test-GitIndex.js.snap 666 2472 1545535033400 1517 | test-GitObjectManager.js.snap 666 186 1545535033400 1518 | test-GitPackIndex.js.snap 666 20036 1545535033400 1519 | test-GitRefManager.js.snap 666 8420 1545535033400 1520 | test-addRemote.js.snap 666 745 1545535033401 1521 | test-branch.js.snap 666 1136 1545535033401 1522 | test-checkout.js.snap 666 2502 1545535033401 1523 | test-commit.js.snap 666 537 1545535033401 1524 | test-deleteBranch.js.snap 666 1487 1545535033401 1525 | test-deleteRemote.js.snap 666 368 1545535033402 1526 | test-deleteTag.js.snap 666 125 1545535033402 1527 | test-exports.js.snap 666 806 1545535033402 1528 | test-flatFileListToDirectoryStructure.js.snap 666 11873 1545535033403 1529 | test-getRemoteInfo.js.snap 666 242 1545535033403 1530 | test-listBranches.js.snap 666 225 1545535033403 1531 | test-listObjects.js.snap 666 8010 1545535033404 1532 | test-listTags.js.snap 666 618 1545535033404 1533 | test-log.js.snap 666 23450 1545535033404 1534 | test-push.js.snap 666 276 1545535033405 1535 | test-readObject.js.snap 666 20362 1545535033405 1536 | test-remove.js.snap 666 2284 1545535033405 1537 | test-resetIndex.js.snap 666 358 1545535033405 1538 | test-resolveRef.js.snap 666 548 1545535033406 1539 | test-writeRef.js.snap 666 240 1545535033406 1540 | test-listCommitsAndTags.js.snap 666 296 1545535033498 1541 | test-listFiles.js.snap 666 1671 1545535033499 1542 | server-only.test-GitRemoteHTTP.js 666 2415 1545535032920 1543 | server-only.test-getRemoteInfo.js 666 1042 1545535032920 1544 | server-only.test-lockfile.js 666 2107 1545535032921 1545 | server-only.test-pull.js 666 908 1545535032921 1546 | test-GitAnnotatedTag.js 666 2464 1545535032921 1547 | test-GitConfig.js 666 16168 1545535032921 1548 | test-GitError.js 666 856 1545535032922 1549 | test-GitIndex.js 666 1690 1545535032922 1550 | test-GitObjectManager.js 666 804 1545535032922 1551 | test-GitPackIndex.js 666 4718 1545535032923 1552 | test-GitPktLine.js 666 6257 1545535032923 1553 | test-GitRefManager.js 666 1580 1545535032923 1554 | test-GitRefSpecSet.js 666 1328 1545535032923 1555 | test-GitRemoteManager.js 666 2496 1545535032923 1556 | test-add.js 666 1111 1545535032924 1557 | test-annotatedTag.js 666 2193 1545535032924 1558 | test-basic-test.js 666 949 1545535032924 1559 | test-branch.js 666 2289 1545535032925 1560 | test-checkout.js 666 3117 1545535032925 1561 | test-clone.js 666 4796 1545535032926 1562 | test-commit.js 666 5480 1545535032926 1563 | test-config.js 666 2083 1545535032926 1564 | test-cores.js 666 1964 1545535032926 1565 | test-currentBranch.js 666 1037 1545535032926 1566 | test-deleteRef.js 666 1302 1545535032927 1567 | test-deleteRemote.js 666 1124 1545535032927 1568 | test-deleteTag.js 666 1095 1545535032927 1569 | test-expandOid.js 666 1572 1545535032927 1570 | test-exports.js 666 447 1545535032928 1571 | test-fetch.js 666 5460 1545535032928 1572 | test-findMergeBase.js 666 6578 1545535032928 1573 | test-findRoot.js 666 1275 1545535032929 1574 | test-flatFileListToDirectoryStructure.js 666 2194 1545535032929 1575 | test-getRemoteInfo.js 666 1066 1545535032929 1576 | test-init.js 666 902 1545535032930 1577 | test-listObjects.js 666 892 1545535032930 1578 | test-listRemotes.js 666 554 1545535032930 1579 | test-log.js 666 2159 1545535032930 1580 | test-merge.js 666 2564 1545535032931 1581 | test-pack.js 666 1453 1545535032932 1582 | test-push.js 666 4809 1545535032932 1583 | test-readObject.js 666 8102 1545535032932 1584 | test-remove.js 666 1218 1545535032932 1585 | test-resetIndex.js 666 1218 1545535032932 1586 | test-resolveRef.js 666 2434 1545535032933 1587 | test-status.js 666 2852 1545535032933 1588 | test-statusMatrix.js 666 1857 1545535032933 1589 | test-tag.js 666 1911 1545535032934 1590 | test-unicode-paths.js 666 4346 1545535032934 1591 | test-uploadPack.js 666 899 1545535032934 1592 | test-version.js 666 243 1545535032935 1593 | test-wire.js 666 15245 1545535032935 1594 | test-writeObject.js 666 15495 1545535032935 1595 | test-writeRef.js 666 1430 1545535032935 1596 | test-addRemote.js 666 1796 1545535032981 1597 | test-deleteBranch.js 666 2310 1545535032981 1598 | test-listBranches.js 666 909 1545535032981 1599 | test-listFiles.js 666 859 1545535032982 1600 | test-listCommitsAndTags.js 666 824 1545535033114 1601 | test-listTags.js 666 619 1545535033114 1602 | spec 777 1603 | support 777 1604 | jasmine.json 666 170 1545535033406 1605 | src 777 1606 | commands 777 1607 | add.js 666 1393 1545535033407 1608 | addRemote.js 666 1968 1545535033407 1609 | annotatedTag.js 666 2411 1545535033407 1610 | branch.js 666 1616 1545535033407 1611 | checkout.js 666 6551 1545535033408 1612 | clone.js 666 2297 1545535033408 1613 | commit.js 666 3571 1545535033409 1614 | config.js 666 1573 1545535033409 1615 | currentBranch.js 666 1238 1545535033409 1616 | deleteRemote.js 666 963 1545535033409 1617 | fetch.js 666 10296 1545535033410 1618 | findMergeBase.js 666 2340 1545535033410 1619 | findRoot.js 666 893 1545535033410 1620 | getRemoteInfo.js 666 1888 1545535033410 1621 | indexPack.js 666 1042 1545535033410 1622 | init.js 666 1127 1545535033411 1623 | isDescendent.js 666 2580 1545535033411 1624 | listCommitsAndTags.js 666 1946 1545535033411 1625 | listFiles.js 666 1611 1545535033412 1626 | listObjects.js 666 1664 1545535033412 1627 | listRemotes.js 666 872 1545535033412 1628 | log.js 666 2424 1545535033412 1629 | merge.js 666 2816 1545535033412 1630 | pack.js 666 2138 1545535033413 1631 | pull.js 666 1732 1545535033414 1632 | push.js 666 5375 1545535033414 1633 | readObject.js 666 3901 1545535033414 1634 | remove.js 666 838 1545535033414 1635 | resetIndex.js 666 2032 1545535033415 1636 | sign.js 666 1859 1545535033415 1637 | status.js 666 5375 1545535033415 1638 | statusMatrix.js 666 3266 1545535033416 1639 | tag.js 666 1177 1545535033416 1640 | types.js 666 149 1545535033416 1641 | uploadPack.js 666 1455 1545535033416 1642 | verify.js 666 2014 1545535033416 1643 | version.js 666 286 1545535033417 1644 | walkBeta1.js 666 2039 1545535033418 1645 | writeObject.js 666 1720 1545535033418 1646 | writeRef.js 666 1381 1545535033418 1647 | deleteBranch.js 666 1451 1545535033499 1648 | deleteRef.js 666 600 1545535033499 1649 | deleteTag.js 666 848 1545535033499 1650 | listBranches.js 666 631 1545535033500 1651 | resolveRef.js 666 724 1545535033500 1652 | expandRef.js 666 683 1545535033664 1653 | listTags.js 666 581 1545535033665 1654 | expandOid.js 666 682 1545535033974 1655 | managers 777 1656 | GitConfigManager.js 666 726 1545535033418 1657 | GitIgnoreManager.js 666 2027 1545535033418 1658 | GitIndexManager.js 666 1827 1545535033418 1659 | GitRefManager.js 666 9579 1545535033419 1660 | GitRemoteHTTP.js 666 5126 1545535033419 1661 | GitRemoteManager.js 666 1552 1545535033419 1662 | GitShallowManager.js 666 1235 1545535033419 1663 | README.md 666 270 1545535033420 1664 | models 777 1665 | FileSystem.js 666 6431 1545535033420 1666 | GitAnnotatedTag.js 666 3102 1545535033420 1667 | GitCommit.js 666 4786 1545535033421 1668 | GitConfig.js 666 7587 1545535033421 1669 | GitError.js 666 11400 1545535033421 1670 | GitIndex.js 666 7240 1545535033422 1671 | GitObject.js 666 867 1545535033422 1672 | GitPackIndex.js 666 13931 1545535033422 1673 | GitPackedRefs.js 666 1339 1545535033422 1674 | GitPktLine.js 666 2964 1545535033422 1675 | GitRefSpec.js 666 1326 1545535033423 1676 | GitRefSpecSet.js 666 942 1545535033423 1677 | GitSideBand.js 666 4665 1545535033423 1678 | GitTree.js 666 3713 1545535033424 1679 | GitWalkerFs.js 666 3214 1545535033424 1680 | GitWalkerIndex.js 666 2891 1545535033424 1681 | GitWalkerRepo.js 666 3366 1545535033425 1682 | PGP.js 666 7496 1545535033425 1683 | RunningMinimum.js 666 500 1545535033425 1684 | SignedGitCommit.js 666 1553 1545535033425 1685 | storage 777 1686 | expandOid.js 666 1044 1545535033426 1687 | expandOidLoose.js 666 381 1545535033426 1688 | readObject.js 666 2053 1545535033426 1689 | readObjectLoose.js 666 355 1545535033426 1690 | readObjectPacked.js 666 1320 1545535033426 1691 | readPackIndex.js 666 684 1545535033427 1692 | writeObject.js 666 664 1545535033427 1693 | writeObjectLoose.js 666 762 1545535033428 1694 | expandOidPacked.js 666 952 1545535033500 1695 | utils 777 1696 | BufferCursor.js 666 1444 1545535033428 1697 | StreamReader.js 666 3619 1545535033428 1698 | arrayRange.js 666 166 1545535033428 1699 | auth.js 666 548 1545535033429 1700 | basename.js 666 174 1545535033429 1701 | calculateBasicAuthHeader.js 666 148 1545535033429 1702 | calculateBasicAuthUsernamePasswordPair.js 666 1706 1545535033429 1703 | compareAge.js 666 93 1545535033429 1704 | comparePath.js 666 180 1545535033430 1705 | compareRefNames.js 666 276 1545535033430 1706 | compareStats.js 666 922 1545535033430 1707 | compareStrings.js 666 122 1545535033431 1708 | dirname.js 666 192 1545535033432 1709 | filterCapabilities.js 666 227 1545535033432 1710 | flatFileListToDirectoryStructure.js 666 1482 1545535033432 1711 | formatAuthor.js 666 969 1545535033432 1712 | git-list-pack.js 666 3460 1545535033432 1713 | hashObject.js 666 210 1545535033433 1714 | indent.js 666 139 1545535033433 1715 | join.js 666 253 1545535033433 1716 | log.js 666 434 1545535033433 1717 | logCommit.js 666 711 1545535033434 1718 | normalizeAuthorObject.js 666 659 1545535033434 1719 | normalizeMode.js 666 1195 1545535033434 1720 | normalizeNewlines.js 666 249 1545535033435 1721 | normalizeStats.js 666 1289 1545535033435 1722 | oauth2.js 666 739 1545535033435 1723 | outdent.js 666 115 1545535033435 1724 | padHex.js 666 97 1545535033435 1725 | parseAuthor.js 666 752 1545535033436 1726 | path.js 666 248 1545535033436 1727 | patternRoot.js 666 190 1545535033436 1728 | pkg.js 666 129 1545535033437 1729 | plugins.js 666 2814 1545535033437 1730 | resolveTree.js 666 841 1545535033437 1731 | shasum.js 666 264 1545535033437 1732 | sleep.js 666 104 1545535033438 1733 | symbols.js 666 418 1545535033438 1734 | unionOfIterators.js 666 2241 1545535033438 1735 | worthWalking.js 666 218 1545535033438 1736 | wire 777 1737 | parseReceivePackResponse.js 666 1071 1545535033439 1738 | parseRefsAdResponse.js 666 1887 1545535033439 1739 | parseUploadPackRequest.js 666 1232 1545535033439 1740 | parseUploadPackResponse.js 666 1510 1545535033439 1741 | writeRefsAdResponse.js 666 876 1545535033440 1742 | writeUploadPackRequest.js 666 1195 1545535033440 1743 | writeReceivePackRequest.js 666 535 1545535033500 1744 | index.d.ts 666 13095 1545535032936 1745 | index.js 666 1996 1545535032936 1746 | internal-apis.d.ts 666 47 1545535032936 1747 | internal-apis.js 666 1909 1545535032936 1748 | .all-contributorsrc 666 6686 1545535032592 1749 | .babelrc 666 137 1545535032592 1750 | .editorconfig 666 40 1545535032593 1751 | .gitattributes 666 114 1545535032594 1752 | .gitignore 666 974 1545535032594 1753 | .releaserc 666 254 1545535032594 1754 | CODE_OF_CONDUCT.md 666 3215 1545535032594 1755 | CONTRIBUTING.md 666 3916 1545535032595 1756 | LICENSE.md 666 1072 1545535032596 1757 | README.md 666 25315 1545535032596 1758 | azure-pipelines.yml 666 2521 1545535032596 1759 | cli.js 777 695 1545535032596 1760 | jest.config.js 666 492 1545535032596 1761 | karma.conf.js 666 6959 1545535032597 1762 | package-lock.json 666 815202 1545535032598 1763 | package-scripts.js 666 4802 1545535032598 1764 | package.json 666 3996 1545535032598 1765 | renovate.json 666 244 1545535032598 1766 | rollup.config.js 666 804 1545535032598 1767 | tsconfig.json 666 581 1545535032598 1768 | webpack.config.js 666 1730 1545535032599 1769 | ` -------------------------------------------------------------------------------- /src/__tests__/fallback.spec.js: -------------------------------------------------------------------------------- 1 | import FS from "../index.js"; 2 | 3 | const fs = new FS("fallbackfs", { wipe: true, url: 'http://localhost:9876/base/src/__tests__/__fixtures__/test-folder' }); 4 | 5 | describe("http fallback", () => { 6 | it("sanity check", () => { 7 | expect(fs.promises._backend._http).not.toBeFalsy() 8 | }) 9 | it("loads", (done) => { 10 | fs.promises._activate().then(() => { 11 | done() 12 | }).catch(err => { 13 | expect(err).toBe(null) 14 | done() 15 | }) 16 | }) 17 | describe("readdir", () => { 18 | it("read root dir", done => { 19 | fs.readdir("/", (err, data) => { 20 | expect(err).toBe(null); 21 | expect(data).toEqual(['0', '1', '2', 'a.txt', 'b.txt', 'c.txt']) 22 | done(); 23 | }); 24 | }); 25 | it("read child dir", done => { 26 | fs.readdir("/1", (err, data) => { 27 | expect(err).toBe(null); 28 | expect(data).toEqual(['d.txt', 'e.txt', 'f.txt']) 29 | done(); 30 | }); 31 | }); 32 | }); 33 | 34 | describe("readFile", () => { 35 | it("read non-existant file throws", done => { 36 | fs.readFile("/readFile/non-existant.txt", (err, data) => { 37 | expect(err).not.toBe(null); 38 | done(); 39 | }); 40 | }); 41 | it("read file not in superblock throws", done => { 42 | fs.readFile("/not-in-superblock.txt", (err, data) => { 43 | expect(err).not.toBe(null); 44 | done(); 45 | }); 46 | }); 47 | it("read file /a.txt", done => { 48 | fs.readFile("/a.txt", 'utf8', (err, data) => { 49 | expect(err).toBe(null); 50 | expect(data).toEqual('Hello from "a"'); 51 | done(); 52 | }); 53 | }); 54 | it("read file /1/d.txt", done => { 55 | fs.readFile("/1/d.txt", 'utf8', (err, data) => { 56 | expect(err).toBe(null); 57 | expect(data).toEqual('Hello from "d"'); 58 | done(); 59 | }); 60 | }); 61 | it("make a symlink and read file /2/a.txt through it", done => { 62 | fs.symlink("a.txt", "/2/symlink.txt", (err) => { 63 | expect(err).toBe(null); 64 | fs.readFile("/2/symlink.txt", 'utf8', (err, data) => { 65 | expect(err).toBe(null); 66 | expect(data).toEqual('Hello from "a"'); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | 73 | describe("writeFile", () => { 74 | it("writing a file overwrites the server version", done => { 75 | fs.writeFile("/b.txt", "welcome", (err) => { 76 | expect(err).toBe(null) 77 | fs.readFile("/b.txt", 'utf8', (err, data) => { 78 | expect(err).toBe(null); 79 | expect(data).toEqual('welcome'); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | 86 | describe("unlink", () => { 87 | it("deleting a file should make the file appear deleted", done => { 88 | fs.unlink("/0/a.txt", (err) => { 89 | fs.readFile("/0/a.txt", 'utf8', (err) => { 90 | expect(err).not.toBe(null); 91 | expect(err.code).toEqual('ENOENT'); 92 | fs.readdir("/0", (err, data) => { 93 | expect(err).toBe(null) 94 | expect(data).toEqual(["b.txt", "c.txt"]) 95 | done(); 96 | }); 97 | }); 98 | }); 99 | }); 100 | }); 101 | describe("backFile", () => { 102 | it("backing a nonexistant file throws", done => { 103 | fs.backFile("/backFile/non-existant.txt", (err, data) => { 104 | expect(err).not.toBe(null); 105 | done(); 106 | }); 107 | }); 108 | it("backing a file makes it readable", done => { 109 | fs.backFile("/not-in-superblock.txt", (err, data) => { 110 | expect(err).toBe(null) 111 | fs.readFile("/not-in-superblock.txt", 'utf8', (err, data) => { 112 | expect(err).toBe(null); 113 | expect(data).toEqual('Hello from "not-in-superblock"'); 114 | fs.unlink("/not-in-superblock.txt", (err, data) => { 115 | expect(err).toBe(null); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/__tests__/fs.promises.spec.js: -------------------------------------------------------------------------------- 1 | import FS from "../index.js"; 2 | 3 | const fs = new FS("testfs-promises", { wipe: true }).promises; 4 | 5 | const HELLO = new Uint8Array([72, 69, 76, 76, 79]); 6 | 7 | if (!Promise.prototype.finally) { 8 | Promise.prototype.finally = function (onFinally) { 9 | this.then(onFinally, onFinally); 10 | } 11 | } 12 | 13 | describe("fs.promises module", () => { 14 | describe("mkdir", () => { 15 | it("root directory already exists", (done) => { 16 | fs.mkdir("/").catch(err => { 17 | expect(err).not.toBe(null); 18 | expect(err.code).toEqual("EEXIST"); 19 | done(); 20 | }); 21 | }); 22 | it("create empty directory", done => { 23 | fs.mkdir("/mkdir-test") 24 | .then(() => { 25 | fs.stat("/mkdir-test").then(stat => { 26 | done(); 27 | }); 28 | }) 29 | .catch(err => { 30 | expect(err.code).toEqual("EEXIST"); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe("writeFile", () => { 37 | it("create file", done => { 38 | fs.mkdir("/writeFile").finally(() => { 39 | fs.writeFile("/writeFile/writeFile-uint8.txt", HELLO).then(() => { 40 | fs.stat("/writeFile/writeFile-uint8.txt").then(stats => { 41 | expect(stats.size).toEqual(5); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | }); 47 | it("create file (from string)", done => { 48 | fs.mkdir("/writeFile").finally(() => { 49 | fs.writeFile("/writeFile/writeFile-string.txt", "HELLO").then(() => { 50 | fs.stat("/writeFile/writeFile-string.txt").then(stats => { 51 | expect(stats.size).toEqual(5); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | }); 57 | it("write file perserves old inode", done => { 58 | fs.mkdir("/writeFile").finally(() => { 59 | fs.writeFile("/writeFile/writeFile-inode.txt", "HELLO").then(() => { 60 | fs.stat("/writeFile/writeFile-inode.txt").then(stats => { 61 | let inode = stats.ino; 62 | fs.writeFile("/writeFile/writeFile-inode.txt", "WORLD").then(() => { 63 | fs.stat("/writeFile/writeFile-inode.txt").then(stats => { 64 | expect(stats.ino).toEqual(inode); 65 | done(); 66 | }); 67 | }); 68 | }); 69 | }); 70 | }); 71 | }); 72 | it("write file perserves old mode", done => { 73 | fs.mkdir("/writeFile").finally(() => { 74 | fs.writeFile("/writeFile/writeFile-mode.txt", "HELLO", { mode: 0o635 }).then(() => { 75 | fs.stat("/writeFile/writeFile-mode.txt").then(stats => { 76 | let mode = stats.mode; 77 | expect(mode).toEqual(0o635) 78 | fs.writeFile("/writeFile/writeFile-mode.txt", "WORLD").then(() => { 79 | fs.stat("/writeFile/writeFile-mode.txt").then(stats => { 80 | expect(stats.mode).toEqual(0o635); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | }); 86 | }); 87 | }); 88 | it("write file in place of an existing directory throws", done => { 89 | fs.mkdir("/writeFile").finally(() => { 90 | fs.writeFile("/writeFile", "HELLO") 91 | .then(() => { 92 | fail(); 93 | done(); 94 | }) 95 | .catch(err => { 96 | expect(err).not.toBe(null); 97 | done(); 98 | }); 99 | }); 100 | }); 101 | }); 102 | 103 | describe("readFile", () => { 104 | it("read non-existant file throws", done => { 105 | fs.readFile("/readFile/non-existant.txt").catch(err => { 106 | expect(err).not.toBe(null); 107 | done(); 108 | }); 109 | }); 110 | it("read file", done => { 111 | fs.mkdir("/readFile").finally(() => { 112 | fs.writeFile("/readFile/readFile-uint8.txt", "HELLO").then(() => { 113 | fs.readFile("/readFile/readFile-uint8.txt").then(data => { 114 | // instanceof comparisons on Uint8Array's retrieved from IDB are broken in Safari Mobile 11.x (source: https://github.com/dfahlander/Dexie.js/issues/656#issuecomment-391866600) 115 | expect([...data]).toEqual([...HELLO]); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | }); 121 | it("read file (encoding shorthand)", done => { 122 | fs.mkdir("/readFile").finally(() => { 123 | fs.writeFile("/readFile/readFile-encoding-shorthand.txt", "HELLO").then(() => { 124 | fs.readFile("/readFile/readFile-encoding-shorthand.txt", "utf8").then(data => { 125 | expect(data).toEqual("HELLO"); 126 | done(); 127 | }); 128 | }); 129 | }); 130 | }); 131 | it("read file (encoding longhand)", done => { 132 | fs.mkdir("/readFile").finally(() => { 133 | fs.writeFile("/readFile/readFile-encoding-longhand.txt", "HELLO").then(() => { 134 | fs.readFile("/readFile/readFile-encoding-longhand.txt", { encoding: "utf8" }).then(data => { 135 | expect(data).toEqual("HELLO"); 136 | done(); 137 | }); 138 | }); 139 | }); 140 | }); 141 | }); 142 | 143 | describe("readdir", () => { 144 | it("read non-existant dir returns undefined", done => { 145 | fs.readdir("/readdir/non-existant").catch(err => { 146 | expect(err).not.toBe(null); 147 | done(); 148 | }); 149 | }); 150 | it("read root directory", done => { 151 | fs.mkdir("/readdir").finally(() => { 152 | fs.readdir("/").then(data => { 153 | expect(data.includes("readdir")).toBe(true); 154 | done(); 155 | }); 156 | }); 157 | }); 158 | it("read child directory", done => { 159 | fs.mkdir("/readdir").finally(() => { 160 | fs.writeFile("/readdir/1.txt", "").then(() => { 161 | fs.readdir("/readdir").then(data => { 162 | expect(data).toEqual(["1.txt"]) 163 | done(); 164 | }); 165 | }); 166 | }); 167 | }); 168 | it("read a file throws", done => { 169 | fs.mkdir("/readdir2").finally(() => { 170 | fs.writeFile("/readdir2/not-a-dir", "").then(() => { 171 | fs.readdir("/readdir2/not-a-dir").catch(err => { 172 | expect(err).not.toBe(null); 173 | expect(err.code).toBe('ENOTDIR'); 174 | done(); 175 | }); 176 | }) 177 | }) 178 | }); 179 | }); 180 | 181 | describe("rmdir", () => { 182 | it("delete root directory fails", done => { 183 | fs.rmdir("/").catch(err => { 184 | expect(err).not.toBe(null); 185 | expect(err.code).toEqual("ENOTEMPTY"); 186 | done(); 187 | }); 188 | }); 189 | it("delete non-existant directory fails", done => { 190 | fs.rmdir("/rmdir/non-existant").catch(err => { 191 | expect(err).not.toBe(null); 192 | expect(err.code).toEqual("ENOENT"); 193 | done(); 194 | }); 195 | }); 196 | it("delete non-empty directory fails", done => { 197 | fs.mkdir("/rmdir").finally(() => { 198 | fs.mkdir("/rmdir/not-empty").finally(() => { 199 | fs.writeFile("/rmdir/not-empty/file.txt", "").then(() => { 200 | 201 | fs.rmdir("/rmdir/not-empty").catch(err => { 202 | expect(err).not.toBe(null); 203 | expect(err.code).toEqual("ENOTEMPTY"); 204 | done(); 205 | }); 206 | }) 207 | }) 208 | }) 209 | }); 210 | it("delete empty directory", done => { 211 | fs.mkdir("/rmdir").finally(() => { 212 | fs.mkdir("/rmdir/empty").finally(() => { 213 | fs.readdir("/rmdir").then(data => { 214 | let originalSize = data.length; 215 | fs.rmdir("/rmdir/empty").then(() => { 216 | fs.readdir("/rmdir").then(data => { 217 | expect(data.length === originalSize - 1); 218 | expect(data.includes("empty")).toBe(false); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | }); 224 | }); 225 | }); 226 | it("delete a file throws", done => { 227 | fs.mkdir("/rmdir").finally(() => { 228 | fs.writeFile("/rmdir/not-a-dir", "").then(() => { 229 | fs.rmdir("/rmdir/not-a-dir").catch(err => { 230 | expect(err).not.toBe(null); 231 | expect(err.code).toBe('ENOTDIR'); 232 | done(); 233 | }); 234 | }); 235 | }); 236 | }); 237 | }); 238 | 239 | describe("unlink", () => { 240 | it("create and delete file", done => { 241 | fs.mkdir("/unlink").finally(() => { 242 | fs.writeFile("/unlink/file.txt", "").then(() => { 243 | fs.readdir("/unlink").then(data => { 244 | let originalSize = data.length; 245 | fs.unlink("/unlink/file.txt").then(() => { 246 | fs.readdir("/unlink").then(data => { 247 | expect(data.length).toBe(originalSize - 1) 248 | expect(data.includes("file.txt")).toBe(false); 249 | fs.readFile("/unlink/file.txt").catch(err => { 250 | expect(err).not.toBe(null) 251 | expect(err.code).toBe("ENOENT") 252 | done(); 253 | }); 254 | }); 255 | }); 256 | }); 257 | }); 258 | }); 259 | }); 260 | }); 261 | 262 | describe("rename", () => { 263 | it("create and rename file", done => { 264 | fs.mkdir("/rename").finally(() => { 265 | fs.writeFile("/rename/a.txt", "").then(() => { 266 | fs.rename("/rename/a.txt", "/rename/b.txt").then(() => { 267 | fs.readdir("/rename").then(data => { 268 | expect(data.includes("a.txt")).toBe(false); 269 | expect(data.includes("b.txt")).toBe(true); 270 | fs.readFile("/rename/a.txt").catch(err => { 271 | expect(err).not.toBe(null) 272 | expect(err.code).toBe("ENOENT") 273 | fs.readFile("/rename/b.txt", "utf8").then(data => { 274 | expect(data).toBe("") 275 | done(); 276 | }); 277 | }); 278 | }); 279 | }); 280 | }); 281 | }); 282 | }); 283 | it("create and rename directory", done => { 284 | fs.mkdir("/rename").finally(() => { 285 | fs.mkdir("/rename/a").finally(() => { 286 | fs.writeFile("/rename/a/file.txt", "").then(() => { 287 | fs.rename("/rename/a", "/rename/b").then(() => { 288 | fs.readdir("/rename").then(data => { 289 | expect(data.includes("a")).toBe(false); 290 | expect(data.includes("b")).toBe(true); 291 | fs.readFile("/rename/a/file.txt").catch(err => { 292 | expect(err).not.toBe(null) 293 | expect(err.code).toBe("ENOENT") 294 | fs.readFile("/rename/b/file.txt", "utf8").then(data => { 295 | expect(data).toBe("") 296 | done(); 297 | }); 298 | }); 299 | }); 300 | }); 301 | }); 302 | }); 303 | }); 304 | }); 305 | }); 306 | 307 | describe("symlink", () => { 308 | it("symlink a file and read/write to it", done => { 309 | fs.mkdir("/symlink").finally(() => { 310 | fs.writeFile("/symlink/a.txt", "hello").then(() => { 311 | fs.symlink("/symlink/a.txt", "/symlink/b.txt").then(() => { 312 | fs.readFile("/symlink/b.txt", "utf8").then(data => { 313 | expect(data).toBe("hello") 314 | fs.writeFile("/symlink/b.txt", "world").then(() => { 315 | fs.readFile("/symlink/a.txt", "utf8").then(data => { 316 | expect(data).toBe("world"); 317 | done(); 318 | }) 319 | }) 320 | }); 321 | }); 322 | }); 323 | }); 324 | }); 325 | it("symlink a file and read/write to it (relative)", done => { 326 | fs.mkdir("/symlink").finally(() => { 327 | fs.writeFile("/symlink/a.txt", "hello").then(() => { 328 | fs.symlink("a.txt", "/symlink/b.txt").then(() => { 329 | fs.readFile("/symlink/b.txt", "utf8").then(data => { 330 | expect(data).toBe("hello") 331 | fs.writeFile("/symlink/b.txt", "world").then(() => { 332 | fs.readFile("/symlink/a.txt", "utf8").then(data => { 333 | expect(data).toBe("world"); 334 | done(); 335 | }) 336 | }) 337 | }); 338 | }); 339 | }); 340 | }); 341 | }); 342 | it("symlink a directory and read/write to it", done => { 343 | fs.mkdir("/symlink").finally(() => { 344 | fs.mkdir("/symlink/a").finally(() => { 345 | fs.writeFile("/symlink/a/file.txt", "data").then(() => { 346 | fs.symlink("/symlink/a", "/symlink/b").then(() => { 347 | fs.readdir("/symlink/b").then(data => { 348 | expect(data.includes("file.txt")).toBe(true); 349 | fs.readFile("/symlink/b/file.txt", "utf8").then(data => { 350 | expect(data).toBe("data") 351 | fs.writeFile("/symlink/b/file2.txt", "world").then(() => { 352 | fs.readFile("/symlink/a/file2.txt", "utf8").then(data => { 353 | expect(data).toBe("world"); 354 | done(); 355 | }) 356 | }) 357 | }); 358 | }); 359 | }); 360 | }); 361 | }); 362 | }); 363 | }); 364 | it("symlink a directory and read/write to it (relative)", done => { 365 | fs.mkdir("/symlink").finally(() => { 366 | fs.mkdir("/symlink/a").finally(() => { 367 | fs.mkdir("/symlink/b").finally(() => { 368 | fs.writeFile("/symlink/a/file.txt", "data").then(() => { 369 | fs.symlink("../a", "/symlink/b/c").then(() => { 370 | fs.readdir("/symlink/b/c").then(data => { 371 | expect(data.includes("file.txt")).toBe(true); 372 | fs.readFile("/symlink/b/c/file.txt", "utf8").then(data => { 373 | expect(data).toBe("data") 374 | fs.writeFile("/symlink/b/c/file2.txt", "world").then(() => { 375 | fs.readFile("/symlink/a/file2.txt", "utf8").then(data => { 376 | expect(data).toBe("world"); 377 | done(); 378 | }) 379 | }) 380 | }); 381 | }); 382 | }); 383 | }); 384 | }); 385 | }); 386 | }); 387 | }); 388 | it("unlink doesn't follow symlinks", done => { 389 | fs.mkdir("/symlink").finally(() => { 390 | fs.mkdir("/symlink/del").finally(() => { 391 | fs.writeFile("/symlink/del/file.txt", "data").then(() => { 392 | fs.symlink("/symlink/del/file.txt", "/symlink/del/file2.txt").then(() => { 393 | fs.readdir("/symlink/del").then(data => { 394 | expect(data.includes("file.txt")).toBe(true) 395 | expect(data.includes("file2.txt")).toBe(true) 396 | fs.unlink("/symlink/del/file2.txt").then(data => { 397 | fs.readdir("/symlink/del").then(data => { 398 | expect(data.includes("file.txt")).toBe(true) 399 | expect(data.includes("file2.txt")).toBe(false) 400 | fs.readFile("/symlink/del/file.txt", "utf8").then(data => { 401 | expect(data).toBe("data") 402 | done(); 403 | }) 404 | }); 405 | }); 406 | }); 407 | }); 408 | }); 409 | }); 410 | }); 411 | }); 412 | it("lstat doesn't follow symlinks", done => { 413 | fs.mkdir("/symlink").finally(() => { 414 | fs.mkdir("/symlink/lstat").finally(() => { 415 | fs.writeFile("/symlink/lstat/file.txt", "data").then(() => { 416 | fs.symlink("/symlink/lstat/file.txt", "/symlink/lstat/file2.txt").then(() => { 417 | fs.stat("/symlink/lstat/file2.txt").then(stat => { 418 | expect(stat.isFile()).toBe(true) 419 | expect(stat.isSymbolicLink()).toBe(false) 420 | fs.lstat("/symlink/lstat/file2.txt").then(stat => { 421 | expect(stat.isFile()).toBe(false) 422 | expect(stat.isSymbolicLink()).toBe(true) 423 | done(); 424 | }); 425 | }); 426 | }); 427 | }); 428 | }); 429 | }); 430 | }); 431 | }); 432 | 433 | describe("readlink", () => { 434 | it("readlink returns the target path", done => { 435 | fs.mkdir("/readlink").finally(() => { 436 | fs.writeFile("/readlink/a.txt", "hello").then(() => { 437 | fs.symlink("/readlink/a.txt", "/readlink/b.txt").then(() => { 438 | fs.readlink("/readlink/b.txt", "utf8").then(data => { 439 | expect(data).toBe("/readlink/a.txt") 440 | done(); 441 | }); 442 | }); 443 | }); 444 | }); 445 | }); 446 | it("readlink operates on paths with symlinks", done => { 447 | fs.mkdir("/readlink").finally(() => { 448 | fs.symlink("/readlink", "/readlink/sub").then(() => { 449 | fs.writeFile("/readlink/c.txt", "hello").then(() => { 450 | fs.symlink("/readlink/c.txt", "/readlink/d.txt").then(() => { 451 | fs.readlink("/readlink/sub/d.txt").then(data => { 452 | expect(data).toBe("/readlink/c.txt") 453 | done(); 454 | }); 455 | }); 456 | }); 457 | }); 458 | }); 459 | }); 460 | }); 461 | 462 | describe("du", () => { 463 | it("du returns the total file size of a path", done => { 464 | fs.mkdir("/du").finally(() => { 465 | fs.writeFile("/du/a.txt", "hello").then(() => { 466 | fs.writeFile("/du/b.txt", "hello").then(() => { 467 | fs.mkdir("/du/sub").then(() => { 468 | fs.writeFile("/du/sub/a.txt", "hello").then(() => { 469 | fs.writeFile("/du/sub/b.txt", "hello").then(() => { 470 | fs.du("/du/sub/a.txt").then(size => { 471 | expect(size).toBe(5) 472 | fs.du("/du/sub").then(size => { 473 | expect(size).toBe(10) 474 | fs.du("/du").then(size => { 475 | expect(size).toBe(20) 476 | done(); 477 | }); 478 | }); 479 | }); 480 | }); 481 | }); 482 | }); 483 | }); 484 | }); 485 | }); 486 | }); 487 | }); 488 | 489 | }); 490 | -------------------------------------------------------------------------------- /src/__tests__/fs.spec.js: -------------------------------------------------------------------------------- 1 | import FS from "../index.js"; 2 | 3 | const fs = new FS("testfs", { wipe: true }); 4 | 5 | const HELLO = new Uint8Array([72, 69, 76, 76, 79]); 6 | 7 | describe("fs module", () => { 8 | describe("mkdir", () => { 9 | it("root directory already exists", done => { 10 | fs.mkdir("/", err => { 11 | expect(err).not.toBe(null); 12 | expect(err.code).toEqual("EEXIST"); 13 | done(); 14 | }); 15 | }); 16 | it("create empty directory", done => { 17 | fs.mkdir("/mkdir-test", err => { 18 | if (err) { 19 | expect(err.code).toEqual("EEXIST"); 20 | done(); 21 | } else { 22 | fs.stat("/mkdir-test", (err, stat) => { 23 | expect(err).toEqual(null) 24 | done(); 25 | }); 26 | } 27 | }); 28 | }); 29 | }); 30 | 31 | describe("writeFile", () => { 32 | it("create file", done => { 33 | fs.mkdir("/writeFile", err => { 34 | fs.writeFile("/writeFile/writeFile-uint8.txt", HELLO, err => { 35 | expect(err).toBe(null); 36 | fs.stat("/writeFile/writeFile-uint8.txt", (err, stats) => { 37 | expect(err).toEqual(null) 38 | expect(stats.size).toEqual(5); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | }); 44 | it("create file (from string)", done => { 45 | fs.mkdir("/writeFile", err => { 46 | fs.writeFile("/writeFile/writeFile-string.txt", "HELLO", err => { 47 | expect(err).toBe(null); 48 | fs.stat("/writeFile/writeFile-string.txt", (err, stats) => { 49 | expect(stats.size).toEqual(5); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | }); 55 | it("write file perserves old inode", done => { 56 | fs.mkdir("/writeFile", err => { 57 | fs.writeFile("/writeFile/writeFile-inode.txt", "HELLO", err => { 58 | expect(err).toBe(null); 59 | fs.stat("/writeFile/writeFile-inode.txt", (err, stats) => { 60 | expect(err).toBe(null); 61 | let inode = stats.ino; 62 | fs.writeFile("/writeFile/writeFile-inode.txt", "WORLD", err => { 63 | expect(err).toBe(null); 64 | fs.stat("/writeFile/writeFile-inode.txt", (err, stats) => { 65 | expect(err).toBe(null); 66 | expect(stats.ino).toEqual(inode); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | }); 73 | }); 74 | it("write file in place of an existing directory throws", done => { 75 | fs.mkdir("/writeFile", err => { 76 | fs.writeFile("/writeFile", "HELLO", err => { 77 | expect(err).not.toBe(null); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | describe("readFile", () => { 85 | it("read non-existant file throws", done => { 86 | fs.readFile("/readFile/non-existant.txt", (err, data) => { 87 | expect(err).not.toBe(null); 88 | done(); 89 | }); 90 | }); 91 | it("read file", done => { 92 | fs.mkdir("/readFile", err => { 93 | fs.writeFile("/readFile/readFile-uint8.txt", "HELLO", err => { 94 | expect(err).toBe(null); 95 | fs.readFile("/readFile/readFile-uint8.txt", (err, data) => { 96 | expect(err).toBe(null); 97 | // instanceof comparisons on Uint8Array's retrieved from IDB are broken in Safari Mobile 11.x (source: https://github.com/dfahlander/Dexie.js/issues/656#issuecomment-391866600) 98 | expect([...data]).toEqual([...HELLO]); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | }); 104 | it("read file (encoding shorthand)", done => { 105 | fs.mkdir("/readFile", err => { 106 | fs.writeFile("/readFile/readFile-encoding-shorthand.txt", "HELLO", err => { 107 | expect(err).toBe(null); 108 | fs.readFile("/readFile/readFile-encoding-shorthand.txt", "utf8", (err, data) => { 109 | expect(err).toBe(null); 110 | expect(data).toEqual("HELLO"); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); 116 | it("read file (encoding longhand)", done => { 117 | fs.mkdir("/readFile", err => { 118 | fs.writeFile("/readFile/readFile-encoding-longhand.txt", "HELLO", err => { 119 | expect(err).toBe(null); 120 | fs.readFile("/readFile/readFile-encoding-longhand.txt", { encoding: "utf8" }, (err, data) => { 121 | expect(err).toBe(null); 122 | expect(data).toEqual("HELLO"); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | }); 128 | }); 129 | 130 | describe("readdir", () => { 131 | it("read non-existant dir returns undefined", done => { 132 | fs.readdir("/readdir/non-existant", (err, data) => { 133 | expect(err).not.toBe(null); 134 | done(); 135 | }); 136 | }); 137 | it("read root directory", done => { 138 | fs.mkdir("/readdir", err => { 139 | fs.readdir("/", (err, data) => { 140 | expect(err).toBe(null); 141 | expect(data.includes("readdir")).toBe(true); 142 | done(); 143 | }); 144 | }); 145 | }); 146 | it("read child directory", done => { 147 | fs.mkdir("/readdir", () => { 148 | fs.writeFile("/readdir/1.txt", "", () => { 149 | fs.readdir("/readdir", (err, data) => { 150 | expect(err).toBe(null) 151 | expect(data).toEqual(["1.txt"]) 152 | done(); 153 | }); 154 | }); 155 | }); 156 | }); 157 | }); 158 | 159 | describe("rmdir", () => { 160 | it("delete root directory fails", done => { 161 | fs.rmdir("/", err => { 162 | expect(err).not.toBe(null); 163 | expect(err.code).toEqual("ENOTEMPTY"); 164 | done(); 165 | }); 166 | }); 167 | it("delete non-existant directory fails", done => { 168 | fs.rmdir("/rmdir/non-existant", err => { 169 | expect(err).not.toBe(null); 170 | expect(err.code).toEqual("ENOENT"); 171 | done(); 172 | }); 173 | }); 174 | it("delete non-empty directory fails", done => { 175 | fs.mkdir("/rmdir", () => { 176 | fs.mkdir("/rmdir/not-empty", () => { 177 | fs.writeFile("/rmdir/not-empty/file.txt", "", () => { 178 | 179 | fs.rmdir("/rmdir/not-empty", err => { 180 | expect(err).not.toBe(null); 181 | expect(err.code).toEqual("ENOTEMPTY"); 182 | done(); 183 | }); 184 | }) 185 | }) 186 | }) 187 | }); 188 | it("delete empty directory", done => { 189 | fs.mkdir("/rmdir", () => { 190 | fs.mkdir("/rmdir/empty", () => { 191 | fs.readdir("/rmdir", (err, data) => { 192 | expect(err).toBe(null); 193 | let originalSize = data.length; 194 | fs.rmdir("/rmdir/empty", err => { 195 | expect(err).toBe(null); 196 | fs.readdir("/rmdir", (err, data) => { 197 | expect(err).toBe(null); 198 | expect(data.length === originalSize - 1); 199 | expect(data.includes("empty")).toBe(false); 200 | done(); 201 | }); 202 | }); 203 | }); 204 | }); 205 | }); 206 | }); 207 | }); 208 | 209 | describe("unlink", () => { 210 | it("create and delete file", done => { 211 | fs.mkdir("/unlink", () => { 212 | fs.writeFile("/unlink/file.txt", "", () => { 213 | fs.readdir("/unlink", (err, data) => { 214 | let originalSize = data.length; 215 | fs.unlink("/unlink/file.txt", (err) => { 216 | expect(err).toBe(null); 217 | fs.readdir("/unlink", (err, data) => { 218 | expect(data.length).toBe(originalSize - 1) 219 | expect(data.includes("file.txt")).toBe(false); 220 | fs.readFile("/unlink/file.txt", (err, data) => { 221 | expect(err).not.toBe(null) 222 | expect(err.code).toBe("ENOENT") 223 | done(); 224 | }); 225 | }); 226 | }); 227 | }); 228 | }); 229 | }); 230 | }); 231 | }); 232 | 233 | describe("rename", () => { 234 | it("create and rename file", done => { 235 | fs.mkdir("/rename", () => { 236 | fs.writeFile("/rename/a.txt", "", () => { 237 | fs.rename("/rename/a.txt", "/rename/b.txt", (err) => { 238 | expect(err).toBe(null); 239 | fs.readdir("/rename", (err, data) => { 240 | expect(data.includes("a.txt")).toBe(false); 241 | expect(data.includes("b.txt")).toBe(true); 242 | fs.readFile("/rename/a.txt", (err, data) => { 243 | expect(err).not.toBe(null) 244 | expect(err.code).toBe("ENOENT") 245 | fs.readFile("/rename/b.txt", "utf8", (err, data) => { 246 | expect(err).toBe(null) 247 | expect(data).toBe("") 248 | done(); 249 | }); 250 | }); 251 | }); 252 | }); 253 | }); 254 | }); 255 | }); 256 | it("create and rename directory", done => { 257 | fs.mkdir("/rename", () => { 258 | fs.mkdir("/rename/a", () => { 259 | fs.writeFile("/rename/a/file.txt", "", () => { 260 | fs.rename("/rename/a", "/rename/b", (err) => { 261 | expect(err).toBe(null); 262 | fs.readdir("/rename", (err, data) => { 263 | expect(data.includes("a")).toBe(false); 264 | expect(data.includes("b")).toBe(true); 265 | fs.readFile("/rename/a/file.txt", (err, data) => { 266 | expect(err).not.toBe(null) 267 | expect(err.code).toBe("ENOENT") 268 | fs.readFile("/rename/b/file.txt", "utf8", (err, data) => { 269 | expect(err).toBe(null) 270 | expect(data).toBe("") 271 | done(); 272 | }); 273 | }); 274 | }); 275 | }); 276 | }); 277 | }); 278 | }); 279 | }); 280 | }); 281 | 282 | describe("symlink", () => { 283 | it("symlink a file and read/write to it", done => { 284 | fs.mkdir("/symlink", () => { 285 | fs.writeFile("/symlink/a.txt", "hello", () => { 286 | fs.symlink("/symlink/a.txt", "/symlink/b.txt", () => { 287 | fs.readFile("/symlink/b.txt", "utf8", (err, data) => { 288 | expect(err).toBe(null) 289 | expect(data).toBe("hello") 290 | fs.writeFile("/symlink/b.txt", "world", () => { 291 | fs.readFile("/symlink/a.txt", "utf8", (err, data) => { 292 | expect(err).toBe(null) 293 | expect(data).toBe("world"); 294 | done(); 295 | }) 296 | }) 297 | }); 298 | }); 299 | }); 300 | }); 301 | }); 302 | it("symlink a file and read/write to it (relative)", done => { 303 | fs.mkdir("/symlink", () => { 304 | fs.writeFile("/symlink/a.txt", "hello", () => { 305 | fs.symlink("a.txt", "/symlink/b.txt", () => { 306 | fs.readFile("/symlink/b.txt", "utf8", (err, data) => { 307 | expect(err).toBe(null) 308 | expect(data).toBe("hello") 309 | fs.writeFile("/symlink/b.txt", "world", () => { 310 | fs.readFile("/symlink/a.txt", "utf8", (err, data) => { 311 | expect(err).toBe(null) 312 | expect(data).toBe("world"); 313 | done(); 314 | }) 315 | }) 316 | }); 317 | }); 318 | }); 319 | }); 320 | }); 321 | it("symlink a directory and read/write to it", done => { 322 | fs.mkdir("/symlink", () => { 323 | fs.mkdir("/symlink/a", () => { 324 | fs.writeFile("/symlink/a/file.txt", "data", () => { 325 | fs.symlink("/symlink/a", "/symlink/b", () => { 326 | fs.readdir("/symlink/b", (err, data) => { 327 | expect(err).toBe(null) 328 | expect(data.includes("file.txt")).toBe(true); 329 | fs.readFile("/symlink/b/file.txt", "utf8", (err, data) => { 330 | expect(err).toBe(null) 331 | expect(data).toBe("data") 332 | fs.writeFile("/symlink/b/file2.txt", "world", () => { 333 | fs.readFile("/symlink/a/file2.txt", "utf8", (err, data) => { 334 | expect(err).toBe(null); 335 | expect(data).toBe("world"); 336 | done(); 337 | }) 338 | }) 339 | }); 340 | }); 341 | }); 342 | }); 343 | }); 344 | }); 345 | }); 346 | it("symlink a directory and read/write to it (relative)", done => { 347 | fs.mkdir("/symlink", () => { 348 | fs.mkdir("/symlink/a", () => { 349 | fs.mkdir("/symlink/b", () => { 350 | fs.writeFile("/symlink/a/file.txt", "data", () => { 351 | fs.symlink("../a", "/symlink/b/c", () => { 352 | fs.readdir("/symlink/b/c", (err, data) => { 353 | expect(err).toBe(null) 354 | expect(data.includes("file.txt")).toBe(true); 355 | fs.readFile("/symlink/b/c/file.txt", "utf8", (err, data) => { 356 | expect(err).toBe(null) 357 | expect(data).toBe("data") 358 | fs.writeFile("/symlink/b/c/file2.txt", "world", () => { 359 | fs.readFile("/symlink/a/file2.txt", "utf8", (err, data) => { 360 | expect(err).toBe(null); 361 | expect(data).toBe("world"); 362 | done(); 363 | }) 364 | }) 365 | }); 366 | }); 367 | }); 368 | }); 369 | }); 370 | }); 371 | }); 372 | }); 373 | it("unlink doesn't follow symlinks", done => { 374 | fs.mkdir("/symlink", () => { 375 | fs.mkdir("/symlink/del", () => { 376 | fs.writeFile("/symlink/del/file.txt", "data", () => { 377 | fs.symlink("/symlink/del/file.txt", "/symlink/del/file2.txt", () => { 378 | fs.readdir("/symlink/del", (err, data) => { 379 | expect(err).toBe(null) 380 | expect(data.includes("file.txt")).toBe(true) 381 | expect(data.includes("file2.txt")).toBe(true) 382 | fs.unlink("/symlink/del/file2.txt", (err, data) => { 383 | expect(err).toBe(null) 384 | fs.readdir("/symlink/del", (err, data) => { 385 | expect(err).toBe(null) 386 | expect(data.includes("file.txt")).toBe(true) 387 | expect(data.includes("file2.txt")).toBe(false) 388 | fs.readFile("/symlink/del/file.txt", "utf8", (err, data) => { 389 | expect(err).toBe(null) 390 | expect(data).toBe("data") 391 | done(); 392 | }) 393 | }); 394 | }); 395 | }); 396 | }); 397 | }); 398 | }); 399 | }); 400 | }); 401 | it("lstat for symlink creates correct mode", done => { 402 | fs.mkdir("/symlink", () => { 403 | fs.writeFile("/symlink/a.txt", "hello", () => { 404 | fs.symlink("/symlink/a.txt", "/symlink/b.txt", () => { 405 | fs.lstat("/symlink/b.txt", (err, stat) => { 406 | expect(err).toBe(null) 407 | expect(stat.mode).toBe(0o120000) 408 | done(); 409 | }); 410 | }); 411 | }); 412 | }); 413 | }); 414 | it("lstat doesn't follow symlinks", done => { 415 | fs.mkdir("/symlink", () => { 416 | fs.mkdir("/symlink/lstat", () => { 417 | fs.writeFile("/symlink/lstat/file.txt", "data", () => { 418 | fs.symlink("/symlink/lstat/file.txt", "/symlink/lstat/file2.txt", () => { 419 | fs.stat("/symlink/lstat/file2.txt", (err, stat) => { 420 | expect(err).toBe(null) 421 | expect(stat.isFile()).toBe(true) 422 | expect(stat.isSymbolicLink()).toBe(false) 423 | fs.lstat("/symlink/lstat/file2.txt", (err, stat) => { 424 | expect(err).toBe(null) 425 | expect(stat.isFile()).toBe(false) 426 | expect(stat.isSymbolicLink()).toBe(true) 427 | done(); 428 | }); 429 | }); 430 | }); 431 | }); 432 | }); 433 | }); 434 | }); 435 | }); 436 | 437 | describe("readlink", () => { 438 | it("readlink returns the target path", done => { 439 | fs.mkdir("/readlink", () => { 440 | fs.writeFile("/readlink/a.txt", "hello", () => { 441 | fs.symlink("/readlink/a.txt", "/readlink/b.txt", () => { 442 | fs.readlink("/readlink/b.txt", "utf8", (err, data) => { 443 | expect(err).toBe(null) 444 | expect(data).toBe("/readlink/a.txt") 445 | done(); 446 | }); 447 | }); 448 | }); 449 | }); 450 | }); 451 | it("readlink operates on paths with symlinks", done => { 452 | fs.mkdir("/readlink", () => { 453 | fs.symlink("/readlink", "/readlink/sub", () => { 454 | fs.writeFile("/readlink/c.txt", "hello", () => { 455 | fs.symlink("/readlink/c.txt", "/readlink/d.txt", () => { 456 | fs.readlink("/readlink/sub/d.txt", (err, data) => { 457 | expect(err).toBe(null) 458 | expect(data).toBe("/readlink/c.txt") 459 | done(); 460 | }); 461 | }); 462 | }); 463 | }); 464 | }); 465 | }); 466 | }); 467 | 468 | describe("du", () => { 469 | it("du returns the total file size of a path", done => { 470 | fs.mkdir("/du", () => { 471 | fs.writeFile("/du/a.txt", "hello", () => { 472 | fs.writeFile("/du/b.txt", "hello", () => { 473 | fs.mkdir("/du/sub", () => { 474 | fs.writeFile("/du/sub/a.txt", "hello", () => { 475 | fs.writeFile("/du/sub/b.txt", "hello", () => { 476 | fs.du("/du/sub/a.txt", (err, size) => { 477 | expect(err).toBe(null) 478 | expect(size).toBe(5) 479 | fs.du("/du/sub", (err, size) => { 480 | expect(err).toBe(null) 481 | expect(size).toBe(10) 482 | fs.du("/du", (err, size) => { 483 | expect(err).toBe(null) 484 | expect(size).toBe(20) 485 | done(); 486 | }); 487 | }); 488 | }); 489 | }); 490 | }); 491 | }); 492 | }); 493 | }); 494 | }); 495 | }); 496 | }); 497 | 498 | }); 499 | -------------------------------------------------------------------------------- /src/__tests__/hotswap-backends-1.spec.js: -------------------------------------------------------------------------------- 1 | import FS from "../index.js"; 2 | 3 | const fs = new FS(); 4 | const pfs = fs.promises; 5 | 6 | describe("hotswap backends", () => { 7 | 8 | it("re-init with new backend", async () => { 9 | // write a file 10 | fs.init("testfs-1", { wipe: true }) 11 | await pfs.writeFile('/a.txt', 'HELLO'); 12 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO'); 13 | 14 | // we swap backends. file is gone 15 | fs.init('testfs-2', { wipe: true }) 16 | let err = null 17 | try { 18 | await pfs.readFile('/a.txt', 'utf8') 19 | } catch (e) { 20 | err = e 21 | } 22 | expect(err).not.toBeNull(); 23 | expect(err.code).toBe('ENOENT'); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /src/__tests__/hotswap-backends-2.spec.js: -------------------------------------------------------------------------------- 1 | import FS from "../index.js"; 2 | 3 | const fs = new FS(); 4 | const pfs = fs.promises; 5 | 6 | describe("hotswap backends", () => { 7 | 8 | it("swap back and forth between two backends", async () => { 9 | // write a file to backend A 10 | fs.init('testfs-A', { wipe: true }); 11 | await pfs.writeFile('/a.txt', 'HELLO'); 12 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO'); 13 | 14 | // write a file to backend B 15 | fs.init('testfs-B', { wipe: true }) 16 | await pfs.writeFile('/a.txt', 'WORLD'); 17 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('WORLD'); 18 | 19 | // read a file from backend A 20 | fs.init('testfs-A'); 21 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO'); 22 | 23 | // read a file from backend B 24 | fs.init('testfs-B'); 25 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('WORLD'); 26 | 27 | // read a file from backend A 28 | fs.init('testfs-A'); 29 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO'); 30 | 31 | // read a file from backend B 32 | fs.init('testfs-B'); 33 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('WORLD'); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /src/__tests__/hotswap-backends-3.spec.js: -------------------------------------------------------------------------------- 1 | import FS from "../index.js"; 2 | 3 | const fs = new FS(); 4 | const pfs = fs.promises; 5 | 6 | describe("hotswap backends", () => { 7 | 8 | it("a custom backend", async () => { 9 | // we started with a default backend. 10 | fs.init('testfs-default', { wipe: true }) 11 | await pfs.writeFile('/a.txt', 'HELLO'); 12 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO'); 13 | 14 | // we swap backends. 15 | let ranInit = false; 16 | let ranDestroy = false; 17 | fs.init('testfs-custom', { 18 | backend: { 19 | init() { ranInit = true }, 20 | readFile() { return 'dummy' }, 21 | destroy() { ranDestroy = true }, 22 | } 23 | }); 24 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('dummy'); 25 | expect(ranInit).toBe(true); 26 | expect(ranDestroy).toBe(false); 27 | 28 | // we swap back 29 | fs.init('testfs-default'); 30 | expect(await pfs.readFile('/a.txt', 'utf8')).toBe('HELLO'); 31 | expect(ranDestroy).toBe(true); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/hotswap-backends-4.spec.js: -------------------------------------------------------------------------------- 1 | import FS from "../index.js"; 2 | 3 | const fs = new FS(); 4 | const pfs = fs.promises; 5 | 6 | // IDK it's broke. It's time to rewrite LightningFS basically. 7 | 8 | xdescribe("hotswap backends", () => { 9 | 10 | it("graceful transition", async () => { 11 | const N = 1 12 | class MockBackend { 13 | constructor() { 14 | this.count = 0 15 | this.writeFile = this.writeFile.bind(this); 16 | } 17 | async writeFile () { 18 | await new Promise(r => setTimeout(r, 100 * Math.random())) 19 | this.count++ 20 | } 21 | async readFile () { 22 | return 'foo' 23 | } 24 | } 25 | 26 | const b1 = new MockBackend(); 27 | const b2 = new MockBackend(); 28 | 29 | // write N files to mock backend 1 30 | await fs.init('testfs-custom-1', { backend: b1, defer: true }); 31 | for (let i = 0; i < N; i++) { 32 | // we don't await 33 | pfs.writeFile('hello', 'foo'); 34 | } 35 | 36 | // swap backends without waiting 37 | await fs.init('testfs-custom-2', { backend: b2, defer: true }); 38 | expect(pfs._operations.size).toBe(N); 39 | 40 | // write N files to mock backend 2 41 | for (let i = 0; i < N; i++) { 42 | // we don't await 43 | pfs.writeFile('hello', 'foo'); 44 | } 45 | 46 | // swap backend back without waiting 47 | fs.init('testfs-custom-1', { backend: b1, defer: true }); 48 | expect(pfs._operations.size).toBe(N); 49 | 50 | // but now we have to wait. because we're dumb and the hotswapping isn't perfect 51 | await new Promise(r => setTimeout(r, 250)); 52 | expect(pfs._operations.size).toBe(0); 53 | 54 | // everything should be synced now 55 | expect(b1.count).toBe(N) 56 | expect(b2.count).toBe(N) 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /src/__tests__/path.spec.js: -------------------------------------------------------------------------------- 1 | const path = require("../path.js"); 2 | 3 | describe("path module", () => { 4 | describe("path.split", () => { 5 | it("should handle absolute paths", () => { 6 | expect(path.split("/hello/world.txt")).toEqual([ 7 | "/", 8 | "hello", 9 | "world.txt" 10 | ]); 11 | }); 12 | it("should handle relative paths", () => { 13 | expect(path.split("./hello/world.txt")).toEqual([ 14 | ".", 15 | "hello", 16 | "world.txt" 17 | ]); 18 | }); 19 | }); 20 | describe("path.join", () => { 21 | it("should join two partial paths", () => { 22 | expect(path.join("hello/world", "test.txt")).toEqual( 23 | "hello/world/test.txt" 24 | ); 25 | expect(path.join("hello", "world", "test.txt")).toEqual( 26 | "hello/world/test.txt" 27 | ); 28 | }); 29 | it("should join a relative path and a partial path", () => { 30 | expect(path.join("./hello/world", "test.txt")).toEqual( 31 | "./hello/world/test.txt" 32 | ); 33 | expect(path.join("./hello", "world", "test.txt")).toEqual( 34 | "./hello/world/test.txt" 35 | ); 36 | }); 37 | }); 38 | describe("path.normalize", () => { 39 | it("should return normal paths unchanged", () => { 40 | expect(path.normalize("./hello/world.txt")).toEqual("./hello/world.txt"); 41 | expect(path.normalize("/hello/world.txt")).toEqual("/hello/world.txt"); 42 | }); 43 | it("should normalize relative paths to start with ./", () => { 44 | expect(path.normalize("hello/world.txt")).toEqual("./hello/world.txt"); 45 | }); 46 | it("should normalize paths with multiple '.' to a single '.'", () => { 47 | expect(path.normalize("./././hello/./world.txt")).toEqual( 48 | "./hello/world.txt" 49 | ); 50 | }); 51 | it("should normalize paths with '..'", () => { 52 | expect(path.normalize("./hello/../world.txt")).toEqual("./world.txt"); 53 | expect(path.normalize("/hello/../world.txt")).toEqual("/world.txt"); 54 | }); 55 | it("should normalize relative paths above '.'", () => { 56 | expect(path.normalize("./hello/../../world.txt")).toEqual( 57 | "./../world.txt" 58 | ); 59 | }); 60 | it("should refust to normalize absolute paths above '/'", () => { 61 | expect(() => path.normalize("/hello/../../world.txt")).toThrow(); 62 | }); 63 | }); 64 | 65 | describe("path.dirname", () => { 66 | it("should return parent directory of file", () => { 67 | expect(path.dirname("./hello/world.txt")).toEqual("./hello"); 68 | expect(path.dirname("/hello/world.txt")).toEqual("/hello"); 69 | expect(path.dirname("/hello.txt")).toEqual("/"); 70 | }); 71 | 72 | it("should return parent directory of directories", () => { 73 | expect(path.dirname("./hello/world")).toEqual("./hello"); 74 | expect(path.dirname("/hello/world")).toEqual("/hello"); 75 | expect(path.dirname("/hello")).toEqual("/"); 76 | }); 77 | 78 | it("should throw for ambiguous cases", () => { 79 | expect(() => path.dirname("world.txt")).toThrow(); 80 | }); 81 | }); 82 | 83 | describe("path.basename", () => { 84 | it("should return basename of file", () => { 85 | expect(path.basename("./hello/world.txt")).toEqual("world.txt"); 86 | expect(path.basename("/hello/world.txt")).toEqual("world.txt"); 87 | expect(path.basename("/hello.txt")).toEqual("hello.txt"); 88 | }); 89 | 90 | it("should return basename of directories", () => { 91 | expect(path.basename("./hello/world")).toEqual("world"); 92 | expect(path.basename("/hello/world")).toEqual("world"); 93 | expect(path.basename("/hello")).toEqual("hello"); 94 | }); 95 | 96 | it("should throw for ambiguous cases", () => { 97 | expect(() => path.basename("/")).toThrow(); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/__tests__/threadsafety.spec.js: -------------------------------------------------------------------------------- 1 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000 2 | import FS from "../index.js"; 3 | 4 | const fs = new FS("testfs-worker", { wipe: true }).promises; 5 | 6 | describe("thread safety", () => { 7 | it("launch a bunch of workers", (done) => { 8 | let workers = [] 9 | let promises = [] 10 | let numWorkers = 5 11 | fs.readdir('/').then(files => { 12 | expect(files.length).toBe(0); 13 | for (let i = 1; i <= numWorkers; i++) { 14 | let promise = new Promise(resolve => { 15 | let worker = new Worker('http://localhost:9876/base/src/__tests__/threadsafety.worker.js', {name: `worker_${i}`}) 16 | worker.onmessage = (e) => { 17 | if (e.data && e.data.message === 'COMPLETE') resolve() 18 | } 19 | workers.push(worker) 20 | }) 21 | promises.push(promise) 22 | } 23 | Promise.all(promises).then(() => { 24 | fs.readdir('/').then(files => { 25 | expect(files.length).toBe(5 * numWorkers) 26 | done(); 27 | }); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/threadsafety.worker.js: -------------------------------------------------------------------------------- 1 | importScripts('http://localhost:9876/base/dist/lightning-fs.min.js'); 2 | 3 | self.fs = new LightningFS("testfs-worker").promises; 4 | 5 | const sleep = ms => new Promise(r => setTimeout(r, ms)) 6 | 7 | const whoAmI = (typeof window === 'undefined' ? (self.name ? self.name : 'worker') : 'main' )+ ': ' 8 | 9 | async function writeFiles () { 10 | console.log(whoAmI + 'write stuff') 11 | // Chrome Mobile 67 and Mobile Safari 11 do not yet support named Workers 12 | let name = self.name || Math.random() 13 | await Promise.all([0, 1, 2, 3, 4].map(i => self.fs.writeFile(`/${name}_${i}.txt`, String(i)))) 14 | self.postMessage({ message: 'COMPLETE' }) 15 | } 16 | 17 | writeFiles() 18 | -------------------------------------------------------------------------------- /src/clock.js: -------------------------------------------------------------------------------- 1 | const where = typeof window === 'undefined' ? 'worker' : 'main' 2 | 3 | module.exports = function clock(name) { 4 | performance.mark(`${name} start`); 5 | console.log(`${where}: ${name}`) 6 | console.time(`${where}: ${name}`) 7 | return function stopClock() { 8 | performance.mark(`${name} end`); 9 | console.timeEnd(`${where}: ${name}`) 10 | performance.measure(`${name}`, `${name} start`, `${name} end`); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | function Err(name) { 2 | return class extends Error { 3 | constructor(...args) { 4 | super(...args); 5 | this.code = name; 6 | if (this.message) { 7 | this.message = name + ": " + this.message; 8 | } else { 9 | this.message = name; 10 | } 11 | } 12 | }; 13 | } 14 | 15 | const EEXIST = Err("EEXIST"); 16 | const ENOENT = Err("ENOENT"); 17 | const ENOTDIR = Err("ENOTDIR"); 18 | const ENOTEMPTY = Err("ENOTEMPTY"); 19 | const ETIMEDOUT = Err("ETIMEDOUT"); 20 | const EISDIR = Err("EISDIR"); 21 | 22 | module.exports = { EEXIST, ENOENT, ENOTDIR, ENOTEMPTY, ETIMEDOUT, EISDIR }; 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const once = require("just-once"); 2 | 3 | const PromisifiedFS = require('./PromisifiedFS'); 4 | 5 | function wrapCallback (opts, cb) { 6 | if (typeof opts === "function") { 7 | cb = opts; 8 | } 9 | cb = once(cb); 10 | const resolve = (...args) => cb(null, ...args) 11 | return [resolve, cb]; 12 | } 13 | 14 | module.exports = class FS { 15 | constructor(...args) { 16 | this.promises = new PromisifiedFS(...args) 17 | // Needed so things don't break if you destructure fs and pass individual functions around 18 | this.init = this.init.bind(this) 19 | this.readFile = this.readFile.bind(this) 20 | this.writeFile = this.writeFile.bind(this) 21 | this.unlink = this.unlink.bind(this) 22 | this.readdir = this.readdir.bind(this) 23 | this.mkdir = this.mkdir.bind(this) 24 | this.rmdir = this.rmdir.bind(this) 25 | this.rename = this.rename.bind(this) 26 | this.stat = this.stat.bind(this) 27 | this.lstat = this.lstat.bind(this) 28 | this.readlink = this.readlink.bind(this) 29 | this.symlink = this.symlink.bind(this) 30 | this.backFile = this.backFile.bind(this) 31 | this.du = this.du.bind(this) 32 | this.flush = this.flush.bind(this) 33 | } 34 | init(name, options) { 35 | return this.promises.init(name, options) 36 | } 37 | readFile(filepath, opts, cb) { 38 | const [resolve, reject] = wrapCallback(opts, cb); 39 | this.promises.readFile(filepath, opts).then(resolve).catch(reject) 40 | } 41 | writeFile(filepath, data, opts, cb) { 42 | const [resolve, reject] = wrapCallback(opts, cb); 43 | this.promises.writeFile(filepath, data, opts).then(resolve).catch(reject); 44 | } 45 | unlink(filepath, opts, cb) { 46 | const [resolve, reject] = wrapCallback(opts, cb); 47 | this.promises.unlink(filepath, opts).then(resolve).catch(reject); 48 | } 49 | readdir(filepath, opts, cb) { 50 | const [resolve, reject] = wrapCallback(opts, cb); 51 | this.promises.readdir(filepath, opts).then(resolve).catch(reject); 52 | } 53 | mkdir(filepath, opts, cb) { 54 | const [resolve, reject] = wrapCallback(opts, cb); 55 | this.promises.mkdir(filepath, opts).then(resolve).catch(reject) 56 | } 57 | rmdir(filepath, opts, cb) { 58 | const [resolve, reject] = wrapCallback(opts, cb); 59 | this.promises.rmdir(filepath, opts).then(resolve).catch(reject) 60 | } 61 | rename(oldFilepath, newFilepath, cb) { 62 | const [resolve, reject] = wrapCallback(cb); 63 | this.promises.rename(oldFilepath, newFilepath).then(resolve).catch(reject) 64 | } 65 | stat(filepath, opts, cb) { 66 | const [resolve, reject] = wrapCallback(opts, cb); 67 | this.promises.stat(filepath).then(resolve).catch(reject); 68 | } 69 | lstat(filepath, opts, cb) { 70 | const [resolve, reject] = wrapCallback(opts, cb); 71 | this.promises.lstat(filepath).then(resolve).catch(reject); 72 | } 73 | readlink(filepath, opts, cb) { 74 | const [resolve, reject] = wrapCallback(opts, cb); 75 | this.promises.readlink(filepath).then(resolve).catch(reject); 76 | } 77 | symlink(target, filepath, cb) { 78 | const [resolve, reject] = wrapCallback(cb); 79 | this.promises.symlink(target, filepath).then(resolve).catch(reject); 80 | } 81 | backFile(filepath, opts, cb) { 82 | const [resolve, reject] = wrapCallback(opts, cb); 83 | this.promises.backFile(filepath, opts).then(resolve).catch(reject); 84 | } 85 | du(filepath, cb) { 86 | const [resolve, reject] = wrapCallback(cb); 87 | this.promises.du(filepath).then(resolve).catch(reject); 88 | } 89 | flush(cb) { 90 | const [resolve, reject] = wrapCallback(cb); 91 | this.promises.flush().then(resolve).catch(reject); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/path.js: -------------------------------------------------------------------------------- 1 | function normalizePath(path) { 2 | if (path.length === 0) { 3 | return "."; 4 | } 5 | let parts = splitPath(path); 6 | parts = parts.reduce(reducer, []); 7 | return joinPath(...parts); 8 | } 9 | 10 | function resolvePath(...paths) { 11 | let result = ''; 12 | for (let path of paths) { 13 | if (path.startsWith('/')) { 14 | result = path; 15 | } else { 16 | result = normalizePath(joinPath(result, path)); 17 | } 18 | } 19 | return result; 20 | } 21 | 22 | function joinPath(...parts) { 23 | if (parts.length === 0) return ""; 24 | let path = parts.join("/"); 25 | // Replace consecutive '/' 26 | path = path.replace(/\/{2,}/g, "/"); 27 | return path; 28 | } 29 | 30 | function splitPath(path) { 31 | if (path.length === 0) return []; 32 | if (path === "/") return ["/"]; 33 | let parts = path.split("/"); 34 | if (parts[parts.length - 1] === '') { 35 | parts.pop(); 36 | } 37 | if (path[0] === "/") { 38 | // assert(parts[0] === '') 39 | parts[0] = "/"; 40 | } else { 41 | if (parts[0] !== ".") { 42 | parts.unshift("."); 43 | } 44 | } 45 | return parts; 46 | } 47 | 48 | function dirname(path) { 49 | const last = path.lastIndexOf("/"); 50 | if (last === -1) throw new Error(`Cannot get dirname of "${path}"`); 51 | if (last === 0) return "/"; 52 | return path.slice(0, last); 53 | } 54 | 55 | function basename(path) { 56 | if (path === "/") throw new Error(`Cannot get basename of "${path}"`); 57 | const last = path.lastIndexOf("/"); 58 | if (last === -1) return path; 59 | return path.slice(last + 1); 60 | } 61 | 62 | function reducer(ancestors, current) { 63 | // Initial condition 64 | if (ancestors.length === 0) { 65 | ancestors.push(current); 66 | return ancestors; 67 | } 68 | // assert(ancestors.length > 0) 69 | // assert(ancestors[0] === '.' || ancestors[0] === '/') 70 | 71 | // Collapse '.' references 72 | if (current === ".") return ancestors; 73 | 74 | // Collapse '..' references 75 | if (current === "..") { 76 | if (ancestors.length === 1) { 77 | if (ancestors[0] === "/") { 78 | throw new Error("Unable to normalize path - traverses above root directory"); 79 | } 80 | // assert(ancestors[0] === '.') 81 | if (ancestors[0] === ".") { 82 | ancestors.push(current); 83 | return ancestors; 84 | } 85 | } 86 | // assert(ancestors.length > 1) 87 | if (ancestors[ancestors.length - 1] === "..") { 88 | ancestors.push(".."); 89 | return ancestors; 90 | } else { 91 | ancestors.pop(); 92 | return ancestors; 93 | } 94 | } 95 | 96 | ancestors.push(current); 97 | return ancestors; 98 | } 99 | 100 | module.exports = { 101 | join: joinPath, 102 | normalize: normalizePath, 103 | split: splitPath, 104 | basename, 105 | dirname, 106 | resolve: resolvePath, 107 | }; 108 | -------------------------------------------------------------------------------- /src/superblocktxt.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | var fs = require('fs') 3 | var path = require('path') 4 | var symLinks = {} 5 | 6 | module.exports = (dirpath) => { 7 | let str = ""; 8 | const printTree = (root, indent) => { 9 | let files = fs.readdirSync(root) 10 | for (let file of files) { 11 | // Ignore itself 12 | if (file === '.superblock.txt') continue 13 | 14 | let fpath = `${root}/${file}` 15 | let lstat = fs.lstatSync(fpath) 16 | // Avoid infinite loops. 17 | if (lstat.isSymbolicLink()) { 18 | if (!symLinks[lstat.dev]) { 19 | symLinks[lstat.dev] = {} 20 | } 21 | // Skip this entry if we've seen it before 22 | if (symLinks[lstat.dev][lstat.ino]) { 23 | continue 24 | } 25 | symLinks[lstat.dev][lstat.ino] = true 26 | } 27 | let mode = lstat.mode.toString(8); 28 | str += `${"\t".repeat(indent)}` 29 | if (lstat.isDirectory()) { 30 | str += `${file}\t${mode}\n`; 31 | printTree(fpath, indent + 1); 32 | } else { 33 | str += `${file}\t${mode}\t${lstat.size}\t${lstat.mtimeMs}\n`; 34 | } 35 | } 36 | }; 37 | printTree(dirpath, 0); 38 | return str; 39 | } 40 | 41 | if (!module.parent) { 42 | let filepath = process.cwd() + '/.superblock.txt' 43 | let contents = module.exports(process.cwd()) 44 | fs.writeFileSync(filepath, contents) 45 | } 46 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | target: "webworker", 5 | mode: 'production', 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "lightning-fs.min.js", 9 | library: "LightningFS", 10 | libraryTarget: "umd", 11 | }, 12 | }; 13 | --------------------------------------------------------------------------------