├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── npm-publish.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── babel.config.js ├── index.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── shiftleft.yml ├── src ├── filesystem │ ├── fileinfo.js │ ├── filesystem.js │ ├── flags.js │ ├── mode.js │ ├── node.js │ ├── path.js │ └── stats.js ├── index.js └── storages │ ├── base.js │ ├── indexeddb.js │ └── memory.js ├── test └── unit │ ├── .eslintrc │ ├── jest.conf.js │ ├── setup.js │ └── specs │ ├── fileinfo.spec.js │ ├── filesystem.spec.js │ ├── path.spec.js │ └── stats.spec.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 13 | 'standard' 14 | ], 15 | // required to lint *.vue files 16 | plugins: [ 17 | "flowtype-errors" 18 | ], 19 | // add your custom rules here 20 | rules: { 21 | // allow async-await 22 | 'generator-star-spacing': 'off', 23 | "flowtype-errors/show-errors": 2, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [include] 2 | ./src 3 | 4 | [ignore] 5 | .*/build/.* 6 | 7 | [libs] 8 | ./libs 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} 34 | 35 | publish-gpr: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: 16 43 | registry-url: https://npm.pkg.github.com/ 44 | - run: npm ci 45 | - run: npm publish 46 | env: 47 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | tmp/* 3 | dist/* 4 | coverage/* 5 | test/unit/coverage/* 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: 3 | - npm install -g codecov 4 | - npm ci 5 | script: 6 | - npm test 7 | - codecov 8 | node_js: 9 | - 10 10 | - 12 11 | - 14 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors 2 | ================== 3 | 4 | 1. Michal Ostrowski 5 | 2. Deepak Thukral 6 | 7 | CONTRIBUTORS 8 | ================== 9 | 1. Vadim Yuldashbaev 10 | 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at deepak.thukral@fagbokforlaget.no. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Fagbokforlaget V&B AS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleFS 2 | [![view on npm](https://img.shields.io/npm/v/@forlagshuset/simple-fs.svg)](https://www.npmjs.com/package/@forlagshuset/simple-fs) 3 | [![npm module downloads](http://img.shields.io/npm/dt/@forlagshuset/simple-fs.svg)](https://www.npmjs.org/package/@forlagshuset/simple-fs) 4 | [![Dependency Status](https://david-dm.org/fagbokforlaget/simple-fs.svg)](https://david-dm.org/fagbokforlaget/simple-fs) 5 | [![Known Vulnerabilities](https://snyk.io/test/github/fagbokforlaget/simple-fs/badge.svg?targetFile=package.json)](https://snyk.io/test/github/fagbokforlaget/simple-fs?targetFile=package.json) 6 | [![Build Status](https://travis-ci.org/fagbokforlaget/simple-fs.svg?branch=master)](https://travis-ci.org/fagbokforlaget/simple-fs) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 8 | 9 | A minimal, extensible and promise based filesystem layer for modern browsers. 10 | 11 | [Live Demo](https://codepen.io/iapain/full/MxLNeg) 12 | 13 | ## Supported storage backend 14 | Simple-fs provides two storage backend. It's possible to write your own stoage backend using [Storage API](https://github.com/fagbokforlaget/simple-fs/blob/master/src/storages/base.js) 15 | 16 | * IndexedDB (default) 17 | ``` 18 | import { IndexedDbStorage } from '@forlagshuset/simple-fs' 19 | ``` 20 | * Memory (experimental and used for testing) 21 | ``` 22 | import { MemoryStorage } from '@forlagshuset/simple-fs' 23 | ``` 24 | 25 | ## Installation 26 | 27 | npm: 28 | ``` 29 | npm install --save @forlagshuset/simple-fs 30 | ``` 31 | 32 | ## Usage 33 | browser (umd): 34 | ```html 35 | 36 | 51 | ``` 52 | 53 | 54 | browser (modules) 55 | ```javascript 56 | import SimpleFS from '@forlagshuset/simple-fs' 57 | // OR es6 modules from unpkg 58 | import SimpleFS from "//unpkg.com/@forlagshuset/simple-fs?module" 59 | 60 | const fs = new SimpleFS.FileSystem() 61 | 62 | // first create root folder 63 | await fs.mkdir('/myproject') 64 | 65 | // create a file under root folder 66 | const content = new Blob(['This is my cool project'], {type: 'plain/text'}) 67 | await fs.writeFile('/myproject/test.txt', content) 68 | 69 | // get content as blob 70 | let blob = await fs.readFile('/myproject/test.txt') 71 | ``` 72 | 73 | ## API 74 | 75 | FileSystem 76 | ```javascript 77 | constructor({storage: storageObj = new IndexedDbStorage('my-storage-name')}) 78 | mkdir(path: string) 79 | mkdirParents(path: string) // wraps mkdir -p 80 | rmdir(path: string) 81 | rmdirRecursive(path: string) // removes dirs recursively 82 | readFile(path: string, options={}) // returns Blob 83 | writeFile(path: string, data: Blob, options={}) // data should be Blob type 84 | outputFile(path: string, data: Blob, options={}) // Wraps writeFile and recursively creates path if not exists 85 | bulkOutputFiles([{path: string, blob: Blob, options:{}]) // Output files in one transaction, speeds up in chrome 86 | unlink(path: string) 87 | exists(path: string) 88 | stats(path: string) 89 | ``` 90 | 91 | ## Browser support 92 | 93 | * Chrome 94 | * IE Edge 95 | * Firefox 96 | * Safari 97 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/env', 4 | '@babel/flow' 5 | ], 6 | plugins: [ 7 | '@babel/plugin-transform-runtime' 8 | ], 9 | env: { 10 | mjs: { 11 | presets: [ 12 | ['@babel/env', { modules: false}], 13 | ] 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | API test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@forlagshuset/simple-fs", 3 | "version": "0.5.2", 4 | "description": "File system in indexeddb", 5 | "main": "./dist/SimpleFS", 6 | "module": "./dist/SimpleFS.mjs", 7 | "scripts": { 8 | "build:umd": "webpack --env=production", 9 | "build:mjs": "rollup -c rollup.config.js", 10 | "build": "npm run build:umd && npm run build:mjs", 11 | "dev": "webpack --progress --colors --watch --mode=development", 12 | "unit": "jest --config test/unit/jest.conf.js", 13 | "test": "npm run unit", 14 | "test:watch": "mocha --require @babel/register --require @babel/plugin-transform-runtime --colors -w ./test/*.spec.js", 15 | "clean": "rimraf dist coverage", 16 | "prepare": "npm run clean && npm run test && npm run build" 17 | }, 18 | "devDependencies": { 19 | "@babel/cli": "^7.13.10", 20 | "@babel/core": "^7.13.10", 21 | "@babel/plugin-proposal-class-properties": "^7.13.0", 22 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 23 | "@babel/plugin-transform-runtime": "^7.13.10", 24 | "@babel/preset-env": "^7.13.10", 25 | "@babel/preset-flow": "^7.12.13", 26 | "@babel/preset-react": "^7.12.13", 27 | "@babel/register": "^7.13.8", 28 | "@babel/runtime": "^7.13.10", 29 | "babel-core": "^7.0.0-bridge.0", 30 | "babel-loader": "^8.2.2", 31 | "eslint": "^7.22.0", 32 | "eslint-config-standard": "^16.0.2", 33 | "eslint-friendly-formatter": "^4.0.1", 34 | "eslint-loader": "^4.0.2", 35 | "eslint-plugin-flowtype-errors": "^4.4.0", 36 | "eslint-plugin-import": "^2.22.1", 37 | "eslint-plugin-node": "^11.1.0", 38 | "eslint-plugin-promise": "^4.3.1", 39 | "eslint-plugin-standard": "^5.0.0", 40 | "fake-indexeddb": "^3.1.2", 41 | "flow-bin": "^0.146.0", 42 | "jest": "^26.6.3", 43 | "mocha": "^8.3.2", 44 | "rimraf": "^3.0.2", 45 | "rollup": "^2.41.4", 46 | "rollup-plugin-node-resolve": "^5.2.0", 47 | "webpack": "^5.26.2", 48 | "webpack-bundle-analyzer": "^4.4.0", 49 | "webpack-cli": "^4.5.0", 50 | "webpack-dev-server": "^3.11.2", 51 | "webpack-merge": "^5.7.3", 52 | "workbox-webpack-plugin": "^6.1.2" 53 | }, 54 | "engines": { 55 | "node": ">= 6.0.0", 56 | "npm": ">= 3.0.0" 57 | }, 58 | "keywords": [ 59 | "offline", 60 | "indexeddb", 61 | "promise", 62 | "filesystem" 63 | ], 64 | "browserslist": [ 65 | "> 1%", 66 | "last 2 versions", 67 | "not ie <= 8" 68 | ], 69 | "author": "Michal Ostrowski ", 70 | "license": "MIT", 71 | "dependencies": { 72 | "dexie": "3.2.3" 73 | }, 74 | "directories": { 75 | "test": "test" 76 | }, 77 | "repository": { 78 | "type": "git", 79 | "url": "git+ssh://git@github.com/fagbokforlaget/simple-fs.git" 80 | }, 81 | "homepage": "https://github.com/fagbokforlaget/simple-fs#readme" 82 | } 83 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | 3 | export default { 4 | input: 'src/index.js', 5 | output: { 6 | file: 'dist/SimpleFS.mjs', 7 | format: 'esm' 8 | }, 9 | plugins: [resolve()] 10 | }; 11 | -------------------------------------------------------------------------------- /shiftleft.yml: -------------------------------------------------------------------------------- 1 | build_rules: 2 | - id: allow-zero-findings 3 | finding_types: 4 | - vuln 5 | - secret 6 | - insight 7 | severity: 8 | - SEVERITY_MEDIUM_IMPACT 9 | - SEVERITY_HIGH_IMPACT 10 | - SEVERITY_LOW_IMPACT 11 | threshold: 0 -------------------------------------------------------------------------------- /src/filesystem/fileinfo.js: -------------------------------------------------------------------------------- 1 | import Stats from './stats' 2 | 3 | export default class FileInfo extends Stats { 4 | constructor (node, path) { 5 | super(node, path) 6 | this.path = path 7 | this.assign(node) 8 | } 9 | 10 | assign (node) { 11 | Object.keys(node).forEach(key => { 12 | this[key] = node[key] 13 | }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/filesystem/filesystem.js: -------------------------------------------------------------------------------- 1 | // base FileSystem module, initial version, 2 | // needs refactoring 3 | 4 | import Path from './path' 5 | import MODE from './mode' 6 | import Node from './node' 7 | import Stats from './stats' 8 | import FileInfo from './fileinfo' 9 | import IndexedDbStorage from '../storages/indexeddb' 10 | 11 | export default class FileSystem { 12 | constructor (opts = {}) { 13 | this.storage = opts.storage || 14 | new IndexedDbStorage((opts && opts.name) || 'default') 15 | } 16 | 17 | async mkdir (path) { 18 | path = new Path(path).normalize() 19 | 20 | const data = await this.exists(path) 21 | 22 | if (data) { 23 | return data.path 24 | } 25 | 26 | const parent = await this.exists(path.parent) 27 | const parentId = parent ? parent.path : 0 28 | 29 | if (!path.parent.isRoot && !parent) { 30 | throw new Error('parent is not created yet') 31 | } else if (parent && parent.node.mode !== MODE.DIR) { 32 | throw new Error('parent is not dir') 33 | } 34 | 35 | const node = new Node(path.path, MODE.DIR, 0) 36 | return this.storage.create(path.path, node, parentId) 37 | } 38 | 39 | // Recursively creates directory, eq. to mkdir -p 40 | async mkdirParents (path, root = '') { 41 | path = new Path(path).normalize() 42 | const mparts = path.path.split('/') 43 | const mroot = root === '' ? '/' : `${root}/` 44 | const currentPath = mroot + mparts.shift() 45 | 46 | if (mparts.length === 0) { 47 | return this.mkdir(currentPath) 48 | } 49 | 50 | await this.mkdir(currentPath) 51 | return this.mkdirParents(mparts.join('/'), currentPath) 52 | } 53 | 54 | async rmdir (path) { 55 | path = new Path(path).normalize() 56 | const data = await this.exists(path) 57 | let isEmpty = false 58 | 59 | if (!data) { 60 | throw new Error('dir does not exists') 61 | } else if (data.node.mode === MODE.DIR) { 62 | isEmpty = await this.storage.isEmpty(data.path) 63 | } else { 64 | throw new Error('it is not a dir') 65 | } 66 | 67 | if (isEmpty) { 68 | await this.storage.remove(path.path) 69 | } else { 70 | throw new Error('dir is not empty') 71 | } 72 | } 73 | 74 | async readFile (path, options) { 75 | path = new Path(path).normalize() 76 | const data = await this.storage.get(path.path) 77 | if (!data) { 78 | throw new Error(`File ${path.path} does not exist`) 79 | } 80 | return data.node.data 81 | } 82 | 83 | async writeFile (path, data, options) { 84 | path = new Path(path).normalize() 85 | 86 | if (!(data instanceof Blob)) { 87 | throw new Error('data must be instance of Blob') 88 | } 89 | 90 | const parent = await this.exists(path.parent) 91 | 92 | if (!parent) { 93 | throw new Error('file needs parent') 94 | } else if (parent && parent.node.mode !== MODE.DIR) { 95 | throw new Error('parent should be dir') 96 | } 97 | 98 | const parentId = parent.path 99 | const node = new Node(path.path, MODE.FILE, data.size, options, data) 100 | 101 | return this.storage.put(path.path, node, parentId) 102 | } 103 | 104 | // Same as writeFile but it recursively creates directory 105 | // if not exists 106 | async outputFile (path, data, options) { 107 | path = new Path(path).normalize() 108 | 109 | if (!(data instanceof Blob)) { 110 | throw new Error('data must be instance of Blob') 111 | } 112 | 113 | const parentPath = await this.mkdirParents(path.parent) 114 | const parent = await this.exists(parentPath) 115 | if (parent && parent.node.mode !== MODE.DIR) { 116 | throw new Error('parent should be dir') 117 | } 118 | 119 | const parentId = parent.path 120 | const node = new Node(path.path, MODE.FILE, data.size, options, data) 121 | 122 | return this.storage.put(path.path, node, parentId) 123 | } 124 | 125 | // Dexie specific to insert multiple files in one go 126 | // Chrome is quite slow with lots of insertion, hence 127 | // this makes chrome happy 128 | // https://dev.to/skhmt/why-are-indexeddb-operations-significantly-slower-in-chrome-vs-firefox-1bnd 129 | async bulkOutputFiles (objs) { 130 | return this.storage.transaction('rw', async () => { 131 | for (let i = 0; i < objs.length; i++) { 132 | const o = objs[i] 133 | await this.outputFile(o.path, o.blob, o.options || {}) 134 | } 135 | }) 136 | } 137 | 138 | async rename (oldPath, newPath) { 139 | throw new Error('not implemented') 140 | } 141 | 142 | async unlink (path) { 143 | path = new Path(path).normalize() 144 | const data = await this.exists(path) 145 | 146 | if (!data) { 147 | throw new Error('file does not exists') 148 | } else if (data.node.mode === MODE.DIR) { 149 | throw new Error('path points to a directory, please use rmdir') 150 | } else { 151 | return this.storage.remove(data.path) 152 | } 153 | } 154 | 155 | async rmdirRecursive (path) { 156 | path = new Path(path).normalize() 157 | const data = await this.exists(path) 158 | 159 | if (!data) return true 160 | if (data.node.mode !== MODE.DIR) return this.unlink(path) 161 | 162 | const list = await this.ls(path) 163 | 164 | for (const element of list) { 165 | if (element.node.mode !== MODE.DIR) await this.unlink(element.path) 166 | else await this.rmdirRecursive(element.path) 167 | } 168 | 169 | return this.rmdir(path) 170 | } 171 | 172 | async exists (path) { 173 | path = new Path(path).normalize() 174 | 175 | return this.storage.get(path.path) 176 | } 177 | 178 | async stats (path) { 179 | const data = await this.exists(path) 180 | if (data) return new Stats(data.node, this.path) 181 | else throw new Error('path does not exist') 182 | } 183 | 184 | async ls (path, filters = {}) { 185 | const filterKeys = Object.keys(filters) 186 | const data = await this.exists(path) 187 | 188 | if (data) { 189 | let nodes = await this.storage.where({ parentId: data.path }) 190 | 191 | if (filterKeys.length > 0) { 192 | nodes = nodes.filter((node) => { 193 | const fileInfo = new FileInfo(node.node, node.path) 194 | 195 | return filterKeys.some((key) => { 196 | return fileInfo[key] === filters[key] 197 | }) 198 | }) 199 | } 200 | 201 | return nodes.map(node => new FileInfo(node.node, node.path)) 202 | } 203 | 204 | throw new Error('path does not exist') 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/filesystem/flags.js: -------------------------------------------------------------------------------- 1 | // flags for files/dirs 2 | 3 | export default { 4 | READ: 'READ', 5 | WRITE: 'WRITE' 6 | } 7 | -------------------------------------------------------------------------------- /src/filesystem/mode.js: -------------------------------------------------------------------------------- 1 | // file modes (dir, file, link) 2 | 3 | export default { 4 | DIR: 'DIR', 5 | FILE: 'FILE', 6 | SYMBOLIC_LINK: 'SYMBOLIC_LINK' 7 | } 8 | -------------------------------------------------------------------------------- /src/filesystem/node.js: -------------------------------------------------------------------------------- 1 | // base file (or dir) node 2 | // not sure what else should we keep here 3 | 4 | import MODE from './mode' 5 | 6 | export default class Node { 7 | constructor (path, mode, size, flags, data, atime, ctime, mtime) { 8 | const now = Date.now() 9 | 10 | this.path = path 11 | this.mode = mode || MODE.FILE 12 | this.size = size || 0 13 | this.flags = flags || {} 14 | this.atime = atime || now 15 | this.ctime = ctime || now 16 | this.mtime = mtime || now 17 | this.blksize = undefined 18 | this.nblocks = 1 19 | this.data = data // blob, arraybuffer, text, you name it 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/filesystem/path.js: -------------------------------------------------------------------------------- 1 | // helper class to handle paths 2 | 3 | export default class Path { 4 | constructor (path) { 5 | if (typeof path === 'string') { 6 | this.path = path 7 | } else if (!(path instanceof Path)) { 8 | throw new Error('It\'s neither Path nor string') 9 | } else { 10 | this.path = path.path 11 | } 12 | } 13 | 14 | normalize () { 15 | if (this.path === '\\' || this.path === '/') { 16 | this.path = '/' 17 | return this 18 | } 19 | 20 | this.path = this.path.split(/[/\\]+/).join('/') 21 | 22 | return this 23 | } 24 | 25 | get basename () { 26 | const parts = this.path.match(/(\/)?(\w+\.(\S+))/) 27 | if (parts.size < 2) return undefined 28 | return parts[2] 29 | } 30 | 31 | get extension () { 32 | const parts = this.path.match(/(\/)?(\w+\.(\S+))/) 33 | if (parts.size < 3) return undefined 34 | return parts[3] 35 | } 36 | 37 | get parent () { 38 | const parts = this.path.split('/') 39 | parts.pop() 40 | return new Path(parts.join('/')).normalize() 41 | } 42 | 43 | get isRoot () { 44 | return this.path === '' 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/filesystem/stats.js: -------------------------------------------------------------------------------- 1 | import MODE from './mode' 2 | 3 | export default class Stats { 4 | constructor (node, devName) { 5 | this.node = node 6 | this.dev = devName 7 | this.size = node.size 8 | this.atime = node.atime 9 | this.ctime = node.ctime 10 | this.mtime = node.mtime 11 | this.type = node.mode 12 | } 13 | 14 | isFile () { 15 | return (this.type === MODE.FILE) 16 | } 17 | 18 | isDirectory () { 19 | return (this.type === MODE.DIR) 20 | } 21 | 22 | isSymbolicLink () { 23 | return (this.type === MODE.SYMBOLIC_LINK) 24 | } 25 | 26 | isSocket () { 27 | return false 28 | } 29 | 30 | isFIFO () { 31 | return false 32 | } 33 | 34 | isCharacterDevice () { 35 | return false 36 | } 37 | 38 | isBlockDevice () { 39 | return false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import FileSystem from './filesystem/filesystem' 2 | import IndexedDbStorage from './storages/indexeddb' 3 | import MemoryStorage from './storages/memory' 4 | 5 | export { 6 | FileSystem, 7 | IndexedDbStorage, 8 | MemoryStorage 9 | } 10 | 11 | export default { 12 | FileSystem, 13 | IndexedDbStorage, 14 | MemoryStorage 15 | } 16 | -------------------------------------------------------------------------------- /src/storages/base.js: -------------------------------------------------------------------------------- 1 | export default class BaseStorage { 2 | create (path, node, paraentId) { 3 | throw new Error('not implemented') 4 | } 5 | 6 | remove (path) { 7 | throw new Error('not implemented') 8 | } 9 | 10 | put (path, node, parentId) { 11 | throw new Error('not implemented') 12 | } 13 | 14 | get (path) { 15 | throw new Error('not implemented') 16 | } 17 | 18 | where (params) { 19 | throw new Error('not implemented') 20 | } 21 | 22 | isEmpty (parentId) { 23 | throw new Error('not implemented') 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/storages/indexeddb.js: -------------------------------------------------------------------------------- 1 | // I decied to keep it simple, it's just a storage 2 | 3 | import Dexie from 'dexie' 4 | import BaseStorage from './base' 5 | 6 | export default class Storage extends BaseStorage { 7 | constructor (storageName = 'default') { 8 | super(storageName) 9 | 10 | this.name = 'indexeddb' 11 | this.storage = new Dexie(storageName) 12 | 13 | this.storage.version(1).stores({ 14 | files: 'path,node,parentId' 15 | }) 16 | } 17 | 18 | create (path, node, parentId) { 19 | return this.put(path, node, parentId) 20 | } 21 | 22 | async remove (path) { 23 | await this.storage.files.where({ path: path }).delete() 24 | } 25 | 26 | put (path, node, parentId) { 27 | return this.storage.files.put({ path: path, node: node, parentId: parentId }) 28 | } 29 | 30 | transaction (mode, cb) { 31 | return this.storage.transaction(mode, this.storage.files, cb) 32 | } 33 | 34 | get (path) { 35 | return this.storage.files.get({ path: path }) 36 | } 37 | 38 | getBy (key, value) { 39 | const params = {} 40 | params[key] = value 41 | 42 | return this.storage.files.where(params).toArray() 43 | } 44 | 45 | where (params) { 46 | return this.storage.files.where(params).toArray() 47 | } 48 | 49 | async isEmpty (parentId) { 50 | const count = await this.storage.files.where({ parentId: parentId }).count() 51 | return count === 0 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/storages/memory.js: -------------------------------------------------------------------------------- 1 | import BaseStorage from './base' 2 | 3 | export default class MemoryStorage extends BaseStorage { 4 | constructor (storageName = 'default') { 5 | super(storageName) 6 | 7 | this.name = 'memory' 8 | // for unit testing 9 | this.storage = { 10 | files: {}, 11 | transaction: function (mode, table, cb) { 12 | return new Promise((resolve, reject) => { 13 | resolve(cb()) 14 | }) 15 | } 16 | } 17 | 18 | this.data = {} 19 | } 20 | 21 | async create (path, node, parentId) { 22 | this.data[path] = { path: path, node: node, parentId: parentId } 23 | return path 24 | } 25 | 26 | async remove (path) { 27 | if (path in this.data) { 28 | delete this.data[path] 29 | } 30 | return undefined 31 | } 32 | 33 | async put (path, node, parentId) { 34 | return this.create(path, node, parentId) 35 | } 36 | 37 | async transaction (mode, cb) { 38 | return this.storage.transaction(mode, this.storage.files, cb) 39 | } 40 | 41 | async get (path) { 42 | const keys = Object.keys(this.data) 43 | 44 | for (let i = 0; i < keys.length; i++) { 45 | if (this.data[keys[i]].path === path) { 46 | return this.data[keys[i]] 47 | } 48 | } 49 | return undefined 50 | } 51 | 52 | async where (params) { 53 | const paramsKeys = Object.keys(params) 54 | const ret = [] 55 | 56 | Object.keys(this.data).forEach((d) => { 57 | let canBe = true 58 | const object = this.data[d] 59 | paramsKeys.forEach((param) => { 60 | if (object[param] !== params[param]) canBe = false 61 | }) 62 | if (canBe) ret.push(object) 63 | }) 64 | return ret 65 | } 66 | 67 | async isEmpty (parentId) { 68 | let count = 0 69 | const keys = Object.keys(this.data) 70 | 71 | for (let i = 0; i < keys.length; i++) { 72 | if (this.data[keys[i]].parentId === parentId) { 73 | count += 1 74 | } 75 | } 76 | return count === 0 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } -------------------------------------------------------------------------------- /test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../../'), 5 | moduleDirectories: ['node_modules', 'src'], 6 | moduleFileExtensions: [ 7 | 'js', 8 | 'json' 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | transform: { 14 | '^.+\\.jsx?$': 'babel-jest' 15 | }, 16 | testPathIgnorePatterns: [ 17 | '/test/e2e' 18 | ], 19 | setupFiles: ['/test/unit/setup'], 20 | coverageDirectory: '/coverage', 21 | collectCoverage: true 22 | } 23 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie' 2 | import indexedDB from 'fake-indexeddb' 3 | 4 | Dexie.dependencies.indexedDB = indexedDB 5 | Dexie.dependencies.IDBKeyRange = require('fake-indexeddb/lib/FDBKeyRange') 6 | -------------------------------------------------------------------------------- /test/unit/specs/fileinfo.spec.js: -------------------------------------------------------------------------------- 1 | import FileInfo from '../../../src/filesystem/fileinfo' 2 | 3 | describe('FileInfo API', () => { 4 | it('creates instance', () => { 5 | const info = new FileInfo({ name: 'test', size: 1234, atime: Date.now(), ctime: Date.now(), mtime: Date.now() }, '/somepath') 6 | expect(typeof info).toBe('object') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /test/unit/specs/filesystem.spec.js: -------------------------------------------------------------------------------- 1 | import { FileSystem, MemoryStorage } from '../../../src/index' 2 | 3 | describe('Filesystem API using IDB', () => { 4 | let fs 5 | 6 | beforeEach(() => { 7 | fs = new FileSystem() 8 | }) 9 | 10 | it('creates instance', () => { 11 | expect(typeof fs).toBe('object') 12 | }) 13 | 14 | it('create directory mkdir', async () => { 15 | const id = await fs.mkdir('root') 16 | 17 | expect(typeof id).toBe('string') 18 | }) 19 | 20 | it('create nested directory with error', async () => { 21 | const path = '/root/is/the/king' 22 | 23 | await expect(fs.mkdir(path)).rejects.toEqual(new Error('parent is not created yet')) 24 | }) 25 | 26 | 27 | }) 28 | 29 | describe('Filesystem API using memory storage', () => { 30 | let fs 31 | 32 | beforeEach(() => { 33 | fs = new FileSystem({storage: new MemoryStorage()}) 34 | }) 35 | 36 | it('creates instance', () => { 37 | expect(typeof fs).toBe('object') 38 | }) 39 | 40 | it('create directory mkdir', async () => { 41 | let id = await fs.mkdir('root') 42 | expect(typeof id).toBe('string') 43 | }) 44 | 45 | it('create directory mkdirParents', async () => { 46 | let path = '/root/is/the/king' 47 | let id = await fs.mkdirParents(path) 48 | expect(id).toBe(path) 49 | }) 50 | 51 | it('delete directory rmdir', async () => { 52 | let id = await fs.mkdir('root') 53 | expect(typeof id).toBe('string') 54 | 55 | let resp = await fs.rmdir('root') 56 | expect(resp).toBeUndefined() 57 | }) 58 | 59 | it('delete directory that does not exists', async () => { 60 | await expect(fs.rmdir('root')).rejects.toEqual(new Error('dir does not exists')) 61 | }) 62 | 63 | it('does not delete file with rmdir', async () => { 64 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 65 | await fs.outputFile('/root/xx/test.txt', blob) 66 | 67 | await expect(fs.rmdir('/root/xx/test.txt')).rejects.toEqual(new Error('it is not a dir')) 68 | }) 69 | 70 | it('does not delete non-empty dirs', async () => { 71 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 72 | await fs.outputFile('/root/xx/test.txt', blob) 73 | 74 | await expect(fs.rmdir('/root/xx')).rejects.toEqual(new Error('dir is not empty')) 75 | }) 76 | 77 | it('does bulk insert', async () => { 78 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 79 | await fs.bulkOutputFiles([ 80 | {path: '/root/xx/test.txt', blob: blob}, 81 | {path: '/root/xy/test.txt', blob: blob}, 82 | {path: '/root/xz/test.txt', blob: blob}, 83 | {path: '/root/xa/test.txt', blob: blob}, 84 | {path: '/root/xb/test.txt', blob: blob}, 85 | {path: '/root/xc/test.txt', blob: blob}, 86 | {path: '/root/xd/test.txt', blob: blob}, 87 | {path: '/root/xe/test.txt', blob: blob}, 88 | {path: '/root/xf/test.txt', blob: blob}, 89 | {path: '/root/xg/test.txt', blob: blob}, 90 | ]) 91 | 92 | await expect(fs.rmdir('/root/xx')).rejects.toEqual(new Error('dir is not empty')) 93 | await expect(fs.rmdir('/root/xy')).rejects.toEqual(new Error('dir is not empty')) 94 | await expect(fs.rmdir('/root/xz')).rejects.toEqual(new Error('dir is not empty')) 95 | await expect(fs.rmdir('/root/xa')).rejects.toEqual(new Error('dir is not empty')) 96 | await expect(fs.rmdir('/root/xb')).rejects.toEqual(new Error('dir is not empty')) 97 | await expect(fs.rmdir('/root/xc')).rejects.toEqual(new Error('dir is not empty')) 98 | await expect(fs.rmdir('/root/xd')).rejects.toEqual(new Error('dir is not empty')) 99 | await expect(fs.rmdir('/root/xe')).rejects.toEqual(new Error('dir is not empty')) 100 | await expect(fs.rmdir('/root/xf')).rejects.toEqual(new Error('dir is not empty')) 101 | await expect(fs.rmdir('/root/xg')).rejects.toEqual(new Error('dir is not empty')) 102 | }) 103 | 104 | it('create file', async () => { 105 | let id = await fs.mkdir('root') 106 | expect(typeof id).toBe('string') 107 | 108 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 109 | 110 | let resp = await fs.writeFile('root/test.txt', blob) 111 | expect(typeof resp).toBe('string') 112 | }) 113 | 114 | it('create file and creates parent dirs recursively', async () => { 115 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 116 | 117 | let resp = await fs.outputFile('root/to/some/unknown/folder/test.txt', blob) 118 | expect(typeof resp).toBe('string') 119 | }) 120 | 121 | it('read file', async () => { 122 | let id = await fs.mkdir('root') 123 | expect(typeof id).toBe('string') 124 | 125 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 126 | let resp = await fs.writeFile('root/test.txt', blob) 127 | expect(typeof resp).toBe('string') 128 | 129 | resp = await fs.readFile('root/test.txt') 130 | expect(typeof resp).toBe('object') 131 | }) 132 | 133 | it('read file which does not exists', async () => { 134 | let id = await fs.mkdir('root') 135 | expect(typeof id).toBe('string') 136 | 137 | await expect(fs.readFile('root/test.txt')).rejects.toEqual(new Error('File root/test.txt does not exist')) 138 | }) 139 | 140 | it('write file without root', async () => { 141 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 142 | 143 | await expect(fs.writeFile('root/test.txt', blob)).rejects.toEqual(new Error('file needs parent')) 144 | }) 145 | 146 | it('unlink file', async () => { 147 | let id = await fs.mkdir('root') 148 | expect(typeof id).toBe('string') 149 | 150 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 151 | let resp = await fs.writeFile('root/test.txt', blob) 152 | expect(typeof resp).toBe('string') 153 | 154 | let removed = await fs.unlink('root/test.txt') 155 | expect(removed).toEqual(undefined) 156 | 157 | await expect(fs.unlink('root/test.txt')).rejects.toEqual(new Error('file does not exists')) 158 | }) 159 | 160 | it('stats file', async () => { 161 | let id = await fs.mkdir('root') 162 | expect(typeof id).toBe('string') 163 | 164 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 165 | let resp = await fs.writeFile('root/test.txt', blob) 166 | expect(typeof resp).toBe('string') 167 | 168 | let stats = await fs.stats('root/test.txt') 169 | expect(typeof stats).toBe('object') 170 | }) 171 | 172 | it('check if file is a directory', async () => { 173 | let id = await fs.mkdir('root') 174 | expect(typeof id).toBe('string') 175 | 176 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 177 | let resp = await fs.writeFile('root/test.txt', blob) 178 | expect(typeof resp).toBe('string') 179 | 180 | let stats = await fs.stats('root') 181 | 182 | expect(stats.isFile()).toBe(false) 183 | expect(stats.isSymbolicLink()).toBe(false) 184 | expect(stats.isDirectory()).toBe(true) 185 | }) 186 | 187 | it('rename file', async () => { 188 | await expect(fs.rename('root/test.txt', 'root/new.txt')).rejects.toEqual(new Error('not implemented')) 189 | }) 190 | 191 | it('list root files', async () => { 192 | await fs.mkdir('/root') 193 | await fs.mkdir('/root/files') 194 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 195 | await fs.writeFile('/root/test1.txt', blob) 196 | await fs.writeFile('/root/files/test2.txt', blob) 197 | await fs.writeFile('/root/files/test3.txt', blob) 198 | 199 | let respRoot = await fs.ls('/root') 200 | expect(respRoot.length).toBe(2) 201 | }) 202 | 203 | it('list child files', async () => { 204 | await fs.mkdir('root') 205 | await fs.mkdir('root/files') 206 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 207 | await fs.writeFile('root/test1.txt', blob) 208 | await fs.writeFile('root/files/test2.txt', blob) 209 | await fs.writeFile('root/files/test3.txt', blob) 210 | 211 | let respChild = await fs.ls('root/files') 212 | expect(respChild.length).toBe(2) 213 | }) 214 | 215 | it('list child file as FileInfo', async () => { 216 | await fs.mkdir('root') 217 | await fs.mkdir('root/files') 218 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 219 | await fs.writeFile('root/test1.txt', blob) 220 | await fs.writeFile('root/files/test2.txt', blob) 221 | await fs.writeFile('root/files/test3.txt', blob) 222 | 223 | let respChild = await fs.ls('root/files') 224 | expect(respChild[0].mode).toBe('FILE') 225 | expect(respChild[0].isFile()).toBe(true) 226 | }) 227 | 228 | it('filters output', async () => { 229 | await fs.mkdir('/root') 230 | await fs.mkdir('/root/files') 231 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 232 | await fs.writeFile('/root/test1.txt', blob) 233 | await fs.writeFile('/root/files/test2.txt', blob) 234 | await fs.writeFile('/root/files/test3.txt', blob) 235 | let respRoot = await fs.ls('/root', { 'mode': 'DIR' }) 236 | expect(respRoot.length).toBe(1) 237 | }) 238 | 239 | it('deletes recursively', async () => { 240 | let blob = new Blob(['my test data'], { type: 'plain/text' }) 241 | 242 | let dirs = ['/rootX', '/rootX/files', '/rootX/files/1', '/rootX/anotherFiles'] 243 | let files = ['/rootX/test1.txt', '/rootX/files/test2.txt', '/rootX/files/test3.txt', '/rootX/files/1/test4.txt', '/rootX/anotherFiles/test4.txt'] 244 | 245 | for (let el of dirs) { 246 | await fs.mkdir(el) 247 | } 248 | 249 | for (let el of files) { 250 | await fs.writeFile(el, blob) 251 | } 252 | 253 | await fs.rmdirRecursive('/rootX') 254 | 255 | for (let el of files.concat(dirs)) { 256 | expect(await fs.exists(el)).not.toBe(true) 257 | } 258 | }) 259 | }) 260 | -------------------------------------------------------------------------------- /test/unit/specs/path.spec.js: -------------------------------------------------------------------------------- 1 | import Path from '../../../src/filesystem/path' 2 | 3 | describe('Path API', () => { 4 | it('creates an instance when given string', () => { 5 | const path = new Path('/root/to/object.json') 6 | expect(typeof path).toBe('object') 7 | }) 8 | 9 | it('creates an instance when given another path', () => { 10 | const path = new Path('/root/to/object.json') 11 | const path1 = new Path(path) 12 | expect(typeof path1).toBe('object') 13 | expect(path1.path).toEqual(path.path) 14 | }) 15 | 16 | it('throws an error when an argument is not path nor string', () => { 17 | expect(() => new Path({ path: '/root' })).toThrow() 18 | }) 19 | 20 | it('should normalize path', () => { 21 | const path = new Path('/root//to/object.json').normalize() 22 | 23 | expect(path.path).toEqual('/root/to/object.json') 24 | }) 25 | 26 | it('should return file basename', () => { 27 | const path = new Path('/root/to/object.json') 28 | expect(path.basename).toEqual('object.json') 29 | }) 30 | 31 | it('should return file extension', () => { 32 | const path = new Path('/root/to/object.json') 33 | expect(path.extension).toEqual('json') 34 | }) 35 | 36 | it('should return parent folder', () => { 37 | const path = new Path('/root/to/object.json') 38 | expect(path.parent.path).toEqual('/root/to') 39 | }) 40 | 41 | it('parent should be Path', () => { 42 | const path = new Path('/root/to/object.json') 43 | expect(path.parent instanceof Path).toBe(true) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/unit/specs/stats.spec.js: -------------------------------------------------------------------------------- 1 | import Stats from '../../../src/filesystem/stats' 2 | 3 | describe('Stats API', () => { 4 | it('creates instance', () => { 5 | const info = new Stats({ name: 'test', size: 1234, atime: Date.now(), ctime: Date.now(), mtime: Date.now() }, '/somepath') 6 | expect(typeof info).toBe('object') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const libraryName = 'SimpleFS'; 5 | 6 | let outputFile = `${libraryName}.js`; 7 | 8 | module.exports = { 9 | mode: 'production', 10 | entry: __dirname + '/src/index.js', 11 | devtool: 'source-map', 12 | output: { 13 | path: __dirname + '/dist', 14 | filename: outputFile, 15 | library: libraryName, 16 | libraryExport: 'default', 17 | libraryTarget: 'umd', 18 | globalObject: `typeof self !== 'undefined' ? self : this`, 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | exclude: /node_modules/, 25 | loader: 'babel-loader', 26 | options: { 27 | presets: [ 28 | [ '@babel/env', { modules: false } ] 29 | ] 30 | } 31 | }, 32 | { 33 | test: /(\.jsx|\.js)$/, 34 | loader: "eslint-loader", 35 | options: { 36 | formatter: require("eslint/lib/cli-engine/formatters/stylish") 37 | }, 38 | exclude: /node_modules/ 39 | } 40 | ] 41 | }, 42 | resolve: { 43 | modules: [ 44 | path.join(__dirname, "src"), 45 | "node_modules" 46 | ], 47 | alias: { 48 | dexie: path.resolve(__dirname, 'node_modules/dexie/dist/dexie.min'), 49 | } 50 | }, 51 | plugins: [ 52 | new webpack.DefinePlugin({ 53 | 'process.env.NODE_ENV': '"production"' 54 | }) 55 | ] 56 | }; 57 | --------------------------------------------------------------------------------