├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .travis.yml ├── README.md ├── README_PROTOCOL.md ├── boot.js ├── config.ex ├── config.json ├── docs ├── roadmap.md └── unikernel.md ├── jlog-nomem.js ├── jsfs.service ├── lib ├── constants.js ├── file-types.js ├── fs │ └── disk-operations.js ├── google-cloud-storage │ └── disk-operations.js ├── inode.js ├── utils.js └── validate.js ├── license.md ├── old-jlog.js ├── package-lock.json ├── package.json ├── rock64.md ├── server.js ├── test ├── file-types.js ├── fixtures │ ├── test.mp3 │ └── test.wav ├── utils.js └── validate.js └── tools ├── compress_blocks.js ├── compression_notes.txt ├── jsfsck.js ├── migrate_superblock.js ├── movesomeblocks.sh └── pool_size.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm run lint 32 | - run: npm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config.js 3 | blocks 4 | *swp 5 | .DS_Store 6 | *.sublime-* 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "6.1" 5 | - "5.11" 6 | - "4" 7 | install: 8 | - cp config.ex config.js 9 | - npm install 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsfs 2 | 3 | A general-purpose, deduplicating filesystem with a REST interface, jsfs is intended to provide low-level filesystem functions for Javascript applications. Additional functionality (private file indexes, token lockers, centralized authentication, etc.) are deliberately avoided here and will be implemented in a modular fashion on top of jsfs. 4 | 5 | ## REQUIREMENTS 6 | * Node.js 7 | 8 | ## CONFIGURATION 9 | * Clone this repository 10 | * Copy config.ex to config.js 11 | * Create the `blocks` directory (`mkdir blocks`) 12 | * Start the server (`node server.js`, `npm start`, foreman, pm2, etc.) 13 | 14 | If you don't like storing the data in the same directory as the code (smart), edit config.js to change the location where data (blocks) are stored and restart jsfs for the changes to take effect. 15 | 16 | Additional storage locations can be specified to allow the JSFS pool to span physical devices. In this configuration JSFS will spread the stored blocks evenly across multiple devices (inode files will be written to all devices for redundancy). 17 | 18 | It's important to note that configuring multiple storage devices does not provide redundancy to the data stored in the pool. If a storage device becomes unavaliable, and a file is requested that is composed of blocks on the missing device, the file will be corrupt. If the device is restored, or the blocks that were stored on the device are added to the remaining device, JSFS will automatically return to delivering the undamaged files. 19 | 20 | Future versions of JSFS may include an option to use multiple storage locations for the purpose of redundancy. 21 | 22 | ### REMOTE STORAGE CONFIGURATION 23 | By default, JSFS assumes you are working with a local file system using node's `fs` module. However, JSFS currently supports remote file storage such as blob or object storage services. 24 | 25 | To use a remote storage service: 26 | * Copy /lib/fs/disk-operations.js to /lib/your-storage-serice/disk-operations.js 27 | * Update /lib/your-storage-serice/disk-operations.js as necessary (see /lib/google-cloud-storage/disk-operations.js for examples) 28 | * Update `config.CONFIGURED_STORAGE` to `your-storage-service` 29 | * Add any additional configuration as appropriate. 30 | 31 | When JSFS boots, it will load `./lib/${config.CONFIGURED_STORAGE || "fs"}/disk-operations.js` for all disk-type operations. 32 | 33 | ## API 34 | 35 | ### Keys and Tokens 36 | Keys are used to unlock all operations that can be performed on an object stored in JSFS, and objects can have only one key. With an `access_key`, you can execute all supported HTTP verbs (GET, PUT, DELETE) as well as generate tokens that grant varying degrees of access to the object. 37 | 38 | Tokens are more ephemeral, and any number of them can be generated to grant varying degrees of access to an object. Token generation is described later. 39 | 40 | #### Static access keys 41 | If you want to limit the entire server to a fixed set of static `access_keys`, add some keys to the `STATIC_ACCESS_KEYS` array in `config.js`: 42 | 43 | ```js 44 | STATIC_ACCESS_KEYS: ["foo", "bar"] 45 | ``` 46 | 47 | Any write requests for new files that do not include an `access_key` in this array will return `unauthorized`, as will any write request that includes no `access_key` or `access_token`. 48 | 49 | ### Parameters and Headers 50 | jsfs uses several parameters to control access to objects and how they are stored. These values can also be supplied as request headers by adding a leading "x-" and changing "_" to "-" (`access_token` becomes `x-access-token`). Headers are preferred to querystring parameters because they are less likely to collide but both function the same. 51 | 52 | #### private 53 | By default all objects stored in jsfs are public and will be accessible via any `GET` request. If the `private` parameter is set to `true` a valid `access_key` or `access_token` must be supplied to access the object. 54 | 55 | #### access_key 56 | Specifying a valid access_key authorizes the use of all supported HTTP verbs and is required for requests to change the `access_key` or generate `access_token`s. When a new object is stored, jsfs will generate an `access_key` automatically if one is not specified and return the generated key in the response to a `POST` request. 57 | 58 | An `access_key` can be changed by supplying the current `access_key` along with the `replacement_access_key` parameter. This will cause any existing `access_token`s to become invalid. 59 | 60 | #### access_token 61 | An `access_token` must be provided to execute any request on a `private` object, and is required for `PUT` and `DELETE` if an `access_key` is not supplied. 62 | 63 | ##### Generating access_tokens 64 | Currently there are two types of `access_token`: durable and temporary. Both are generated by creating a string that describes what access is granted and then using SHA1 to generate a hash of this string, but the format and use is a little different. 65 | 66 | Durable `access_token`s are generated by concatinating an object's `access_key` with the HTTP verb that the token will be used for. 67 | 68 | Example to grant GET access: 69 | 70 | "077785b5e45418cf6caabdd686719813fb22e3ce" + "GET" 71 | 72 | This string is then hashed with SHA1 and can be used to perform a `GET` request for the object whose `access_key` was used to generate the token. 73 | 74 | To make a temporary token for this same object, concatinate the `access_key` with the HTTP verb and the expiration time in epoc format (milliseconds since midnight, 01/01/1970): 75 | 76 | "077785b5e45418cf6caabdd686719813fb22e3ce" + "GET" + "1424877559581" 77 | 78 | This string is then hashed with SHA1 and supplied as a parameter or header with the request, along with an additional parameter named `expires` which is set to match the expiration time used above. When the jsfs server receives the request, it generates the same token based on the stored `access_key`, the HTTP method of the incoming request and the supplied `expires` parameter to validate the `access_token`. 79 | 80 | *NOTE: all `access_tokens` can be immediately invalidated by changing an objects `access_key`, however if individual `access_tokens` need to be invalidated a pattern of requesting new, temporary tokens before each request is recommended.* 81 | 82 | ### POST 83 | Stores a new object at the specified URL. If the object exists and the `access_key` is not provided, jsfs returns `405 method not allowed`. 84 | 85 | #### EXAMPLE 86 | Request: 87 | 88 | curl -X POST --data-binary @Brinstar.mp3 "http://localhost:7302/music/Brinstar.mp3" 89 | 90 | Response: 91 | ```` 92 | { 93 | "url": "/localhost/music/Brinstar.mp3", 94 | "created": 1424878242595, 95 | "version": 0, 96 | "private": false, 97 | "encrypted": false, 98 | "fingerprint": "fde752ca6541c16ec626a3cf6e45e835cfd9db9b", 99 | "access_key": "fde752ca6541c16ec626a3cf6e45e835cfd9db9b", 100 | "content_type": "application/x-www-form-urlencoded", 101 | "file_size": 7678080, 102 | "block_size": 1048576, 103 | "blocks": [ 104 | { 105 | "block_hash": "610f0b4c20a47b4162edc224db602a040cc9d243", 106 | "last_seen": "./blocks/" 107 | }, 108 | { 109 | "block_hash": "60a93a7c97fd94bb730516333f1469d101ae9d44", 110 | "last_seen": "./blocks/" 111 | }, 112 | { 113 | "block_hash": "62774a105ffc5f57dcf14d44afcc8880ee2fff8c", 114 | "last_seen": "./blocks/" 115 | }, 116 | { 117 | "block_hash": "14c9c748e3c67d8ec52cfc2e071bbe3126cd303a", 118 | "last_seen": "./blocks/" 119 | }, 120 | { 121 | "block_hash": "8697c9ba80ef824de9b0e35ad6996edaa6cc50df", 122 | "last_seen": "./blocks/" 123 | }, 124 | { 125 | "block_hash": "866581c2a452160748b84dcd33a2e56290f1b585", 126 | "last_seen": "./blocks/" 127 | }, 128 | { 129 | "block_hash": "6c1527902e873054b36adf46278e9938e642721c", 130 | "last_seen": "./blocks/" 131 | }, 132 | { 133 | "block_hash": "10938182cd5e714dacb893d6127f8ca89359fec7", 134 | "last_seen": "./blocks/" 135 | } 136 | ] 137 | } 138 | ```` 139 | 140 | JSFS automatically "namespaces" URL's based on the host name used to make the request. This makes it easy to create partitioned storage by pointing multiple hostnames at the same JSFS server. This is accomplished by expanding the host in reverse notation (i.e.: `foo.com` becomes `.com.foo`); this is handled transparrently by JSFS from the client's perspective. 141 | 142 | This means that you can point DNS records for `foo.com` and `bar.com` to the same JSFS server and then POST `http://foo.com:7302/files/baz.txt` and `http://bar.com:7302/files/baz.txt` without a conflict. 143 | 144 | This also means that `GET http://foo.com:7302/files/baz.txt` and `GET http://bar.com:7302/files/baz.txt` do not return the same file, however if you need to access a file stored via a different host you can reach it using its absolute address (in this case, `http://bar.com:7302/.com.foo/files/baz.txt`). 145 | 146 | ### GET 147 | Retreives the object stored at the specified URL. If the file does not exist a `404 not found` is returned. 148 | 149 | #### EXAMPLE 150 | 151 | Request: 152 | 153 | curl -o Brinstar.mp3 http://localhost:730s/music/Brinstar.mp3 154 | 155 | Response: 156 | The binary file is stored in new local file called `Brinstar.mp3`. 157 | 158 | ### PUT 159 | Updates an existing object stored at the specified location. This method requires authorization, so requests must include a valid `x-access-key` or `x-access-token` header for the specific file, otherwise `401 unauthorized` will be returned. If the file does not exist `405 method not allowed` is returned. 160 | 161 | #### EXAMPLE 162 | Request: 163 | 164 | curl -X PUT -H "x-access-key: 7092bee1ac7d4a5c55cb5ff61043b89a6e32cf71" --data-binary @Brinstar.mp3 "http://localhost:7302/music/Brinstar.mp3" 165 | 166 | Result: 167 | `HTTP 206` 168 | 169 | *note: `POST` and `PUT` can actualy be used interchangably, but HTTP conventions recommend using them as described here.* 170 | 171 | ### DELETE 172 | Removes the file at the specified URL. This method requires authorization so requests must include a valid `x-access-key` or `x-access-token` header for the specified file. If the token is not supplied or is invalid `401 unauthorized` is returend. If the file does not exist `405 method not allowed` is returned. 173 | 174 | #### Example 175 | Request: 176 | 177 | curl -X DELETE -H "x-access-token: 7092bee1ac7d4a5c55cb5ff61043b89a6e32cf71" "http://localhost:7302/music/Brinstar.mp3" 178 | 179 | Response 180 | `HTTP 206` if sucessful. 181 | 182 | ### HEAD 183 | Returns status and header information for the specified URL. 184 | 185 | #### Example 186 | Request: 187 | 188 | curl -I "http://localhost:7302/music/Brinstar.mp3" 189 | 190 | Response: 191 | ```` 192 | HTTP/1.1 200 OK 193 | Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS 194 | Access-Control-Allow-Headers: Accept,Accept-Version,Content-Type,Api-Version,Origin,X-Requested-With,Range,X_FILENAME,X-Access-Key,X-Replacement-Access-Key,X-Access-Token,X-Encrypted,X-Private 195 | Access-Control-Allow-Origin: * 196 | Content-Type: application/x-www-form-urlencoded 197 | Content-Length: 7678080 198 | Date: Wed, 25 Feb 2015 15:43:03 GMT 199 | Connection: keep-alive 200 | ```` 201 | -------------------------------------------------------------------------------- /README_PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # The JSFS Protocol 2 | 3 | > I'm calling this "protocol" for now just because I don't want to waste any time thinking about a better term. This document describes both how a JSFS server should interact with a client, but it also specifies some other aspects about how a "compliant" server should work which I'm not sure is accurately desrcibed as "protocol". 4 | 5 | ## Summary 6 | 7 | JSFS is a federatable, deduplicating filesystem with a REST interface. 8 | 9 | ## Interface 10 | 11 | A compliant JSFS server supports the following HTTP verbs: 12 | 13 | * GET 14 | * PUT [^1] 15 | * POST 16 | * HEAD 17 | * DELETE 18 | * EXECUTE [^2] 19 | 20 | ## Authorization 21 | 22 | Keys and tokens are used to decide if a client has permission to execute a request against a given resource. 23 | 24 | ### Keys 25 | 26 | A valid key will allow a client to make a request against the keys associated resource using any supported verb. Keys set when a resource is created and can be changed if the client has the current key. 27 | 28 | ### Tokens 29 | 30 | 31 | 32 | ## Namespaces 33 | 34 | ## Deduplication 35 | 36 | ## Federation 37 | 38 | 39 | 40 | [^1] PUT is an alias for POST, included for the conveniece of clients. 41 | [^2] No JSFS server with EXECUTE support has been implemented so it may be considered optional for now. 42 | -------------------------------------------------------------------------------- /boot.js: -------------------------------------------------------------------------------- 1 | // This script will boot server.js with the number of workers 2 | // specified in WORKER_COUNT. 3 | // 4 | // The master will respond to SIGHUP, which will trigger 5 | // restarting all the workers and reloading the app. 6 | 7 | var cluster = require('cluster'); 8 | var workerCount = process.env.WORKER_COUNT || 4; 9 | 10 | // Defines what each worker needs to run 11 | cluster.setupMaster({ exec: 'server.js' }); 12 | 13 | // Gets the count of active workers 14 | function numWorkers() { return Object.keys(cluster.workers).length; } 15 | 16 | var stopping = false; 17 | 18 | // Forks off the workers unless the server is stopping 19 | function forkNewWorkers() { 20 | if (!stopping) { 21 | for (var i = numWorkers(); i < workerCount; i++) { cluster.fork(); } 22 | } 23 | } 24 | 25 | // A list of workers queued for a restart 26 | var workersToStop = []; 27 | 28 | // Stops a single worker 29 | // Gives 60 seconds after disconnect before SIGTERM 30 | function stopWorker(worker) { 31 | console.log('stopping', worker.process.pid); 32 | worker.disconnect(); 33 | var killTimer = setTimeout(function() { 34 | worker.kill(); 35 | }, 60000); 36 | 37 | // Ensure we don't stay up just for this setTimeout 38 | killTimer.unref(); 39 | } 40 | 41 | // Tell the next worker queued to restart to disconnect 42 | // This will allow the process to finish it's work 43 | // for 60 seconds before sending SIGTERM 44 | function stopNextWorker() { 45 | var i = workersToStop.pop(); 46 | var worker = cluster.workers[i]; 47 | if (worker) stopWorker(worker); 48 | } 49 | 50 | // Stops all the works at once 51 | function stopAllWorkers() { 52 | stopping = true; 53 | console.log('stopping all workers'); 54 | for (var id in cluster.workers) { 55 | stopWorker(cluster.workers[id]); 56 | } 57 | } 58 | 59 | // Worker is now listening on a port 60 | // Once it is ready, we can signal the next worker to restart 61 | cluster.on('listening', stopNextWorker); 62 | 63 | // A worker has disconnected either because the process was killed 64 | // or we are processing the workersToStop array restarting each process 65 | // In either case, we will fork any workers needed 66 | cluster.on('disconnect', forkNewWorkers); 67 | 68 | // HUP signal sent to the master process to start restarting all the workers sequentially 69 | process.on('SIGHUP', function() { 70 | console.log('restarting all workers'); 71 | workersToStop = Object.keys(cluster.workers); 72 | stopNextWorker(); 73 | }); 74 | 75 | // Kill all the workers at once 76 | process.on('SIGTERM', stopAllWorkers); 77 | 78 | // Fork off the initial workers 79 | forkNewWorkers(); 80 | console.log('app master', process.pid, 'booted'); 81 | -------------------------------------------------------------------------------- /config.ex: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | STORAGE_LOCATIONS:[ 3 | {"path":"./blocks1/"}, 4 | {"path":"./blocks2/"} 5 | ], 6 | BLOCK_SIZE: 1048576, 7 | LOG_LEVEL: 0, 8 | SERVER_PORT: 7302, 9 | REQUEST_TIMEOUT: 30, // minutes 10 | CONFIGURED_STORAGE: "fs", 11 | STATIC_ACCESS_KEYS: [] 12 | }; 13 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "BaseVolumeSz": "1g$ ops image list -t onprem", 3 | "Dirs": ["node_modules","lib","tools","blocks"], 4 | "Files": ["server.js","config.js","jlog-nomem.js"], 5 | "Args": ["server.js"], 6 | "CloudConfig": { 7 | "BucketName": "jsfs-unikernel-blocks", 8 | "Zone": "nyc3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | Future work. 4 | 5 | ## Update-to-date 6 | 7 | Bring the current implementation up-to-date in terms of code, structure, modularity, packaging and testing. 8 | 9 | > Seriously consider refactoring away from callback-oriented to async await or promises. 10 | 11 | Surface existing but undocumented (or unexplained) features such as the namespace, maintenance, advanced configuration. 12 | 13 | Make default configuration secure and private without increasing complexity. 14 | 15 | 16 | ## JSFS+S 17 | 18 | Complete websocket experiments and make the websocket interface part of the standard API. 19 | 20 | 21 | ## JSFS+SX 22 | 23 | Extend file storage to file execution. Supported file types stored with a new "execute" parameter set are executed by JSFS when requested by a client (akin to AWS Lambda or Google Cloud Functions). This allows applications to be built on JSFS that include both client and server code. 24 | 25 | 26 | ## Federated JSFS+SX 27 | 28 | Replace JSFS's inodes with a distributed hash table allowing a group of JSFS nodes to service requests for data and processing distributed across all nodes. 29 | 30 | When a client request is received, the receiving node fulfills the request on its own if possible, if not it transparently collects the requested data from other nodes and delivers it to the client. 31 | 32 | If execution is requested, JSFS can parallelize the task across multiple nodes if requested, potentially moving processing tasks to execute locally where the data to process is stored, minimizing the need to move unprocessed data across the network. 33 | 34 | Finally, the node contacted by the client caches any transfered data (raw or processed) to speed-up subsequent requests and provide data redundancy as well. 35 | 36 | Three protocols will be developed to support federation: metastore, blockstore and proc. 37 | 38 | ### Metastore 39 | 40 | This protocol is used to exchange file metadata in the form of a distributed hash table. 41 | 42 | ### Blockstore 43 | 44 | This protocol is used to transfer blocks of data between nodes in a federation. 45 | 46 | ### Proc 47 | 48 | This protocol is used to start, stop, migrate or otherwise interact with processes being run by clients across federated nodes. 49 | 50 | Full JSFS+X servers implement all three protocols as well as the HTTP API interface ised by clients. Utility or special-purpose servers can also participate in federation by implementing one or more of these protocols, and may be implemented in any technology capable of meeting the protocol specification. 51 | 52 | 53 | ## Local JSFS+SX 54 | 55 | To minimize network-induced latency, a local instance of JSFS is run in federation mode. As a result, applications run from the local instance and incur zero network latency after initial load. 56 | 57 | Processing also occurs locally unless the source data is not local or additional processing power is requested. 58 | 59 | Additionally, name resolution is handled by JSFS using the distributed hash table so the need and latency of DNS lookups are eliminated. 60 | 61 | In this mode, each user's machine serves as an node in the federation, eliminating the *need* for traditional servers (although such servers may still be used to provide redundancy or access to applications from clients that are unable to run a local server). 62 | -------------------------------------------------------------------------------- /docs/unikernel.md: -------------------------------------------------------------------------------- 1 | # Experimental unikernel build 2 | 3 | This is how to build & deploy a JSFS unikernel to Digital Ocean. To do this you'll need to first create an API token and a Spaces bucket. 4 | 5 | ## Setup 6 | 7 | ``` 8 | export DO_TOKEN= 9 | export SPACES_KEY= 10 | export SPACES_SECRET= 11 | 12 | sudo apt install qemu-utils 13 | ``` 14 | 15 | ## Build & Deploy 16 | ``` 17 | ops image create -c config.json --package eyberg/node:20.5.0 -i jsfs -t do -c config.json 18 | ``` 19 | 20 | Check to make sure it was built 21 | ``` 22 | ops image list 23 | ``` 24 | 25 | Create an *new* instance of the new image 26 | ``` 27 | ops instance create jsfs -t do -c config.json 28 | ``` 29 | 30 | > NOTE: if this is an upgrade to an existing instance, migrate data, update DNS and delete the old instance. 31 | > TODO: is there a way to upgrade an instance in-place with a new image? 32 | 33 | Check to make sure it was created 34 | ``` 35 | ops instance list -t do 36 | ``` 37 | 38 | 39 | ## References 40 | 41 | * https://docs.ops.city/ops/digital_ocean 42 | -------------------------------------------------------------------------------- /jlog-nomem.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | 3 | module.exports = { 4 | DEBUG: 0, 5 | INFO: 1, 6 | WARN: 2, 7 | ERROR: 3, 8 | level: 0, // default log level 9 | path: null, // default is, don't log to a file 10 | message: function(severity, log_message){ 11 | if(severity >= this.level){ 12 | console.log(Date() + "\t" + severity + "\t" + log_message); 13 | if(this.path){ 14 | fs.appendFile(this.path, Date() + "\t" + severity + "\t" + log_message + "\n", function(err){ 15 | if(err){ 16 | console.log("Error logging message to file: " + err); 17 | } 18 | }); 19 | } 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /jsfs.service: -------------------------------------------------------------------------------- 1 | [Service] 2 | WorkingDirectory=/var/www/jsfs 3 | ExecStart=/usr/local/bin/node boot.js 4 | ExecReload=/bin/kill -HUP $MAINPID 5 | Restart=always 6 | StandardOutput=syslog 7 | StandardError=syslog 8 | SyslogIdentifier=jsfs 9 | User=web 10 | Group=web 11 | Environment='NODE_ENV=production' 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* globals module */ 3 | 4 | module.exports.ALLOWED_METHODS = ["GET", 5 | "POST", 6 | "PUT", 7 | "DELETE", 8 | "OPTIONS"]; 9 | 10 | module.exports.ALLOWED_HEADERS = ["Accept", 11 | "Accept-Version", 12 | "Api-Version", 13 | "Content-Type", 14 | "Origin", 15 | "Range", 16 | "X_FILENAME", 17 | "X-Access-Key", 18 | "X-Access-Token", 19 | "X-Append", 20 | "X-Encrypted", 21 | "X-Private", 22 | "X-Replacement-Access-Key", 23 | "X-Requested-With"]; 24 | 25 | module.exports.EXPOSED_HEADERS = ["X-Media-Bitrate", 26 | "X-Media-Channels", 27 | "X-Media-Duration", 28 | "X-Media-Resolution", 29 | "X-Media-Size", 30 | "X-Media-Type"]; 31 | 32 | module.exports.ACCEPTED_PARAMS = [{"access_key": "x"}, 33 | {"access_token": "x"}, 34 | {"block_only": "x"}, 35 | {"content_type": undefined}, 36 | {"encrypted": "x"}, 37 | {"expires": "x"}, 38 | {"inode_only": "x"}, 39 | {"private": "x"}, 40 | {"replacement_access_key": "x"}, 41 | {"version": "x"}]; 42 | -------------------------------------------------------------------------------- /lib/file-types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* globals module */ 3 | 4 | /** 5 | 6 | Lookup WAVE format by chunk size. 7 | Chunk size of 16 === PCM (1) 8 | 9 | Chunk size of 40 === WAVE_FORMAT_EXTENSIBLE (65534) 10 | The WAVE_FORMAT_EXTENSIBLE format should be used whenever: 11 | PCM data has more than 16 bits/sample. 12 | The number of channels is more than 2. 13 | The actual number of bits/sample is not equal to the container size. 14 | The mapping from channels to speakers needs to be specified. 15 | We should probably do more finer-grained analysis of this format for determining duration, 16 | by examining any fact chunk between the fmt chunk and the data,but this should be enough for 17 | current use cases. 18 | 19 | Chunk size of 18 === non-PCM (3, 6, or 7) 20 | This could be IEEE float (3), 8-bit ITU-T G.711 A-law (6), 8-bit ITU-T G.711 µ-law (7), 21 | all of which probably require different calculations for duration and are not implemented 22 | 23 | Further reading: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html 24 | 25 | */ 26 | 27 | var WAVE_FMTS = { 28 | 16 : 1, 29 | 40 : 65534 30 | }; 31 | 32 | function format_from_block(block){ 33 | if (is_wave(block)) { 34 | return "wave"; 35 | } else { 36 | return "unknown"; 37 | } 38 | } 39 | 40 | function is_wave(block){ 41 | return block.toString("utf8", 0, 4) === "RIFF" && 42 | block.toString("utf8", 8, 12) === "WAVE" && 43 | WAVE_FMTS[block.readUInt32LE(16)] == block.readUInt16LE(20); 44 | } 45 | 46 | var _analyze_type = { 47 | wave: function(block, result){ 48 | 49 | var subchunk_byte = 36; 50 | var subchunk_id = block.toString("utf8", subchunk_byte, subchunk_byte+4); 51 | var block_length = block.length; 52 | 53 | while (subchunk_id !== 'data' && subchunk_byte < block_length) { 54 | // update start byte for subchunk by adding 55 | // the size of the subchunk + 8 for the id and size bytes (4 each) 56 | subchunk_byte = subchunk_byte + block.readUInt32LE(subchunk_byte+4) + 8; 57 | subchunk_id = block.toString("utf8", subchunk_byte, subchunk_byte+4); 58 | } 59 | 60 | var subchunk_size = block.readUInt32LE(subchunk_byte+4); 61 | var audio_data_size = subchunk_id === 'data' ? subchunk_size : result.size; 62 | 63 | result.type = "wave"; 64 | result.size = block.readUInt32LE(4); 65 | result.channels = block.readUInt16LE(22); 66 | result.bitrate = block.readUInt32LE(24); 67 | result.resolution = block.readUInt16LE(34); 68 | result.subchunk_id = subchunk_id; 69 | result.subchunk_byte = subchunk_byte; 70 | result.data_block_size = block.readUInt16LE(32); 71 | result.duration = (audio_data_size * 8) / (result.channels * result.resolution * result.bitrate); 72 | 73 | return result; 74 | 75 | }, 76 | 77 | unknown: function(block, result){ 78 | return result; 79 | } 80 | }; 81 | 82 | // examine the contents of a block to generate metadata 83 | module.exports.analyze = function(block){ 84 | var result = { type: format_from_block(block) }; 85 | 86 | return _analyze_type[result.type](block, result); 87 | 88 | }; 89 | -------------------------------------------------------------------------------- /lib/fs/disk-operations.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* globals require, module */ 3 | 4 | var fs = require("fs"); 5 | 6 | module.exports.exists = fs.stat; 7 | module.exports.read = fs.readFile; 8 | module.exports.stream_read = fs.createReadStream; 9 | module.exports.write = fs.writeFile; 10 | module.exports.delete = fs.unlink; 11 | -------------------------------------------------------------------------------- /lib/google-cloud-storage/disk-operations.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* globals require, module */ 3 | 4 | /** 5 | 6 | To use google-cloud-storage, you'll need to install it's dependency: 7 | 8 | `npm install --save @google-cloud/storage` 9 | 10 | and update config.js with additional parameters: 11 | 12 | CONFIGURED_STORAGE: "google-cloud-storage", 13 | GOOGLE_CLOUD_STORAGE: { 14 | BUCKET: "your-bucket-name", 15 | AUTHENTICATION: { 16 | projectId: 'your-project-123', 17 | keyFilename: '/path/to/keyfile.json' 18 | } 19 | } 20 | 21 | If you are running on a Google Compute Engine VM, you do not need to 22 | include AUTHENTICATION (eg. `config.GOOGLE_CLOUD_STORAGE.AUTHENTICATION` 23 | should return `undefined`). 24 | 25 | You will also need to be sure that the authenticated account has "full" 26 | permissions to the Storage API: 27 | https://cloud.google.com/storage/docs/access-control/iam 28 | 29 | Further information at 30 | https://github.com/GoogleCloudPlatform/google-cloud-node#google-cloud-storage-beta 31 | 32 | JSFS neither endorses nor is endorsed by Google. 33 | 34 | **/ 35 | 36 | var config = require("../../config.js"); 37 | var gcs = require("@google-cloud/storage")(config.GOOGLE_CLOUD_STORAGE.AUTHENTICATION); 38 | var bucket = gcs.bucket(config.GOOGLE_CLOUD_STORAGE.BUCKET); 39 | 40 | module.exports.read = function(file_path, callback){ 41 | return bucket.file(file_path).download(callback); 42 | }; 43 | 44 | module.exports.exists = function(file_path, callback){ 45 | return bucket.file(file_path).getMetadata(callback); 46 | }; 47 | 48 | module.exports.stream_read = function(file_path){ 49 | return bucket.file(file_path).createReadStream(); 50 | }; 51 | 52 | module.exports.write = function(file_path, contents /*[, options], cb */){ 53 | var args = Array.prototype.slice.call(arguments); 54 | var callback = args.pop(); 55 | 56 | return bucket.file(file_path).save(contents, callback); 57 | }; 58 | 59 | module.exports.delete = function(file_path, callback){ 60 | return bucket.file(file_path).delete(callback); 61 | }; 62 | -------------------------------------------------------------------------------- /lib/inode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* globals require, module, Buffer */ 3 | 4 | var config = require("../config.js"); 5 | var log = require("../jlog-nomem.js"); 6 | var utils = require("./utils.js"); 7 | var file_types = require("./file-types.js"); 8 | 9 | // global to keep track of storage location rotation 10 | var next_storage_location = 0; 11 | 12 | var Inode = { 13 | init: function(url){ 14 | this.input_buffer = new Buffer(""); 15 | this.block_size = config.BLOCK_SIZE; 16 | this.file_metadata = {}; 17 | this.file_metadata.url = url; 18 | this.file_metadata.created = (new Date()).getTime(); 19 | this.file_metadata.version = 0; 20 | this.file_metadata.private = false; 21 | this.file_metadata.encrypted = false; 22 | this.file_metadata.fingerprint = null; 23 | this.file_metadata.access_key = null; 24 | this.file_metadata.content_type = "application/octet-stream"; 25 | this.file_metadata.file_size = 0; 26 | this.file_metadata.block_size = this.block_size; 27 | this.file_metadata.blocks_replicated = 0; 28 | this.file_metadata.inode_replicated = 0; 29 | this.file_metadata.blocks = []; 30 | 31 | // create fingerprint to uniquely identify this file 32 | this.file_metadata.fingerprint = utils.sha1_to_hex(this.file_metadata.url); 33 | 34 | // use fingerprint as default key 35 | this.file_metadata.access_key = this.file_metadata.fingerprint; 36 | }, 37 | write: function(chunk, req, callback){ 38 | this.input_buffer = new Buffer.concat([this.input_buffer, chunk]); 39 | if (this.input_buffer.length > this.block_size) { 40 | req.pause(); 41 | this.process_buffer(false, function(result){ 42 | req.resume(); 43 | callback(result); 44 | }); 45 | } else { 46 | callback(true); 47 | } 48 | }, 49 | close: function(callback){ 50 | var self = this; 51 | log.message(0, "flushing remaining buffer"); 52 | // update original file size 53 | self.file_metadata.file_size = self.file_metadata.file_size + self.input_buffer.length; 54 | 55 | self.process_buffer(true, function(result){ 56 | if(result){ 57 | // write inode to disk 58 | utils.save_inode(self.file_metadata, callback); 59 | } 60 | }); 61 | }, 62 | process_buffer: function(flush, callback){ 63 | var self = this; 64 | var total = flush ? 0 : self.block_size; 65 | this.store_block(!flush, function(err/*, result*/){ 66 | if (err) { 67 | log.message(log.DEBUG, "process_buffer result: " + err); 68 | return callback(false); 69 | } 70 | 71 | if (self.input_buffer.length > total) { 72 | self.process_buffer(flush, callback); 73 | } else { 74 | callback(true); 75 | } 76 | 77 | }); 78 | }, 79 | store_block: function(update_file_size, callback){ 80 | var self = this; 81 | var chunk_size = this.block_size; 82 | 83 | // grab the next block 84 | var block = this.input_buffer.slice(0, chunk_size); 85 | 86 | if(this.file_metadata.blocks.length === 0){ 87 | 88 | // grok known file types 89 | var analysis_result = file_types.analyze(block); 90 | 91 | log.message(log.INFO, "block analysis result: " + JSON.stringify(analysis_result)); 92 | 93 | // if we found out anything useful, annotate the object's metadata 94 | this.file_metadata.media_type = analysis_result.type; 95 | if(analysis_result.type != "unknown"){ 96 | this.file_metadata.media_size = analysis_result.size; 97 | this.file_metadata.media_channels = analysis_result.channels; 98 | this.file_metadata.media_bitrate = analysis_result.bitrate; 99 | this.file_metadata.media_resolution = analysis_result.resolution; 100 | this.file_metadata.media_duration = analysis_result.duration; 101 | } 102 | 103 | if (analysis_result.type === "wave") { 104 | chunk_size = utils.wave_audio_offset(block, analysis_result) 105 | block = block.slice(0, chunk_size); 106 | } 107 | } 108 | 109 | // if encryption is set, encrypt using the hash above 110 | if(this.file_metadata.encrypted && this.file_metadata.access_key){ 111 | log.message(log.INFO, "encrypting block"); 112 | block = utils.encrypt(block, this.file_metadata.access_key); 113 | } else { 114 | 115 | // if even one block can't be encrypted, say so and stop trying 116 | this.file_metadata.encrypted = false; 117 | } 118 | 119 | // store the block 120 | var block_object = {}; 121 | 122 | // generate a hash of the block to use as a handle/filename 123 | block_object.block_hash = utils.sha1_to_hex(block); 124 | 125 | utils.commit_block_to_disk(block, block_object, next_storage_location, function(err, result){ 126 | if (err) { 127 | return callback(err); 128 | } 129 | 130 | // increment (or reset) storage location (striping) 131 | next_storage_location++; 132 | if(next_storage_location === config.STORAGE_LOCATIONS.length){ 133 | next_storage_location = 0; 134 | } 135 | 136 | // update inode 137 | self.file_metadata.blocks.push(result); 138 | 139 | // update original file size 140 | // we need to update filesize here due to truncation at the front, 141 | // but need the check to avoid double setting during flush 142 | // is there a better way? 143 | if (update_file_size) { 144 | self.file_metadata.file_size = self.file_metadata.file_size + chunk_size; 145 | } 146 | 147 | // advance buffer 148 | self.input_buffer = self.input_buffer.slice(chunk_size); 149 | return callback(null, result); 150 | }); 151 | } 152 | }; 153 | 154 | module.exports = Inode; 155 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* globals require, module */ 3 | 4 | var crypto = require("crypto"); 5 | var path = require("path"); 6 | var url = require("url"); 7 | var log = require("../jlog-nomem.js"); 8 | var config = require("../config.js"); 9 | var operations = require("./" + (config.CONFIGURED_STORAGE || "fs") + "/disk-operations.js"); 10 | 11 | var TOTAL_LOCATIONS = config.STORAGE_LOCATIONS.length; 12 | 13 | // simple encrypt-decrypt functions 14 | module.exports.encrypt = function encrypt(data, key){ 15 | var cipher = crypto.createCipher("aes-256-cbc", key); 16 | cipher.write(data); 17 | cipher.end(); 18 | return cipher.read(); 19 | }; 20 | 21 | var sha1_to_hex = function sha1_to_hex(data){ 22 | var shasum = crypto.createHash("sha1"); 23 | shasum.update(data); 24 | return shasum.digest("hex"); 25 | }; 26 | module.exports.sha1_to_hex = sha1_to_hex; 27 | 28 | // save inode to disk 29 | module.exports.save_inode = function save_inode(inode, callback){ 30 | var accessed_locations = 0; 31 | 32 | var _cb = function _cb(error){ 33 | accessed_locations++; 34 | if(error){ 35 | log.message(log.ERROR, "Error saving inode: " + error); 36 | } else { 37 | log.message(log.INFO, "Inode saved to disk"); 38 | } 39 | if (accessed_locations === TOTAL_LOCATIONS) { 40 | return callback(inode); 41 | } 42 | }; 43 | 44 | // store a copy of each inode in each storage location for redundancy 45 | for(var storage_location in config.STORAGE_LOCATIONS){ 46 | var selected_location = config.STORAGE_LOCATIONS[storage_location]; 47 | operations.write(path.join(selected_location.path, inode.fingerprint + ".json"), JSON.stringify(inode), _cb); 48 | } 49 | }; 50 | 51 | // load inode from disk 52 | module.exports.load_inode = function load_inode(uri, callback){ 53 | log.message(log.DEBUG, "uri: " + uri); 54 | 55 | // calculate fingerprint 56 | var inode_fingerprint = sha1_to_hex(uri); 57 | 58 | var _load_inode = function _load_inode(idx){ 59 | var selected_path = config.STORAGE_LOCATIONS[idx].path; 60 | log.message(log.DEBUG, "Loading inode from " + selected_path); 61 | 62 | operations.read(path.join(selected_path, inode_fingerprint + ".json"), function(err, data){ 63 | idx++; 64 | if (err) { 65 | if (idx === TOTAL_LOCATIONS) { 66 | log.message(log.WARN, "Unable to load inode for requested URL: " + uri); 67 | return callback(err); 68 | } else { 69 | log.message(log.DEBUG, "Error loading inode from " + selected_path); 70 | return _load_inode(idx); 71 | } 72 | } 73 | 74 | try { 75 | var inode = JSON.parse(data); 76 | log.message(log.INFO, "Inode loaded from " + selected_path); 77 | return callback(null, inode); 78 | } catch(ex) { 79 | if (idx === TOTAL_LOCATIONS) { 80 | log.message(log.WARN, "Unable to parse inode for requested URL: " + uri); 81 | return callback(ex); 82 | } else { 83 | log.message(log.DEBUG, "Error parsing inode from " + selected_path); 84 | return _load_inode(idx); 85 | } 86 | } 87 | }); 88 | }; 89 | 90 | _load_inode(0); 91 | }; 92 | 93 | module.exports.commit_block_to_disk = function commit_block_to_disk(block, block_object, next_storage_location, callback) { 94 | // if storage locations exist, save the block to disk 95 | var total_locations = config.STORAGE_LOCATIONS.length; 96 | 97 | if(total_locations > 0){ 98 | 99 | // check all storage locations to see if we already have this block 100 | 101 | var on_complete = function on_complete(found_block){ 102 | // TODO: consider increasing found count to enable block redundancy 103 | if(!found_block){ 104 | 105 | // write new block to next storage location 106 | // TODO: consider implementing in-band compression here 107 | var dir = config.STORAGE_LOCATIONS[next_storage_location].path; 108 | operations.write(dir + block_object.block_hash, block, "binary", function(err){ 109 | if (err) { 110 | return callback(err); 111 | } 112 | 113 | block_object.last_seen = dir; 114 | log.message(log.INFO, "New block " + block_object.block_hash + " written to " + dir); 115 | 116 | return callback(null, block_object); 117 | 118 | }); 119 | 120 | } else { 121 | log.message(log.INFO, "Duplicate block " + block_object.block_hash + " not written to disk"); 122 | return callback(null, block_object); 123 | } 124 | }; 125 | 126 | var locate_block = function locate_block(idx){ 127 | var location = config.STORAGE_LOCATIONS[idx]; 128 | var file = location.path + block_object.block_hash; 129 | idx++; 130 | 131 | operations.exists(file + ".gz", function(err, result){ 132 | 133 | if (result) { 134 | log.message(log.INFO, "Duplicate compressed block " + block_object.block_hash + " found in " + location.path); 135 | block_object.last_seen = location.path; 136 | return on_complete(true); 137 | } else { 138 | operations.exists(file, function(err_2, result_2){ 139 | 140 | if (err_2) { 141 | log.message(log.INFO, "Block " + block_object.block_hash + " not found in " + location.path); 142 | } 143 | 144 | if (result_2) { 145 | log.message(log.INFO, "Duplicate block " + block_object.block_hash + " found in " + location.path); 146 | block_object.last_seen = location.path; 147 | return on_complete(true); 148 | } else { 149 | if (idx >= total_locations) { 150 | return on_complete(false); 151 | } else { 152 | locate_block(idx); 153 | } 154 | } 155 | }); 156 | } 157 | }); 158 | }; 159 | 160 | locate_block(0); 161 | 162 | } else { 163 | log.message(log.WARN, "No storage locations configured, block not written to disk"); 164 | return callback(null, block_object); 165 | } 166 | }; 167 | 168 | // Use analyze data to identify offset until non-zero audio, grab just that portion to store. 169 | 170 | // In analyze we identified the "data" starting byte and block_align ((Bit Size * Channels) / 8) 171 | // We'll start the scan at block.readUInt32LE([data chunk offset] + 8) in order to find the 172 | // start of non-zero audio data, and slice off everything before that point as a seperate block. 173 | // That way we can deduplicate tracks with slightly different silent leads. 174 | module.exports.wave_audio_offset = function wave_audio_offset(block, data, default_size){ 175 | // block_align most likely to be 4, but it'd be nice to handle alternate cases. 176 | // Essentially, we should use block["readUInt" + (block_align * 8) + "LE"]() to scan the block. 177 | var block_align = data.data_block_size; 178 | 179 | if (data.subchunk_id === "data" && block_align === 4) { 180 | // start of audio subchunk + 4 bytes for the label ("data") + 4 bytes for size) 181 | var data_offset = data.subchunk_byte + 4 + 4; 182 | var block_length = block.length; 183 | 184 | // Increment our offset by block_align, since we're analyzing on the basis of it. 185 | for (data_offset; (data_offset + block_align) < block_length; data_offset = data_offset + block_align) { 186 | if (block.readUInt32LE(data_offset) !== 0) { 187 | log.message(log.INFO, "Storing the first " + data_offset + " bytes seperately"); 188 | // return the offset with first non-zero audio data; 189 | return data_offset; 190 | } 191 | } 192 | // if we didn't return out of the for loop, return default 193 | return default_size; 194 | 195 | } else { 196 | // If we didn't find a data chunk, return default 197 | return default_size; 198 | } 199 | }; 200 | 201 | function dasherize(s){ 202 | return s.replace(/_/g, '-'); 203 | } 204 | 205 | function to_header(param, obj){ 206 | var prefix = obj[param]; 207 | return (prefix ? prefix + "-" : "") + dasherize(param); 208 | } 209 | 210 | module.exports.request_parameters = function request_parameters(accepted_params, uri, headers){ 211 | var q = url.parse(uri, true).query; 212 | 213 | return accepted_params.reduce(function(o,p){ 214 | var _p = Object.keys(p)[0]; 215 | o[_p] = q[_p] || headers[to_header(_p, p)]; 216 | return o; 217 | }, {}); 218 | }; 219 | 220 | module.exports.target_from_url = function target_from_url(hostname, uri) { 221 | var parsed = url.parse(uri); 222 | var pathname = parsed.pathname; 223 | hostname = hostname.split(":")[0]; 224 | 225 | if (pathname.substring(0,2) !== "/.") { 226 | return "/" + hostname.split(".").reverse().join(".") + pathname; 227 | } else { 228 | return "/" + pathname.substring(2); 229 | } 230 | }; 231 | -------------------------------------------------------------------------------- /lib/validate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* globals require, module */ 3 | 4 | var log = require("../jlog-nomem.js"); 5 | var utils = require("./utils.js"); 6 | 7 | var time_valid = function time_valid(expires){ 8 | return !expires || ( expires >= new Date().getTime() ); 9 | }; 10 | 11 | var expected_token = function expected_token(inode, method, params) { 12 | var expected_token = utils.sha1_to_hex(inode.access_key + method + (params.expires || "")); 13 | 14 | log.message(log.DEBUG,"expected_token: " + expected_token); 15 | log.message(log.DEBUG,"access_token: " + params.access_token); 16 | 17 | return expected_token === params.access_token; 18 | }; 19 | 20 | var token_valid = function token_valid(inode, method, params){ 21 | // don't bother validating tokens for HEAD, OPTIONS requests 22 | // jjg - 08172015: might make sense to address this by removing the check from 23 | // the method handlers below, but since I'm not sure if this is 24 | // permanent, this is cleaner for now 25 | 26 | return !!params.access_token && 27 | (( method === "HEAD" || method === "OPTIONS" ) || 28 | ( time_valid(params.expires) && expected_token.apply(null, Array.prototype.slice.call(arguments)) )); 29 | }; 30 | 31 | var has_key = function has_key(inode, params){ 32 | return params.access_key && params.access_key === inode.access_key; 33 | }; 34 | module.exports.has_key = has_key; 35 | 36 | module.exports.is_authorized = function is_authorized(inode, method, params){ 37 | return has_key(inode, params) || 38 | token_valid.apply(null, Array.prototype.slice.call(arguments)); 39 | }; 40 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jason J. Gullickson. 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. 10 | 11 | -------------------------------------------------------------------------------- /old-jlog.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | 3 | module.exports = { 4 | DEBUG: 0, 5 | INFO: 1, 6 | WARN: 2, 7 | ERROR: 3, 8 | level: 0, // default log level 9 | path: null, // default is, don't log to a file 10 | message: function(severity, log_message){ 11 | if(severity >= this.level){ 12 | console.log(Date() + "\t" + Math.round((process.memoryUsage().rss/1024)/1024) + "MB\t" + severity + "\t" + log_message); 13 | if(this.path){ 14 | fs.appendFile(this.path, Date() + "\t" + Math.round((process.memoryUsage().rss/1024)/1024) + "MB\t" + severity + "\t" + log_message + "\n", function(err){ 15 | if(err){ 16 | console.log("Error logging message to file: " + err); 17 | } 18 | }); 19 | } 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsfs", 3 | "version": "4.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "jsfs", 9 | "version": "4.2.0", 10 | "license": "GPL-3.0", 11 | "dependencies": { 12 | "through": "2.3.8" 13 | }, 14 | "devDependencies": { 15 | "chai": "^3.5.0", 16 | "mocha": "^9.1.3", 17 | "mock-fs": "^4.0.0-beta.1" 18 | } 19 | }, 20 | "node_modules/@ungap/promise-all-settled": { 21 | "version": "1.1.2", 22 | "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", 23 | "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", 24 | "dev": true 25 | }, 26 | "node_modules/ansi-colors": { 27 | "version": "4.1.1", 28 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 29 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 30 | "dev": true, 31 | "engines": { 32 | "node": ">=6" 33 | } 34 | }, 35 | "node_modules/ansi-regex": { 36 | "version": "5.0.1", 37 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 38 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 39 | "dev": true, 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | }, 44 | "node_modules/ansi-styles": { 45 | "version": "4.3.0", 46 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 47 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 48 | "dev": true, 49 | "dependencies": { 50 | "color-convert": "^2.0.1" 51 | }, 52 | "engines": { 53 | "node": ">=8" 54 | }, 55 | "funding": { 56 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 57 | } 58 | }, 59 | "node_modules/anymatch": { 60 | "version": "3.1.2", 61 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 62 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 63 | "dev": true, 64 | "dependencies": { 65 | "normalize-path": "^3.0.0", 66 | "picomatch": "^2.0.4" 67 | }, 68 | "engines": { 69 | "node": ">= 8" 70 | } 71 | }, 72 | "node_modules/argparse": { 73 | "version": "2.0.1", 74 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 75 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 76 | "dev": true 77 | }, 78 | "node_modules/assertion-error": { 79 | "version": "1.1.0", 80 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 81 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 82 | "dev": true, 83 | "engines": { 84 | "node": "*" 85 | } 86 | }, 87 | "node_modules/balanced-match": { 88 | "version": "1.0.2", 89 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 90 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 91 | "dev": true 92 | }, 93 | "node_modules/binary-extensions": { 94 | "version": "2.2.0", 95 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 96 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 97 | "dev": true, 98 | "engines": { 99 | "node": ">=8" 100 | } 101 | }, 102 | "node_modules/brace-expansion": { 103 | "version": "1.1.11", 104 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 105 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 106 | "dev": true, 107 | "dependencies": { 108 | "balanced-match": "^1.0.0", 109 | "concat-map": "0.0.1" 110 | } 111 | }, 112 | "node_modules/braces": { 113 | "version": "3.0.2", 114 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 115 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 116 | "dev": true, 117 | "dependencies": { 118 | "fill-range": "^7.0.1" 119 | }, 120 | "engines": { 121 | "node": ">=8" 122 | } 123 | }, 124 | "node_modules/browser-stdout": { 125 | "version": "1.3.1", 126 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 127 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 128 | "dev": true 129 | }, 130 | "node_modules/camelcase": { 131 | "version": "6.2.1", 132 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", 133 | "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", 134 | "dev": true, 135 | "engines": { 136 | "node": ">=10" 137 | }, 138 | "funding": { 139 | "url": "https://github.com/sponsors/sindresorhus" 140 | } 141 | }, 142 | "node_modules/chai": { 143 | "version": "3.5.0", 144 | "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", 145 | "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", 146 | "dev": true, 147 | "dependencies": { 148 | "assertion-error": "^1.0.1", 149 | "deep-eql": "^0.1.3", 150 | "type-detect": "^1.0.0" 151 | }, 152 | "engines": { 153 | "node": ">= 0.4.0" 154 | } 155 | }, 156 | "node_modules/chalk": { 157 | "version": "4.1.2", 158 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 159 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 160 | "dev": true, 161 | "dependencies": { 162 | "ansi-styles": "^4.1.0", 163 | "supports-color": "^7.1.0" 164 | }, 165 | "engines": { 166 | "node": ">=10" 167 | }, 168 | "funding": { 169 | "url": "https://github.com/chalk/chalk?sponsor=1" 170 | } 171 | }, 172 | "node_modules/chalk/node_modules/supports-color": { 173 | "version": "7.2.0", 174 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 175 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 176 | "dev": true, 177 | "dependencies": { 178 | "has-flag": "^4.0.0" 179 | }, 180 | "engines": { 181 | "node": ">=8" 182 | } 183 | }, 184 | "node_modules/chokidar": { 185 | "version": "3.5.2", 186 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 187 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 188 | "dev": true, 189 | "dependencies": { 190 | "anymatch": "~3.1.2", 191 | "braces": "~3.0.2", 192 | "glob-parent": "~5.1.2", 193 | "is-binary-path": "~2.1.0", 194 | "is-glob": "~4.0.1", 195 | "normalize-path": "~3.0.0", 196 | "readdirp": "~3.6.0" 197 | }, 198 | "engines": { 199 | "node": ">= 8.10.0" 200 | }, 201 | "optionalDependencies": { 202 | "fsevents": "~2.3.2" 203 | } 204 | }, 205 | "node_modules/cliui": { 206 | "version": "7.0.4", 207 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 208 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 209 | "dev": true, 210 | "dependencies": { 211 | "string-width": "^4.2.0", 212 | "strip-ansi": "^6.0.0", 213 | "wrap-ansi": "^7.0.0" 214 | } 215 | }, 216 | "node_modules/color-convert": { 217 | "version": "2.0.1", 218 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 219 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 220 | "dev": true, 221 | "dependencies": { 222 | "color-name": "~1.1.4" 223 | }, 224 | "engines": { 225 | "node": ">=7.0.0" 226 | } 227 | }, 228 | "node_modules/color-name": { 229 | "version": "1.1.4", 230 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 231 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 232 | "dev": true 233 | }, 234 | "node_modules/concat-map": { 235 | "version": "0.0.1", 236 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 237 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 238 | "dev": true 239 | }, 240 | "node_modules/debug": { 241 | "version": "4.3.2", 242 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", 243 | "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", 244 | "dev": true, 245 | "dependencies": { 246 | "ms": "2.1.2" 247 | }, 248 | "engines": { 249 | "node": ">=6.0" 250 | }, 251 | "peerDependenciesMeta": { 252 | "supports-color": { 253 | "optional": true 254 | } 255 | } 256 | }, 257 | "node_modules/debug/node_modules/ms": { 258 | "version": "2.1.2", 259 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 260 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 261 | "dev": true 262 | }, 263 | "node_modules/decamelize": { 264 | "version": "4.0.0", 265 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 266 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 267 | "dev": true, 268 | "engines": { 269 | "node": ">=10" 270 | }, 271 | "funding": { 272 | "url": "https://github.com/sponsors/sindresorhus" 273 | } 274 | }, 275 | "node_modules/deep-eql": { 276 | "version": "0.1.3", 277 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", 278 | "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", 279 | "dev": true, 280 | "dependencies": { 281 | "type-detect": "0.1.1" 282 | }, 283 | "engines": { 284 | "node": "*" 285 | } 286 | }, 287 | "node_modules/deep-eql/node_modules/type-detect": { 288 | "version": "0.1.1", 289 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", 290 | "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", 291 | "dev": true, 292 | "engines": { 293 | "node": "*" 294 | } 295 | }, 296 | "node_modules/diff": { 297 | "version": "5.0.0", 298 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 299 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 300 | "dev": true, 301 | "engines": { 302 | "node": ">=0.3.1" 303 | } 304 | }, 305 | "node_modules/emoji-regex": { 306 | "version": "8.0.0", 307 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 308 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 309 | "dev": true 310 | }, 311 | "node_modules/escalade": { 312 | "version": "3.1.1", 313 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 314 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 315 | "dev": true, 316 | "engines": { 317 | "node": ">=6" 318 | } 319 | }, 320 | "node_modules/escape-string-regexp": { 321 | "version": "4.0.0", 322 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 323 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 324 | "dev": true, 325 | "engines": { 326 | "node": ">=10" 327 | }, 328 | "funding": { 329 | "url": "https://github.com/sponsors/sindresorhus" 330 | } 331 | }, 332 | "node_modules/fill-range": { 333 | "version": "7.0.1", 334 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 335 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 336 | "dev": true, 337 | "dependencies": { 338 | "to-regex-range": "^5.0.1" 339 | }, 340 | "engines": { 341 | "node": ">=8" 342 | } 343 | }, 344 | "node_modules/find-up": { 345 | "version": "5.0.0", 346 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 347 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 348 | "dev": true, 349 | "dependencies": { 350 | "locate-path": "^6.0.0", 351 | "path-exists": "^4.0.0" 352 | }, 353 | "engines": { 354 | "node": ">=10" 355 | }, 356 | "funding": { 357 | "url": "https://github.com/sponsors/sindresorhus" 358 | } 359 | }, 360 | "node_modules/flat": { 361 | "version": "5.0.2", 362 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 363 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 364 | "dev": true, 365 | "bin": { 366 | "flat": "cli.js" 367 | } 368 | }, 369 | "node_modules/fs.realpath": { 370 | "version": "1.0.0", 371 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 372 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 373 | "dev": true 374 | }, 375 | "node_modules/fsevents": { 376 | "version": "2.3.2", 377 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 378 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 379 | "dev": true, 380 | "hasInstallScript": true, 381 | "optional": true, 382 | "os": [ 383 | "darwin" 384 | ], 385 | "engines": { 386 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 387 | } 388 | }, 389 | "node_modules/get-caller-file": { 390 | "version": "2.0.5", 391 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 392 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 393 | "dev": true, 394 | "engines": { 395 | "node": "6.* || 8.* || >= 10.*" 396 | } 397 | }, 398 | "node_modules/glob": { 399 | "version": "7.1.7", 400 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 401 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 402 | "dev": true, 403 | "dependencies": { 404 | "fs.realpath": "^1.0.0", 405 | "inflight": "^1.0.4", 406 | "inherits": "2", 407 | "minimatch": "^3.0.4", 408 | "once": "^1.3.0", 409 | "path-is-absolute": "^1.0.0" 410 | }, 411 | "engines": { 412 | "node": "*" 413 | }, 414 | "funding": { 415 | "url": "https://github.com/sponsors/isaacs" 416 | } 417 | }, 418 | "node_modules/glob-parent": { 419 | "version": "5.1.2", 420 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 421 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 422 | "dev": true, 423 | "dependencies": { 424 | "is-glob": "^4.0.1" 425 | }, 426 | "engines": { 427 | "node": ">= 6" 428 | } 429 | }, 430 | "node_modules/growl": { 431 | "version": "1.10.5", 432 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 433 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 434 | "dev": true, 435 | "engines": { 436 | "node": ">=4.x" 437 | } 438 | }, 439 | "node_modules/has-flag": { 440 | "version": "4.0.0", 441 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 442 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 443 | "dev": true, 444 | "engines": { 445 | "node": ">=8" 446 | } 447 | }, 448 | "node_modules/he": { 449 | "version": "1.2.0", 450 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 451 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 452 | "dev": true, 453 | "bin": { 454 | "he": "bin/he" 455 | } 456 | }, 457 | "node_modules/inflight": { 458 | "version": "1.0.6", 459 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 460 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 461 | "dev": true, 462 | "dependencies": { 463 | "once": "^1.3.0", 464 | "wrappy": "1" 465 | } 466 | }, 467 | "node_modules/inherits": { 468 | "version": "2.0.4", 469 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 470 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 471 | "dev": true 472 | }, 473 | "node_modules/is-binary-path": { 474 | "version": "2.1.0", 475 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 476 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 477 | "dev": true, 478 | "dependencies": { 479 | "binary-extensions": "^2.0.0" 480 | }, 481 | "engines": { 482 | "node": ">=8" 483 | } 484 | }, 485 | "node_modules/is-extglob": { 486 | "version": "2.1.1", 487 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 488 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 489 | "dev": true, 490 | "engines": { 491 | "node": ">=0.10.0" 492 | } 493 | }, 494 | "node_modules/is-fullwidth-code-point": { 495 | "version": "3.0.0", 496 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 497 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 498 | "dev": true, 499 | "engines": { 500 | "node": ">=8" 501 | } 502 | }, 503 | "node_modules/is-glob": { 504 | "version": "4.0.3", 505 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 506 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 507 | "dev": true, 508 | "dependencies": { 509 | "is-extglob": "^2.1.1" 510 | }, 511 | "engines": { 512 | "node": ">=0.10.0" 513 | } 514 | }, 515 | "node_modules/is-number": { 516 | "version": "7.0.0", 517 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 518 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 519 | "dev": true, 520 | "engines": { 521 | "node": ">=0.12.0" 522 | } 523 | }, 524 | "node_modules/is-plain-obj": { 525 | "version": "2.1.0", 526 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 527 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 528 | "dev": true, 529 | "engines": { 530 | "node": ">=8" 531 | } 532 | }, 533 | "node_modules/is-unicode-supported": { 534 | "version": "0.1.0", 535 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 536 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 537 | "dev": true, 538 | "engines": { 539 | "node": ">=10" 540 | }, 541 | "funding": { 542 | "url": "https://github.com/sponsors/sindresorhus" 543 | } 544 | }, 545 | "node_modules/isexe": { 546 | "version": "2.0.0", 547 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 548 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 549 | "dev": true 550 | }, 551 | "node_modules/js-yaml": { 552 | "version": "4.1.0", 553 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 554 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 555 | "dev": true, 556 | "dependencies": { 557 | "argparse": "^2.0.1" 558 | }, 559 | "bin": { 560 | "js-yaml": "bin/js-yaml.js" 561 | } 562 | }, 563 | "node_modules/locate-path": { 564 | "version": "6.0.0", 565 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 566 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 567 | "dev": true, 568 | "dependencies": { 569 | "p-locate": "^5.0.0" 570 | }, 571 | "engines": { 572 | "node": ">=10" 573 | }, 574 | "funding": { 575 | "url": "https://github.com/sponsors/sindresorhus" 576 | } 577 | }, 578 | "node_modules/log-symbols": { 579 | "version": "4.1.0", 580 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 581 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 582 | "dev": true, 583 | "dependencies": { 584 | "chalk": "^4.1.0", 585 | "is-unicode-supported": "^0.1.0" 586 | }, 587 | "engines": { 588 | "node": ">=10" 589 | }, 590 | "funding": { 591 | "url": "https://github.com/sponsors/sindresorhus" 592 | } 593 | }, 594 | "node_modules/minimatch": { 595 | "version": "3.0.4", 596 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 597 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 598 | "dev": true, 599 | "dependencies": { 600 | "brace-expansion": "^1.1.7" 601 | }, 602 | "engines": { 603 | "node": "*" 604 | } 605 | }, 606 | "node_modules/mocha": { 607 | "version": "9.1.3", 608 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", 609 | "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", 610 | "dev": true, 611 | "dependencies": { 612 | "@ungap/promise-all-settled": "1.1.2", 613 | "ansi-colors": "4.1.1", 614 | "browser-stdout": "1.3.1", 615 | "chokidar": "3.5.2", 616 | "debug": "4.3.2", 617 | "diff": "5.0.0", 618 | "escape-string-regexp": "4.0.0", 619 | "find-up": "5.0.0", 620 | "glob": "7.1.7", 621 | "growl": "1.10.5", 622 | "he": "1.2.0", 623 | "js-yaml": "4.1.0", 624 | "log-symbols": "4.1.0", 625 | "minimatch": "3.0.4", 626 | "ms": "2.1.3", 627 | "nanoid": "3.1.25", 628 | "serialize-javascript": "6.0.0", 629 | "strip-json-comments": "3.1.1", 630 | "supports-color": "8.1.1", 631 | "which": "2.0.2", 632 | "workerpool": "6.1.5", 633 | "yargs": "16.2.0", 634 | "yargs-parser": "20.2.4", 635 | "yargs-unparser": "2.0.0" 636 | }, 637 | "bin": { 638 | "_mocha": "bin/_mocha", 639 | "mocha": "bin/mocha" 640 | }, 641 | "engines": { 642 | "node": ">= 12.0.0" 643 | }, 644 | "funding": { 645 | "type": "opencollective", 646 | "url": "https://opencollective.com/mochajs" 647 | } 648 | }, 649 | "node_modules/mock-fs": { 650 | "version": "4.14.0", 651 | "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.14.0.tgz", 652 | "integrity": "sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw==", 653 | "dev": true 654 | }, 655 | "node_modules/ms": { 656 | "version": "2.1.3", 657 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 658 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 659 | "dev": true 660 | }, 661 | "node_modules/nanoid": { 662 | "version": "3.1.25", 663 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", 664 | "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", 665 | "dev": true, 666 | "bin": { 667 | "nanoid": "bin/nanoid.cjs" 668 | }, 669 | "engines": { 670 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 671 | } 672 | }, 673 | "node_modules/normalize-path": { 674 | "version": "3.0.0", 675 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 676 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 677 | "dev": true, 678 | "engines": { 679 | "node": ">=0.10.0" 680 | } 681 | }, 682 | "node_modules/once": { 683 | "version": "1.4.0", 684 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 685 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 686 | "dev": true, 687 | "dependencies": { 688 | "wrappy": "1" 689 | } 690 | }, 691 | "node_modules/p-limit": { 692 | "version": "3.1.0", 693 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 694 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 695 | "dev": true, 696 | "dependencies": { 697 | "yocto-queue": "^0.1.0" 698 | }, 699 | "engines": { 700 | "node": ">=10" 701 | }, 702 | "funding": { 703 | "url": "https://github.com/sponsors/sindresorhus" 704 | } 705 | }, 706 | "node_modules/p-locate": { 707 | "version": "5.0.0", 708 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 709 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 710 | "dev": true, 711 | "dependencies": { 712 | "p-limit": "^3.0.2" 713 | }, 714 | "engines": { 715 | "node": ">=10" 716 | }, 717 | "funding": { 718 | "url": "https://github.com/sponsors/sindresorhus" 719 | } 720 | }, 721 | "node_modules/path-exists": { 722 | "version": "4.0.0", 723 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 724 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 725 | "dev": true, 726 | "engines": { 727 | "node": ">=8" 728 | } 729 | }, 730 | "node_modules/path-is-absolute": { 731 | "version": "1.0.1", 732 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 733 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 734 | "dev": true, 735 | "engines": { 736 | "node": ">=0.10.0" 737 | } 738 | }, 739 | "node_modules/picomatch": { 740 | "version": "2.3.0", 741 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 742 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", 743 | "dev": true, 744 | "engines": { 745 | "node": ">=8.6" 746 | }, 747 | "funding": { 748 | "url": "https://github.com/sponsors/jonschlinkert" 749 | } 750 | }, 751 | "node_modules/randombytes": { 752 | "version": "2.1.0", 753 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 754 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 755 | "dev": true, 756 | "dependencies": { 757 | "safe-buffer": "^5.1.0" 758 | } 759 | }, 760 | "node_modules/readdirp": { 761 | "version": "3.6.0", 762 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 763 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 764 | "dev": true, 765 | "dependencies": { 766 | "picomatch": "^2.2.1" 767 | }, 768 | "engines": { 769 | "node": ">=8.10.0" 770 | } 771 | }, 772 | "node_modules/require-directory": { 773 | "version": "2.1.1", 774 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 775 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 776 | "dev": true, 777 | "engines": { 778 | "node": ">=0.10.0" 779 | } 780 | }, 781 | "node_modules/safe-buffer": { 782 | "version": "5.2.1", 783 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 784 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 785 | "dev": true, 786 | "funding": [ 787 | { 788 | "type": "github", 789 | "url": "https://github.com/sponsors/feross" 790 | }, 791 | { 792 | "type": "patreon", 793 | "url": "https://www.patreon.com/feross" 794 | }, 795 | { 796 | "type": "consulting", 797 | "url": "https://feross.org/support" 798 | } 799 | ] 800 | }, 801 | "node_modules/serialize-javascript": { 802 | "version": "6.0.0", 803 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 804 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 805 | "dev": true, 806 | "dependencies": { 807 | "randombytes": "^2.1.0" 808 | } 809 | }, 810 | "node_modules/string-width": { 811 | "version": "4.2.3", 812 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 813 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 814 | "dev": true, 815 | "dependencies": { 816 | "emoji-regex": "^8.0.0", 817 | "is-fullwidth-code-point": "^3.0.0", 818 | "strip-ansi": "^6.0.1" 819 | }, 820 | "engines": { 821 | "node": ">=8" 822 | } 823 | }, 824 | "node_modules/strip-ansi": { 825 | "version": "6.0.1", 826 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 827 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 828 | "dev": true, 829 | "dependencies": { 830 | "ansi-regex": "^5.0.1" 831 | }, 832 | "engines": { 833 | "node": ">=8" 834 | } 835 | }, 836 | "node_modules/strip-json-comments": { 837 | "version": "3.1.1", 838 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 839 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 840 | "dev": true, 841 | "engines": { 842 | "node": ">=8" 843 | }, 844 | "funding": { 845 | "url": "https://github.com/sponsors/sindresorhus" 846 | } 847 | }, 848 | "node_modules/supports-color": { 849 | "version": "8.1.1", 850 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 851 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 852 | "dev": true, 853 | "dependencies": { 854 | "has-flag": "^4.0.0" 855 | }, 856 | "engines": { 857 | "node": ">=10" 858 | }, 859 | "funding": { 860 | "url": "https://github.com/chalk/supports-color?sponsor=1" 861 | } 862 | }, 863 | "node_modules/through": { 864 | "version": "2.3.8", 865 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 866 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 867 | }, 868 | "node_modules/to-regex-range": { 869 | "version": "5.0.1", 870 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 871 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 872 | "dev": true, 873 | "dependencies": { 874 | "is-number": "^7.0.0" 875 | }, 876 | "engines": { 877 | "node": ">=8.0" 878 | } 879 | }, 880 | "node_modules/type-detect": { 881 | "version": "1.0.0", 882 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", 883 | "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", 884 | "dev": true, 885 | "engines": { 886 | "node": "*" 887 | } 888 | }, 889 | "node_modules/which": { 890 | "version": "2.0.2", 891 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 892 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 893 | "dev": true, 894 | "dependencies": { 895 | "isexe": "^2.0.0" 896 | }, 897 | "bin": { 898 | "node-which": "bin/node-which" 899 | }, 900 | "engines": { 901 | "node": ">= 8" 902 | } 903 | }, 904 | "node_modules/workerpool": { 905 | "version": "6.1.5", 906 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", 907 | "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", 908 | "dev": true 909 | }, 910 | "node_modules/wrap-ansi": { 911 | "version": "7.0.0", 912 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 913 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 914 | "dev": true, 915 | "dependencies": { 916 | "ansi-styles": "^4.0.0", 917 | "string-width": "^4.1.0", 918 | "strip-ansi": "^6.0.0" 919 | }, 920 | "engines": { 921 | "node": ">=10" 922 | }, 923 | "funding": { 924 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 925 | } 926 | }, 927 | "node_modules/wrappy": { 928 | "version": "1.0.2", 929 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 930 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 931 | "dev": true 932 | }, 933 | "node_modules/y18n": { 934 | "version": "5.0.8", 935 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 936 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 937 | "dev": true, 938 | "engines": { 939 | "node": ">=10" 940 | } 941 | }, 942 | "node_modules/yargs": { 943 | "version": "16.2.0", 944 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 945 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 946 | "dev": true, 947 | "dependencies": { 948 | "cliui": "^7.0.2", 949 | "escalade": "^3.1.1", 950 | "get-caller-file": "^2.0.5", 951 | "require-directory": "^2.1.1", 952 | "string-width": "^4.2.0", 953 | "y18n": "^5.0.5", 954 | "yargs-parser": "^20.2.2" 955 | }, 956 | "engines": { 957 | "node": ">=10" 958 | } 959 | }, 960 | "node_modules/yargs-parser": { 961 | "version": "20.2.4", 962 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 963 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 964 | "dev": true, 965 | "engines": { 966 | "node": ">=10" 967 | } 968 | }, 969 | "node_modules/yargs-unparser": { 970 | "version": "2.0.0", 971 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 972 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 973 | "dev": true, 974 | "dependencies": { 975 | "camelcase": "^6.0.0", 976 | "decamelize": "^4.0.0", 977 | "flat": "^5.0.2", 978 | "is-plain-obj": "^2.1.0" 979 | }, 980 | "engines": { 981 | "node": ">=10" 982 | } 983 | }, 984 | "node_modules/yocto-queue": { 985 | "version": "0.1.0", 986 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 987 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 988 | "dev": true, 989 | "engines": { 990 | "node": ">=10" 991 | }, 992 | "funding": { 993 | "url": "https://github.com/sponsors/sindresorhus" 994 | } 995 | } 996 | }, 997 | "dependencies": { 998 | "@ungap/promise-all-settled": { 999 | "version": "1.1.2", 1000 | "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", 1001 | "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", 1002 | "dev": true 1003 | }, 1004 | "ansi-colors": { 1005 | "version": "4.1.1", 1006 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 1007 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 1008 | "dev": true 1009 | }, 1010 | "ansi-regex": { 1011 | "version": "5.0.1", 1012 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1013 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1014 | "dev": true 1015 | }, 1016 | "ansi-styles": { 1017 | "version": "4.3.0", 1018 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1019 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1020 | "dev": true, 1021 | "requires": { 1022 | "color-convert": "^2.0.1" 1023 | } 1024 | }, 1025 | "anymatch": { 1026 | "version": "3.1.2", 1027 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 1028 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 1029 | "dev": true, 1030 | "requires": { 1031 | "normalize-path": "^3.0.0", 1032 | "picomatch": "^2.0.4" 1033 | } 1034 | }, 1035 | "argparse": { 1036 | "version": "2.0.1", 1037 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1038 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1039 | "dev": true 1040 | }, 1041 | "assertion-error": { 1042 | "version": "1.1.0", 1043 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 1044 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 1045 | "dev": true 1046 | }, 1047 | "balanced-match": { 1048 | "version": "1.0.2", 1049 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1050 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1051 | "dev": true 1052 | }, 1053 | "binary-extensions": { 1054 | "version": "2.2.0", 1055 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 1056 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 1057 | "dev": true 1058 | }, 1059 | "brace-expansion": { 1060 | "version": "1.1.11", 1061 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1062 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1063 | "dev": true, 1064 | "requires": { 1065 | "balanced-match": "^1.0.0", 1066 | "concat-map": "0.0.1" 1067 | } 1068 | }, 1069 | "braces": { 1070 | "version": "3.0.2", 1071 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 1072 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 1073 | "dev": true, 1074 | "requires": { 1075 | "fill-range": "^7.0.1" 1076 | } 1077 | }, 1078 | "browser-stdout": { 1079 | "version": "1.3.1", 1080 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 1081 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 1082 | "dev": true 1083 | }, 1084 | "camelcase": { 1085 | "version": "6.2.1", 1086 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", 1087 | "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", 1088 | "dev": true 1089 | }, 1090 | "chai": { 1091 | "version": "3.5.0", 1092 | "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", 1093 | "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", 1094 | "dev": true, 1095 | "requires": { 1096 | "assertion-error": "^1.0.1", 1097 | "deep-eql": "^0.1.3", 1098 | "type-detect": "^1.0.0" 1099 | } 1100 | }, 1101 | "chalk": { 1102 | "version": "4.1.2", 1103 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1104 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1105 | "dev": true, 1106 | "requires": { 1107 | "ansi-styles": "^4.1.0", 1108 | "supports-color": "^7.1.0" 1109 | }, 1110 | "dependencies": { 1111 | "supports-color": { 1112 | "version": "7.2.0", 1113 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1114 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1115 | "dev": true, 1116 | "requires": { 1117 | "has-flag": "^4.0.0" 1118 | } 1119 | } 1120 | } 1121 | }, 1122 | "chokidar": { 1123 | "version": "3.5.2", 1124 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 1125 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 1126 | "dev": true, 1127 | "requires": { 1128 | "anymatch": "~3.1.2", 1129 | "braces": "~3.0.2", 1130 | "fsevents": "~2.3.2", 1131 | "glob-parent": "~5.1.2", 1132 | "is-binary-path": "~2.1.0", 1133 | "is-glob": "~4.0.1", 1134 | "normalize-path": "~3.0.0", 1135 | "readdirp": "~3.6.0" 1136 | } 1137 | }, 1138 | "cliui": { 1139 | "version": "7.0.4", 1140 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 1141 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 1142 | "dev": true, 1143 | "requires": { 1144 | "string-width": "^4.2.0", 1145 | "strip-ansi": "^6.0.0", 1146 | "wrap-ansi": "^7.0.0" 1147 | } 1148 | }, 1149 | "color-convert": { 1150 | "version": "2.0.1", 1151 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1152 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1153 | "dev": true, 1154 | "requires": { 1155 | "color-name": "~1.1.4" 1156 | } 1157 | }, 1158 | "color-name": { 1159 | "version": "1.1.4", 1160 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1161 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1162 | "dev": true 1163 | }, 1164 | "concat-map": { 1165 | "version": "0.0.1", 1166 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1167 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 1168 | "dev": true 1169 | }, 1170 | "debug": { 1171 | "version": "4.3.2", 1172 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", 1173 | "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", 1174 | "dev": true, 1175 | "requires": { 1176 | "ms": "2.1.2" 1177 | }, 1178 | "dependencies": { 1179 | "ms": { 1180 | "version": "2.1.2", 1181 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1182 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1183 | "dev": true 1184 | } 1185 | } 1186 | }, 1187 | "decamelize": { 1188 | "version": "4.0.0", 1189 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 1190 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 1191 | "dev": true 1192 | }, 1193 | "deep-eql": { 1194 | "version": "0.1.3", 1195 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", 1196 | "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", 1197 | "dev": true, 1198 | "requires": { 1199 | "type-detect": "0.1.1" 1200 | }, 1201 | "dependencies": { 1202 | "type-detect": { 1203 | "version": "0.1.1", 1204 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", 1205 | "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", 1206 | "dev": true 1207 | } 1208 | } 1209 | }, 1210 | "diff": { 1211 | "version": "5.0.0", 1212 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 1213 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 1214 | "dev": true 1215 | }, 1216 | "emoji-regex": { 1217 | "version": "8.0.0", 1218 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1219 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1220 | "dev": true 1221 | }, 1222 | "escalade": { 1223 | "version": "3.1.1", 1224 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 1225 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 1226 | "dev": true 1227 | }, 1228 | "escape-string-regexp": { 1229 | "version": "4.0.0", 1230 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1231 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1232 | "dev": true 1233 | }, 1234 | "fill-range": { 1235 | "version": "7.0.1", 1236 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 1237 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 1238 | "dev": true, 1239 | "requires": { 1240 | "to-regex-range": "^5.0.1" 1241 | } 1242 | }, 1243 | "find-up": { 1244 | "version": "5.0.0", 1245 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 1246 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 1247 | "dev": true, 1248 | "requires": { 1249 | "locate-path": "^6.0.0", 1250 | "path-exists": "^4.0.0" 1251 | } 1252 | }, 1253 | "flat": { 1254 | "version": "5.0.2", 1255 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 1256 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 1257 | "dev": true 1258 | }, 1259 | "fs.realpath": { 1260 | "version": "1.0.0", 1261 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 1262 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 1263 | "dev": true 1264 | }, 1265 | "fsevents": { 1266 | "version": "2.3.2", 1267 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 1268 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 1269 | "dev": true, 1270 | "optional": true 1271 | }, 1272 | "get-caller-file": { 1273 | "version": "2.0.5", 1274 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 1275 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 1276 | "dev": true 1277 | }, 1278 | "glob": { 1279 | "version": "7.1.7", 1280 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 1281 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 1282 | "dev": true, 1283 | "requires": { 1284 | "fs.realpath": "^1.0.0", 1285 | "inflight": "^1.0.4", 1286 | "inherits": "2", 1287 | "minimatch": "^3.0.4", 1288 | "once": "^1.3.0", 1289 | "path-is-absolute": "^1.0.0" 1290 | } 1291 | }, 1292 | "glob-parent": { 1293 | "version": "5.1.2", 1294 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1295 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1296 | "dev": true, 1297 | "requires": { 1298 | "is-glob": "^4.0.1" 1299 | } 1300 | }, 1301 | "growl": { 1302 | "version": "1.10.5", 1303 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 1304 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 1305 | "dev": true 1306 | }, 1307 | "has-flag": { 1308 | "version": "4.0.0", 1309 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1310 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1311 | "dev": true 1312 | }, 1313 | "he": { 1314 | "version": "1.2.0", 1315 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 1316 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 1317 | "dev": true 1318 | }, 1319 | "inflight": { 1320 | "version": "1.0.6", 1321 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1322 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 1323 | "dev": true, 1324 | "requires": { 1325 | "once": "^1.3.0", 1326 | "wrappy": "1" 1327 | } 1328 | }, 1329 | "inherits": { 1330 | "version": "2.0.4", 1331 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1332 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1333 | "dev": true 1334 | }, 1335 | "is-binary-path": { 1336 | "version": "2.1.0", 1337 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1338 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1339 | "dev": true, 1340 | "requires": { 1341 | "binary-extensions": "^2.0.0" 1342 | } 1343 | }, 1344 | "is-extglob": { 1345 | "version": "2.1.1", 1346 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1347 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 1348 | "dev": true 1349 | }, 1350 | "is-fullwidth-code-point": { 1351 | "version": "3.0.0", 1352 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1353 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1354 | "dev": true 1355 | }, 1356 | "is-glob": { 1357 | "version": "4.0.3", 1358 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1359 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1360 | "dev": true, 1361 | "requires": { 1362 | "is-extglob": "^2.1.1" 1363 | } 1364 | }, 1365 | "is-number": { 1366 | "version": "7.0.0", 1367 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1368 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1369 | "dev": true 1370 | }, 1371 | "is-plain-obj": { 1372 | "version": "2.1.0", 1373 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 1374 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 1375 | "dev": true 1376 | }, 1377 | "is-unicode-supported": { 1378 | "version": "0.1.0", 1379 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 1380 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 1381 | "dev": true 1382 | }, 1383 | "isexe": { 1384 | "version": "2.0.0", 1385 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1386 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 1387 | "dev": true 1388 | }, 1389 | "js-yaml": { 1390 | "version": "4.1.0", 1391 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1392 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1393 | "dev": true, 1394 | "requires": { 1395 | "argparse": "^2.0.1" 1396 | } 1397 | }, 1398 | "locate-path": { 1399 | "version": "6.0.0", 1400 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1401 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1402 | "dev": true, 1403 | "requires": { 1404 | "p-locate": "^5.0.0" 1405 | } 1406 | }, 1407 | "log-symbols": { 1408 | "version": "4.1.0", 1409 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 1410 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 1411 | "dev": true, 1412 | "requires": { 1413 | "chalk": "^4.1.0", 1414 | "is-unicode-supported": "^0.1.0" 1415 | } 1416 | }, 1417 | "minimatch": { 1418 | "version": "3.0.4", 1419 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1420 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1421 | "dev": true, 1422 | "requires": { 1423 | "brace-expansion": "^1.1.7" 1424 | } 1425 | }, 1426 | "mocha": { 1427 | "version": "9.1.3", 1428 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", 1429 | "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", 1430 | "dev": true, 1431 | "requires": { 1432 | "@ungap/promise-all-settled": "1.1.2", 1433 | "ansi-colors": "4.1.1", 1434 | "browser-stdout": "1.3.1", 1435 | "chokidar": "3.5.2", 1436 | "debug": "4.3.2", 1437 | "diff": "5.0.0", 1438 | "escape-string-regexp": "4.0.0", 1439 | "find-up": "5.0.0", 1440 | "glob": "7.1.7", 1441 | "growl": "1.10.5", 1442 | "he": "1.2.0", 1443 | "js-yaml": "4.1.0", 1444 | "log-symbols": "4.1.0", 1445 | "minimatch": "3.0.4", 1446 | "ms": "2.1.3", 1447 | "nanoid": "3.1.25", 1448 | "serialize-javascript": "6.0.0", 1449 | "strip-json-comments": "3.1.1", 1450 | "supports-color": "8.1.1", 1451 | "which": "2.0.2", 1452 | "workerpool": "6.1.5", 1453 | "yargs": "16.2.0", 1454 | "yargs-parser": "20.2.4", 1455 | "yargs-unparser": "2.0.0" 1456 | } 1457 | }, 1458 | "mock-fs": { 1459 | "version": "4.14.0", 1460 | "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.14.0.tgz", 1461 | "integrity": "sha512-qYvlv/exQ4+svI3UOvPUpLDF0OMX5euvUH0Ny4N5QyRyhNdgAgUrVH3iUINSzEPLvx0kbo/Bp28GJKIqvE7URw==", 1462 | "dev": true 1463 | }, 1464 | "ms": { 1465 | "version": "2.1.3", 1466 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1467 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1468 | "dev": true 1469 | }, 1470 | "nanoid": { 1471 | "version": "3.1.25", 1472 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", 1473 | "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", 1474 | "dev": true 1475 | }, 1476 | "normalize-path": { 1477 | "version": "3.0.0", 1478 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1479 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1480 | "dev": true 1481 | }, 1482 | "once": { 1483 | "version": "1.4.0", 1484 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1485 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1486 | "dev": true, 1487 | "requires": { 1488 | "wrappy": "1" 1489 | } 1490 | }, 1491 | "p-limit": { 1492 | "version": "3.1.0", 1493 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1494 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1495 | "dev": true, 1496 | "requires": { 1497 | "yocto-queue": "^0.1.0" 1498 | } 1499 | }, 1500 | "p-locate": { 1501 | "version": "5.0.0", 1502 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1503 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1504 | "dev": true, 1505 | "requires": { 1506 | "p-limit": "^3.0.2" 1507 | } 1508 | }, 1509 | "path-exists": { 1510 | "version": "4.0.0", 1511 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1512 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1513 | "dev": true 1514 | }, 1515 | "path-is-absolute": { 1516 | "version": "1.0.1", 1517 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1518 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1519 | "dev": true 1520 | }, 1521 | "picomatch": { 1522 | "version": "2.3.0", 1523 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 1524 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", 1525 | "dev": true 1526 | }, 1527 | "randombytes": { 1528 | "version": "2.1.0", 1529 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1530 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1531 | "dev": true, 1532 | "requires": { 1533 | "safe-buffer": "^5.1.0" 1534 | } 1535 | }, 1536 | "readdirp": { 1537 | "version": "3.6.0", 1538 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1539 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1540 | "dev": true, 1541 | "requires": { 1542 | "picomatch": "^2.2.1" 1543 | } 1544 | }, 1545 | "require-directory": { 1546 | "version": "2.1.1", 1547 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1548 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 1549 | "dev": true 1550 | }, 1551 | "safe-buffer": { 1552 | "version": "5.2.1", 1553 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1554 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1555 | "dev": true 1556 | }, 1557 | "serialize-javascript": { 1558 | "version": "6.0.0", 1559 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 1560 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 1561 | "dev": true, 1562 | "requires": { 1563 | "randombytes": "^2.1.0" 1564 | } 1565 | }, 1566 | "string-width": { 1567 | "version": "4.2.3", 1568 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1569 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1570 | "dev": true, 1571 | "requires": { 1572 | "emoji-regex": "^8.0.0", 1573 | "is-fullwidth-code-point": "^3.0.0", 1574 | "strip-ansi": "^6.0.1" 1575 | } 1576 | }, 1577 | "strip-ansi": { 1578 | "version": "6.0.1", 1579 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1580 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1581 | "dev": true, 1582 | "requires": { 1583 | "ansi-regex": "^5.0.1" 1584 | } 1585 | }, 1586 | "strip-json-comments": { 1587 | "version": "3.1.1", 1588 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1589 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1590 | "dev": true 1591 | }, 1592 | "supports-color": { 1593 | "version": "8.1.1", 1594 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1595 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1596 | "dev": true, 1597 | "requires": { 1598 | "has-flag": "^4.0.0" 1599 | } 1600 | }, 1601 | "through": { 1602 | "version": "2.3.8", 1603 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1604 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 1605 | }, 1606 | "to-regex-range": { 1607 | "version": "5.0.1", 1608 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1609 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1610 | "dev": true, 1611 | "requires": { 1612 | "is-number": "^7.0.0" 1613 | } 1614 | }, 1615 | "type-detect": { 1616 | "version": "1.0.0", 1617 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", 1618 | "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", 1619 | "dev": true 1620 | }, 1621 | "which": { 1622 | "version": "2.0.2", 1623 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1624 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1625 | "dev": true, 1626 | "requires": { 1627 | "isexe": "^2.0.0" 1628 | } 1629 | }, 1630 | "workerpool": { 1631 | "version": "6.1.5", 1632 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", 1633 | "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", 1634 | "dev": true 1635 | }, 1636 | "wrap-ansi": { 1637 | "version": "7.0.0", 1638 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1639 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1640 | "dev": true, 1641 | "requires": { 1642 | "ansi-styles": "^4.0.0", 1643 | "string-width": "^4.1.0", 1644 | "strip-ansi": "^6.0.0" 1645 | } 1646 | }, 1647 | "wrappy": { 1648 | "version": "1.0.2", 1649 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1650 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1651 | "dev": true 1652 | }, 1653 | "y18n": { 1654 | "version": "5.0.8", 1655 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1656 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1657 | "dev": true 1658 | }, 1659 | "yargs": { 1660 | "version": "16.2.0", 1661 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1662 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1663 | "dev": true, 1664 | "requires": { 1665 | "cliui": "^7.0.2", 1666 | "escalade": "^3.1.1", 1667 | "get-caller-file": "^2.0.5", 1668 | "require-directory": "^2.1.1", 1669 | "string-width": "^4.2.0", 1670 | "y18n": "^5.0.5", 1671 | "yargs-parser": "^20.2.2" 1672 | } 1673 | }, 1674 | "yargs-parser": { 1675 | "version": "20.2.4", 1676 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 1677 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 1678 | "dev": true 1679 | }, 1680 | "yargs-unparser": { 1681 | "version": "2.0.0", 1682 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1683 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1684 | "dev": true, 1685 | "requires": { 1686 | "camelcase": "^6.0.0", 1687 | "decamelize": "^4.0.0", 1688 | "flat": "^5.0.2", 1689 | "is-plain-obj": "^2.1.0" 1690 | } 1691 | }, 1692 | "yocto-queue": { 1693 | "version": "0.1.0", 1694 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1695 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1696 | "dev": true 1697 | } 1698 | } 1699 | } 1700 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsfs", 3 | "version": "4.2.0", 4 | "description": "deduplicating filesystem with a REST interface", 5 | "main": "server.js", 6 | "files": [ 7 | "server.js", 8 | "config.ex", 9 | "jlog.js", 10 | "lib" 11 | ], 12 | "directories": { 13 | "lib": "./lib", 14 | "tools": "./tools" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/jjg/jsfs.git" 19 | }, 20 | "homepage": "https://github.com/jjg/jsfs", 21 | "keywords": [ 22 | "filesystem", 23 | "deduplicate", 24 | "http", 25 | "rest" 26 | ], 27 | "author": { 28 | "name": "Jason Gullickson" 29 | }, 30 | "contributors": [ 31 | { 32 | "name": "Marc Brakken" 33 | } 34 | ], 35 | "license": "GPL-3.0", 36 | "scripts": { 37 | "test": "./node_modules/.bin/mocha --reporter spec" 38 | }, 39 | "dependencies": { 40 | "through": "^2.3.8" 41 | }, 42 | "devDependencies": { 43 | "chai": "^3.5.0", 44 | "mocha": "^9.1.3", 45 | "mock-fs": "^4.0.0-beta.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rock64.md: -------------------------------------------------------------------------------- 1 | # Installing JSFS on Rock64 Hardware 2 | 3 | Pine64's [Rock64](https://www.pine64.org/devices/single-board-computers/rock64/) SBC is an inexpensive ARM computer well suited for running JSFS. 4 | 5 | ## Prerequisites 6 | 7 | ### Node.js 8 | 9 | Node is built from source using the follow steps (~ minutes): 10 | 11 | 0. sudo apt-get install python g++ make 12 | 1. wget https://nodejs.org/dist/v14.17.5/node-v14.17.5.tar.gz 13 | 2. tar zxf node-v14.17.5.tar.gz 14 | 3. cd node-v14.15.5 15 | 4. ./configure 16 | 5. make -j4 (this step fails: g++: fatal error: Killed signal terminated program cc1plus 17 | 6. sudo make install 18 | 19 | Try using precompiled binaries instead: 20 | 21 | 0. sudo apt install xz-utils 22 | 1. wget https://nodejs.org/dist/v14.17.5/node-v14.17.5-linux-arm64.tar.xz 23 | 2. tar -xf node-v14.17.5-linux-arm64.tar.xz 24 | 25 | 26 | ### Storage 27 | 28 | You'll probably want to use something other than the boot SD card for storage. Attach something via USB and mount it at boot like so: 29 | 30 | ### Test Run 31 | 32 | ~/node-v14.17.5-linux-arm64/bin/node ./server.js 33 | 34 | This fails with the following error: 35 | 36 | ``` 37 | internal/modules/cjs/loader.js:892 38 | throw err; 39 | ^ 40 | 41 | Error: Cannot find module 'through' 42 | ``` 43 | 44 | Looks like someone added undocumented dependencies... 45 | 46 | Let's try and resolve that: 47 | 48 | ~/node-v14.17.5-linux-arm64/bin/node ~/node-v14.17.5-linux-arm64/bin/npm install through 49 | 50 | This time the server starts. 51 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* globals require */ 3 | 4 | /// jsfs - Javascript filesystem with a REST interface 5 | 6 | // *** CONVENTIONS *** 7 | // strings are double-quoted, variables use underscores, constants are ALL CAPS 8 | 9 | // *** UTILITIES & MODULES *** 10 | var http = require("http"); 11 | var crypto = require("crypto"); 12 | var zlib = require("zlib"); 13 | var through = require("through"); 14 | var config = require("./config.js"); 15 | var log = require("./jlog-nomem.js"); 16 | var CONSTANTS = require("./lib/constants.js"); 17 | var utils = require("./lib/utils.js"); 18 | var validate = require("./lib/validate.js"); 19 | var operations = require("./lib/" + (config.CONFIGURED_STORAGE || "fs") + "/disk-operations.js"); 20 | 21 | // base storage object 22 | var Inode = require("./lib/inode.js"); 23 | 24 | // get this now, rather than at several other points 25 | var TOTAL_LOCATIONS = config.STORAGE_LOCATIONS.length; 26 | 27 | // all responses include these headers to support cross-domain requests 28 | var ALLOWED_METHODS = CONSTANTS.ALLOWED_METHODS.join(","); 29 | var ALLOWED_HEADERS = CONSTANTS.ALLOWED_HEADERS.join(","); 30 | var EXPOSED_HEADERS = CONSTANTS.EXPOSED_HEADERS.join(","); 31 | var ACCEPTED_PARAMS = CONSTANTS.ACCEPTED_PARAMS; 32 | 33 | // *** CONFIGURATION *** 34 | log.level = config.LOG_LEVEL; // the minimum level of log messages to record: 0 = info, 1 = warn, 2 = error 35 | log.message(log.INFO, "JSFS ready to process requests"); 36 | 37 | // at the highest level, jsfs is an HTTP server that accepts GET, POST, PUT, DELETE and OPTIONS methods 38 | http.createServer(function(req, res){ 39 | 40 | // override default 2 minute time-out 41 | res.setTimeout(config.REQUEST_TIMEOUT * 60 * 1000); 42 | 43 | log.message(log.DEBUG, "Initial request received"); 44 | 45 | res.setHeader("Access-Control-Allow-Methods", ALLOWED_METHODS); 46 | res.setHeader("Access-Control-Allow-Headers", ALLOWED_HEADERS); 47 | res.setHeader("Access-Control-Allow-Origin", "*"); 48 | res.setHeader("Access-Control-Expose-Headers", EXPOSED_HEADERS); 49 | 50 | // all requests are interrorgated for these values 51 | var target_url = utils.target_from_url(req.headers["host"], req.url); 52 | 53 | // check for request parameters, first in the header and then in the querystring 54 | // Moved these to an object to avoid possible issues from "private" being a reserved word 55 | // (for future use) and to avoid jslint errors from unimplemented param handlers. 56 | var params = utils.request_parameters(ACCEPTED_PARAMS, req.url, req.headers); 57 | 58 | log.message(log.INFO, "Received " + req.method + " request for URL " + target_url); 59 | 60 | // load requested inode 61 | switch(req.method){ 62 | 63 | case "GET": 64 | utils.load_inode(target_url, function(err, inode){ 65 | if (err) { 66 | log.message(log.WARN, "Result: 404"); 67 | res.statusCode = 404; 68 | return res.end(); 69 | } 70 | 71 | var requested_file = inode; 72 | 73 | // check authorization 74 | if (inode.private){ 75 | if (validate.is_authorized(inode, req.method, params)) { 76 | log.message(log.INFO, "GET request authorized"); 77 | } else { 78 | log.message(log.WARN, "GET request unauthorized"); 79 | res.statusCode = 401; 80 | return res.end(); 81 | } 82 | } 83 | 84 | var create_decryptor = function create_decryptor(options){ 85 | return options.encrypted ? crypto.createDecipher("aes-256-cbc", options.key) : through(); 86 | }; 87 | 88 | var create_unzipper = function create_unzipper(compressed){ 89 | return compressed ? zlib.createGunzip() : through(); 90 | }; 91 | 92 | // return status 93 | res.statusCode = 200; 94 | 95 | // return file metadata as HTTP headers 96 | res.setHeader("Content-Type", requested_file.content_type); 97 | res.setHeader("Content-Length", requested_file.file_size); 98 | 99 | var total_blocks = requested_file.blocks.length; 100 | var idx = 0; 101 | 102 | var search_for_block = function search_for_block(_idx){ 103 | var location = config.STORAGE_LOCATIONS[_idx]; 104 | var search_path = location.path + requested_file.blocks[idx].block_hash; 105 | _idx++; 106 | 107 | operations.exists(search_path + "gz", function(err, result){ 108 | if (result) { 109 | log.message(log.INFO, "Found compressed block " + requested_file.blocks[idx].block_hash + ".gz in " + location.path); 110 | requested_file.blocks[idx].last_seen = location.path; 111 | utils.save_inode(requested_file, function(){ 112 | return read_file(search_path + ".gz", true); 113 | }); 114 | } else { 115 | operations.exists(search_path, function(_err, _result){ 116 | if (_result) { 117 | log.message(log.INFO, "Found block " + requested_file.blocks[idx].block_hash + " in " + location.path); 118 | requested_file.blocks[idx].last_seen = location.path; 119 | utils.save_inode(requested_file, function(){ 120 | return read_file(search_path, false); 121 | }); 122 | 123 | } else { 124 | if (_idx === TOTAL_LOCATIONS) { 125 | // we get here if we didn't find the block 126 | log.message(log.ERROR, "Unable to locate block in any storage location"); 127 | res.statusCode = 500; 128 | return res.end("Unable to return file, missing blocks"); 129 | } else { 130 | return search_for_block(_idx); 131 | } 132 | } 133 | }); 134 | } 135 | }); 136 | }; 137 | 138 | var read_file = function read_file(path, try_compressed){ 139 | var read_stream = operations.stream_read(path); 140 | var decryptor = create_decryptor({ encrypted : requested_file.encrypted, key : requested_file.access_key}); 141 | var unzipper = create_unzipper(try_compressed); 142 | var should_end = (idx + 1) === total_blocks; 143 | 144 | function on_error(){ 145 | if (try_compressed) { 146 | log.message(log.WARN, "Cannot locate compressed block in last_seen location, trying uncompressed"); 147 | return load_from_last_seen(false); 148 | } else { 149 | log.message(log.WARN, "Did not find block in expected location. Searching..."); 150 | return search_for_block(0); 151 | } 152 | } 153 | 154 | function on_end(){ 155 | idx++; 156 | read_stream.removeListener("end", on_end); 157 | read_stream.removeListener("error", on_error); 158 | if (res.getMaxListeners !== undefined) { 159 | res.setMaxListeners(res.getMaxListeners() - 1); 160 | } 161 | send_blocks(); 162 | } 163 | 164 | if (res.getMaxListeners !== undefined) { 165 | res.setMaxListeners(res.getMaxListeners() + 1); 166 | } else { 167 | res.setMaxListeners(0); 168 | } 169 | read_stream.on("end", on_end); 170 | read_stream.on("error", on_error); 171 | read_stream.pipe(unzipper).pipe(decryptor).pipe(res, {end: should_end}); 172 | }; 173 | 174 | var load_from_last_seen = function load_from_last_seen(try_compressed){ 175 | var sfx = try_compressed ? ".gz" : ""; 176 | var block = requested_file.blocks[idx]; 177 | var block_filename = block.last_seen + block.block_hash + sfx; 178 | read_file(block_filename, try_compressed); 179 | }; 180 | 181 | var send_blocks = function send_blocks(){ 182 | 183 | if (idx === total_blocks) { // we're done 184 | return; 185 | } else { 186 | if (requested_file.blocks[idx].last_seen) { 187 | load_from_last_seen(true); 188 | } else { 189 | search_for_block(0); 190 | } 191 | } 192 | }; 193 | 194 | send_blocks(); 195 | 196 | }); 197 | 198 | break; 199 | 200 | case "POST": 201 | case "PUT": 202 | // check if a file exists at this url 203 | log.message(log.DEBUG, "Begin checking for existing file"); 204 | utils.load_inode(target_url, function(err, inode){ 205 | 206 | if (inode){ 207 | 208 | // check authorization 209 | if (validate.is_authorized(inode, req.method, params)){ 210 | log.message(log.INFO, "File update request authorized"); 211 | } else { 212 | log.message(log.WARN, "File update request unauthorized"); 213 | res.statusCode = 401; 214 | res.end(); 215 | return; 216 | } 217 | } else { 218 | 219 | // if static access keys are configured, require an access key or token 220 | // TODO: Maybe DRY up these unauthorized responses. 221 | if("STATIC_ACCESS_KEYS" in config && config.STATIC_ACCESS_KEYS.length > 0){ 222 | if((!params.access_key || params.access_key.length < 1) && (!params.access_token || params.access_token.length < 1)) { 223 | log.message(log.WARN, "File update request unauthorized"); 224 | res.statusCode = 401; 225 | res.end(); 226 | return; 227 | } 228 | // if an access_key was provided, check against the static keys 229 | if(params.access_key && params.access_key.length > 0){ 230 | if(!config.STATIC_ACCESS_KEYS.includes(params.access_key)){ 231 | log.message(log.WARN, "File update request unauthorized"); 232 | res.statusCode = 401; 233 | res.end(); 234 | return; 235 | } 236 | } 237 | } 238 | 239 | log.message(log.DEBUG, "No existing file found, storing new file"); 240 | } 241 | 242 | // store the posted data at the specified URL 243 | var new_file = Object.create(Inode); 244 | new_file.init(target_url); 245 | log.message(log.DEBUG, "New file object created"); 246 | 247 | // set additional file properties (content-type, etc.) 248 | if(params.content_type){ 249 | log.message(log.INFO, "Content-Type: " + params.content_type); 250 | new_file.file_metadata.content_type = params.content_type; 251 | } 252 | if(params.private){ 253 | new_file.file_metadata.private = true; 254 | } 255 | if(params.encrypted){ 256 | new_file.file_metadata.encrypted = true; 257 | } 258 | 259 | // if access_key is supplied with update, replace the default one 260 | if(params.access_key){ 261 | new_file.file_metadata.access_key = params.access_key; 262 | } 263 | log.message(log.INFO, "File properties set"); 264 | 265 | req.on("data", function(chunk){ 266 | new_file.write(chunk, req, function(result){ 267 | if (!result) { 268 | log.message(log.ERROR, "Error writing data to storage object"); 269 | res.statusCode = 500; 270 | res.end(); 271 | } 272 | }); 273 | }); 274 | 275 | req.on("end", function(){ 276 | log.message(log.INFO, "End of request"); 277 | if(new_file){ 278 | log.message(log.DEBUG, "Closing new file"); 279 | new_file.close(function(result){ 280 | if(result){ 281 | res.end(JSON.stringify(result)); 282 | } else { 283 | log.message(log.ERROR, "Error closing storage object"); 284 | res.statusCode = 500; 285 | res.end(); 286 | } 287 | }); 288 | } 289 | }); 290 | 291 | }); 292 | 293 | break; 294 | 295 | case "DELETE": 296 | 297 | // remove the data stored at the specified URL 298 | utils.load_inode(target_url, function(error, inode){ 299 | 300 | if (error) { 301 | log.message(log.WARN, "Error loading inode: " + error.toString()); 302 | } 303 | 304 | if(inode){ 305 | 306 | // authorize (only keyholder can delete) 307 | if(validate.has_key(inode, params)){ 308 | 309 | // delete inode file 310 | log.message(log.INFO, "Delete request authorized"); 311 | 312 | var remove_inode = function remove_inode(idx){ 313 | var location = config.STORAGE_LOCATIONS[idx]; 314 | var file = location.path + inode.fingerprint + ".json"; 315 | 316 | operations.delete(file, function(err){ 317 | idx++; 318 | if (err) { 319 | log.message(log.WARN, "Inode " + inode.fingerprint + " doesn't exist in location " + location.path); 320 | } 321 | 322 | if (idx === TOTAL_LOCATIONS) { 323 | res.statusCode = 204; 324 | return res.end(); 325 | } else { 326 | remove_inode(idx); 327 | } 328 | 329 | }); 330 | 331 | }; 332 | 333 | remove_inode(0); 334 | 335 | } else { 336 | log.message(log.WARN, "Delete request unauthorized"); 337 | res.statusCode = 401; 338 | res.end(); 339 | } 340 | } else { 341 | log.message(log.WARN, "Delete request file not found"); 342 | res.statusCode = 404; 343 | res.end(); 344 | } 345 | }); 346 | 347 | break; 348 | 349 | case "HEAD": 350 | utils.load_inode(target_url, function(error, requested_file){ 351 | if (error) { 352 | log.message(log.WARN, "Error loading inode: " + error.toString()); 353 | } 354 | 355 | if(requested_file){ 356 | 357 | // construct headers 358 | res.setHeader("Content-Type", requested_file.content_type); 359 | res.setHeader("Content-Length", requested_file.file_size); 360 | 361 | // add extended object headers if we have them 362 | if(requested_file.media_type){ 363 | res.setHeader("X-Media-Type", requested_file.media_type); 364 | if(requested_file.media_type !== "unknown"){ 365 | res.setHeader("X-Media-Size", requested_file.media_size); 366 | res.setHeader("X-Media-Channels", requested_file.media_channels); 367 | res.setHeader("X-Media-Bitrate", requested_file.media_bitrate); 368 | res.setHeader("X-Media-Resolution", requested_file.media_resolution); 369 | res.setHeader("X-Media-Duration", requested_file.media_duration); 370 | } 371 | } 372 | return res.end(); 373 | } else { 374 | log.message(log.INFO, "HEAD Result: 404"); 375 | res.statusCode = 404; 376 | return res.end(); 377 | } 378 | }); 379 | 380 | break; 381 | 382 | case "OPTIONS": 383 | // support for OPTIONS is required to support cross-domain requests (CORS) 384 | res.writeHead(204); 385 | res.end(); 386 | break; 387 | 388 | default: 389 | res.writeHead(405); 390 | res.end("method " + req.method + " is not supported"); 391 | } 392 | 393 | }).listen(config.SERVER_PORT); 394 | -------------------------------------------------------------------------------- /test/file-types.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var expect = require('chai').expect 3 | var file_types = require("../lib/file-types.js"); 4 | var config = require("../config.js"); 5 | 6 | var BLOCK_SIZE = config.BLOCK_SIZE; 7 | 8 | var WAVE_RESULT = { 9 | bitrate : 44100, 10 | channels : 2, 11 | data_block_size : 4, 12 | duration : 302.26666666666665, 13 | resolution : 16, 14 | size : 53319876, 15 | subchunk_byte : 36, 16 | subchunk_id : "data", 17 | type : "wave" 18 | }; 19 | 20 | var UNKNOWN_RESULT = { type: "unknown" }; 21 | 22 | function load_test_block(file, callback) { 23 | fs.readFile(file, function(err, data){ 24 | if (err) { 25 | return callback(err); 26 | } 27 | 28 | return callback(null, data.slice(0, BLOCK_SIZE)); 29 | }); 30 | } 31 | 32 | describe("file-types.js", function() { 33 | 34 | describe("#analyze", function() { 35 | 36 | it("should return full result for wave files", function(done) { 37 | load_test_block("./test/fixtures/test.wav", function(error, block){ 38 | if (error) { 39 | done(error); 40 | } else { 41 | var result = file_types.analyze(block); 42 | expect(result).to.be.an("object"); 43 | expect(result).to.deep.equal(WAVE_RESULT); 44 | expect(result).to.have.all.keys(Object.keys(WAVE_RESULT)); 45 | done(); 46 | } 47 | }); 48 | }); 49 | 50 | it("should return `{ type: \"unknown\" }` for mp3 (anything not wave)", function(done) { 51 | load_test_block("./test/fixtures/test.mp3", function(error, block){ 52 | if (error) { 53 | done(error); 54 | } else { 55 | var result = file_types.analyze(block); 56 | expect(result).to.be.an("object"); 57 | expect(result).to.deep.equal(UNKNOWN_RESULT); 58 | expect(result).to.have.all.keys("type"); 59 | done(); 60 | } 61 | }); 62 | }); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/fixtures/test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjg/jsfs/6165f89c3ad24a4760d1da6adfb2d6c22b6132b7/test/fixtures/test.mp3 -------------------------------------------------------------------------------- /test/fixtures/test.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jjg/jsfs/6165f89c3ad24a4760d1da6adfb2d6c22b6132b7/test/fixtures/test.wav -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var mock = require('mock-fs'); 3 | var expect = require('chai').expect 4 | var utils = require("../lib/utils.js"); 5 | var file_types = require("../lib/file-types.js"); 6 | var CONSTANTS = require("../lib/constants.js"); 7 | var config = require("../config.js"); 8 | var log = require("../jlog.js"); 9 | 10 | var DEFAULT_STORAGE = config.STORAGE_LOCATIONS; 11 | var BLOCK_SIZE = config.BLOCK_SIZE; 12 | var TEST_PATH = "/com.jsfs.test/path/to/file.json"; 13 | var ACCEPTED_PARAMS = CONSTANTS.ACCEPTED_PARAMS; 14 | var TEST_INODE_1 = { fingerprint : "test1" }; 15 | var TEST_INODE_2 = { fingerprint : "test2" }; 16 | 17 | function load_test_block(file, callback) { 18 | fs.readFile(file, function(err, data){ 19 | if (err) { 20 | return callback(err); 21 | } 22 | 23 | return callback(null, data.slice(0, BLOCK_SIZE)); 24 | }); 25 | } 26 | 27 | describe("utils.js", function() { 28 | 29 | before(function(){ 30 | // suppress debug log output for tests 31 | log.level = 4; 32 | }); 33 | 34 | after(function(){ 35 | // restore default log level 36 | log.level = config.LOG_LEVEL 37 | }); 38 | 39 | describe("#wave_audio_offset(block, data, default_size)", function() { 40 | 41 | it("should return smaller offset for wave", function(done) { 42 | load_test_block("./test/fixtures/test.wav", function(error, block){ 43 | if (error) { 44 | done(error); 45 | } else { 46 | var offset = utils.wave_audio_offset(block, file_types.analyze(block), BLOCK_SIZE); 47 | 48 | expect(offset).to.be.a("number"); 49 | expect(offset).to.equal(44); 50 | expect(offset).to.be.at.most(BLOCK_SIZE); 51 | expect(offset).to.be.below(BLOCK_SIZE); 52 | done(); 53 | } 54 | }); 55 | }); 56 | 57 | it("should return default offset for not wave", function(done) { 58 | load_test_block("./test/fixtures/test.mp3", function(error, block){ 59 | if (error) { 60 | done(error); 61 | } else { 62 | var offset = utils.wave_audio_offset(block, file_types.analyze(block), BLOCK_SIZE); 63 | 64 | expect(offset).to.be.a("number"); 65 | expect(offset).to.equal(BLOCK_SIZE); 66 | expect(offset).to.be.at.most(BLOCK_SIZE); 67 | done(); 68 | } 69 | }); 70 | }); 71 | 72 | }); 73 | 74 | context("inode operations", function(){ 75 | 76 | before(function(){ 77 | config.STORAGE_LOCATIONS = [ 78 | {"path":"fake/blocks1/","capacity":4294967296}, 79 | {"path":"fake/blocks2/","capacity":4294967296} 80 | ]; 81 | 82 | var fake_data = { 83 | fake: { 84 | "blocks1": { 85 | }, 86 | "blocks2": { 87 | } 88 | } 89 | }; 90 | 91 | var hash_1 = utils.sha1_to_hex("test_inode_1"); 92 | var hash_2 = utils.sha1_to_hex("test_inode_2"); 93 | fake_data.fake.blocks1[hash_1 + ".json"] = JSON.stringify(TEST_INODE_1); 94 | fake_data.fake.blocks2[hash_1 + ".json"] = JSON.stringify(TEST_INODE_1); 95 | fake_data.fake.blocks2[hash_2 + ".json"] = JSON.stringify(TEST_INODE_2); 96 | 97 | mock(fake_data); 98 | }); 99 | 100 | after(function(){ 101 | // restore default log level 102 | mock.restore(); 103 | config.STORAGE_LOCATIONS = DEFAULT_STORAGE; 104 | }); 105 | 106 | describe("#load_inode(url, callback)", function() { 107 | 108 | it("should find an inode", function(done) { 109 | utils.load_inode("test_inode_1", function(err, inode){ 110 | if (err) { 111 | done(err); 112 | } else { 113 | expect(inode).to.be.an("object"); 114 | expect(inode).to.deep.equal(TEST_INODE_1); 115 | done(); 116 | } 117 | }); 118 | }); 119 | 120 | it("should searche multiple directories and return found inode", function(done) { 121 | utils.load_inode("test_inode_2", function(err, inode){ 122 | if (err) { 123 | done(err); 124 | } else { 125 | expect(inode).to.be.an("object"); 126 | expect(inode).to.deep.equal(TEST_INODE_2); 127 | done(); 128 | } 129 | }); 130 | }); 131 | 132 | it("should return an error for missing inode", function(done) { 133 | utils.load_inode("test_inode_3", function(err, inode){ 134 | expect(inode).to.be.undefined; 135 | expect(err).to.be.an.instanceof(Error); 136 | expect(err).to.have.property("code", "ENOENT"); 137 | expect(err).to.have.property("errno", 34); 138 | done(); 139 | }); 140 | }); 141 | 142 | }); 143 | 144 | describe("#save_inode(inode, callback)", function(){ 145 | 146 | it("should save an inode", function(done) { 147 | var path = "test_inode_4"; 148 | var inode = { fingerprint: utils.sha1_to_hex(path) }; 149 | 150 | utils.save_inode(inode, function(found_inode){ 151 | expect(found_inode).to.deep.equal(inode); 152 | 153 | // maybe should do manual inspection of each directory 154 | utils.load_inode(path, function(err, response){ 155 | expect(err).to.be.null; 156 | expect(response).to.be.an("object"); 157 | expect(response).to.deep.equal(inode); 158 | done(); 159 | }); 160 | }); 161 | }); 162 | }); 163 | 164 | }); 165 | 166 | describe("#target_from_url(uri)", function() { 167 | 168 | it("should set target from url", function() { 169 | var host = "test.jsfs.com"; 170 | var uri = "/path/to/file.json"; 171 | var result = utils.target_from_url(host, uri); 172 | 173 | expect(result).to.be.a("string"); 174 | expect(result).to.equal(TEST_PATH); 175 | }); 176 | 177 | it("should return fully specificed target path", function() { 178 | var host = "test2.jsfs.com"; 179 | var uri = "/.com.jsfs.test/path/to/file.json"; 180 | var result = utils.target_from_url(host, uri); 181 | 182 | expect(result).to.be.a("string"); 183 | expect(result).to.equal(TEST_PATH); 184 | }); 185 | 186 | it("should ignore query params", function() { 187 | var host = "test.jsfs.com"; 188 | var uri = "/path/to/file.json?test=query&more=fun"; 189 | var result = utils.target_from_url(host, uri); 190 | 191 | expect(result).to.be.a("string"); 192 | expect(result).to.equal(TEST_PATH); 193 | }); 194 | 195 | it("should ignore port", function() { 196 | var host = "test.jsfs.com:1234"; 197 | var uri = "/path/to/file.json"; 198 | var result = utils.target_from_url(host, uri); 199 | 200 | expect(result).to.be.a("string"); 201 | expect(result).to.equal(TEST_PATH); 202 | }); 203 | 204 | }); 205 | 206 | describe("#request_parameters", function() { 207 | 208 | it("should return object with all parameters", function() { 209 | var test_uri = "http://test.jsfs.com/path/to/file.json"; 210 | var headers = {}; 211 | var result = utils.request_parameters(ACCEPTED_PARAMS, test_uri, headers); 212 | 213 | expect(result).to.be.an("object"); 214 | expect(Object.keys(result)).to.have.lengthOf(ACCEPTED_PARAMS.length); 215 | expect(result).to.have.all.keys(ACCEPTED_PARAMS.map(function(p){ return Object.keys(p)[0]; })); 216 | }); 217 | 218 | it("should set content-type from header", function() { 219 | var test_uri = "http://test.jsfs.com/path/to/file.json"; 220 | var headers = { "content-type": "application/json" }; 221 | var result = utils.request_parameters(ACCEPTED_PARAMS, test_uri, headers); 222 | 223 | expect(result).to.be.an("object"); 224 | expect(result.content_type).to.equal("application/json"); 225 | }); 226 | 227 | it("should set parameters from query", function() { 228 | var test_uri = "http://test.jsfs.com/path/to/file.json?access_token=testing"; 229 | var headers = {}; 230 | var result = utils.request_parameters(ACCEPTED_PARAMS, test_uri, headers); 231 | 232 | expect(result).to.be.an("object"); 233 | expect(result.access_token).to.equal("testing"); 234 | }); 235 | 236 | it("should set parameters from header", function() { 237 | var test_uri = "http://test.jsfs.com/path/to/file.json"; 238 | var headers = {"x-access-token": "testing"}; 239 | var result = utils.request_parameters(ACCEPTED_PARAMS, test_uri, headers); 240 | 241 | expect(result).to.be.an("object"); 242 | expect(result.access_token).to.equal("testing"); 243 | }); 244 | 245 | it("should give priority to url query over header", function() { 246 | var test_uri = "http://test.jsfs.com/path/to/file.json?access_token=testing"; 247 | var headers = {"x-access-token": "ignore_me"}; 248 | var result = utils.request_parameters(ACCEPTED_PARAMS, test_uri, headers); 249 | 250 | expect(result).to.be.an("object"); 251 | expect(result.access_token).to.equal("testing"); 252 | }); 253 | 254 | }); 255 | 256 | }); 257 | -------------------------------------------------------------------------------- /test/validate.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var validate = require("../lib/validate.js"); 3 | var log = require("../jlog.js"); 4 | var config = require("../config.js"); 5 | var utils = require("../lib/utils.js"); 6 | 7 | var GOOD_KEY = "testing_key"; 8 | var BAD_KEY = "wrong_key"; 9 | var INODE = { access_key: GOOD_KEY }; 10 | var GET = "GET"; 11 | 12 | function setExpire(minutes){ 13 | var d = new Date(); 14 | d.setMinutes(d.getMinutes() + minutes); 15 | return d.getTime(); 16 | } 17 | 18 | describe("validation.js", function(){ 19 | 20 | before(function(){ 21 | // suppress debug log output for tests 22 | log.level = 4; 23 | }); 24 | 25 | after(function(){ 26 | // restore default log level 27 | log.level = config.LOG_LEVEL 28 | }); 29 | 30 | describe("#has_key(inode, params)", function() { 31 | it("should validate an access_key", function() { 32 | var params = { access_key: GOOD_KEY }; 33 | var result = validate.has_key(INODE, params); 34 | 35 | expect(result).to.be.true; 36 | }); 37 | 38 | it("should reject an incorrect access_key", function() { 39 | var params = { access_key: BAD_KEY }; 40 | var result = validate.has_key(INODE, params); 41 | 42 | expect(result).to.be.false; 43 | }); 44 | 45 | }); 46 | 47 | describe("#is_authorized(inode, method, params)", function() { 48 | 49 | it("should validate an access_key", function() { 50 | var params = { access_key: GOOD_KEY }; 51 | var result = validate.is_authorized(INODE, GET, params); 52 | 53 | expect(result).to.be.true; 54 | }); 55 | 56 | it("should reject an incorrect access key", function(){ 57 | var params = { access_key: BAD_KEY }; 58 | var result = validate.is_authorized(INODE, GET, params); 59 | 60 | expect(result).to.be.false; 61 | }); 62 | 63 | it("should validate an access token", function(){ 64 | var params = { access_token: utils.sha1_to_hex(GOOD_KEY + GET) }; 65 | var result = validate.is_authorized(INODE, GET, params); 66 | 67 | expect(result).to.be.true; 68 | }); 69 | 70 | it("should reject an access token for wrong method", function(){ 71 | var params = { access_token: utils.sha1_to_hex(GOOD_KEY + "POST") }; 72 | var result = validate.is_authorized(INODE, GET, params); 73 | 74 | expect(result).to.be.false; 75 | }); 76 | 77 | it("should reject wrong access token", function(){ 78 | var params = { access_token: utils.sha1_to_hex(BAD_KEY + GET) }; 79 | var result = validate.is_authorized(INODE, GET, params); 80 | 81 | expect(result).to.be.false; 82 | }); 83 | 84 | it("should validate a future time token", function() { 85 | var expires = setExpire(30); 86 | var params = { 87 | access_token : utils.sha1_to_hex(GOOD_KEY + GET + expires), 88 | expires : expires 89 | }; 90 | var result = validate.is_authorized(INODE, GET, params) 91 | 92 | expect(result).to.be.true; 93 | }); 94 | 95 | it("should reject an expired time token", function() { 96 | var expires = setExpire(-1); 97 | var params = { 98 | access_token : utils.sha1_to_hex(GOOD_KEY + GET + expires), 99 | expires : expires 100 | }; 101 | var result = validate.is_authorized(INODE, GET, params); 102 | 103 | expect(result).to.be.false; 104 | }); 105 | 106 | it("should validate HEAD requests", function(){ 107 | var params = { access_token: utils.sha1_to_hex(BAD_KEY + GET) }; 108 | var result = validate.is_authorized(INODE, "HEAD", params); 109 | 110 | expect(result).to.be.true; 111 | }); 112 | 113 | it("should validate OPTIONS requests", function(){ 114 | var params = { access_token: utils.sha1_to_hex(BAD_KEY + GET) }; 115 | var result = validate.is_authorized(INODE, "OPTIONS", params); 116 | 117 | expect(result).to.be.true; 118 | }); 119 | 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /tools/compress_blocks.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var zlib = require("zlib"); 3 | var files; 4 | var block_location; 5 | var total; 6 | 7 | var process_next_file = function process_next_file(){ 8 | console.log((((total - files.length) / total) * 100) + "% completed"); 9 | 10 | if (files.length > 0) { 11 | process_file(files.pop()); 12 | } else { 13 | console.log("No more blocks to compress"); 14 | } 15 | }; 16 | 17 | var process_file = function process_file(file){ 18 | var selected_file = block_location + "/" + file; 19 | 20 | if(selected_file.indexOf(".gz") < 0 && selected_file.indexOf(".json") < 0){ 21 | var on_finish = function on_finish(){ 22 | writer.removeListener("finish", on_finish); 23 | fs.unlink(selected_file, function(err){ 24 | if (err) { 25 | console.log(err); 26 | return; 27 | } 28 | process_next_file(); 29 | }); 30 | }; 31 | 32 | var writer = fs.createWriteStream(selected_file + ".gz"); 33 | writer.on("finish", on_finish); 34 | fs.createReadStream(selected_file).pipe(zlib.createGzip()).pipe(writer); 35 | 36 | } else { 37 | process_next_file(); 38 | } 39 | 40 | }; 41 | 42 | if(process.argv[2]){ 43 | 44 | block_location = process.argv[2] 45 | 46 | fs.readdir(block_location, function(err, _files){ 47 | if (err) { 48 | console.log(err); 49 | return; 50 | } 51 | 52 | files = _files; 53 | total = files.length; 54 | process_next_file(); 55 | }); 56 | } else { 57 | console.log("usage: compress_blocks.js "); 58 | } 59 | -------------------------------------------------------------------------------- /tools/compression_notes.txt: -------------------------------------------------------------------------------- 1 | block compression notes 2 | 3 | WAVE source directory size: 476128K (465M) 4 | Uncompressed JSFS blocks + inodes: 476224K (466M) 5 | Compressed JSFS blocks: 425460K (416M) 6 | ~11% savings compressed 7 | 8 | 9 | GET uncompressed blocks 10 | 379MB 11 | 12 | jason@Argo:~/Development/jsfs$ curl -o ./output/test1.img http://localhost:7302/update.img 13 | % Total % Received % Xferd Average Speed Time Time Time Current 14 | Dload Upload Total Spent Left Speed 15 | 100 507M 100 507M 0 0 55.2M 0 0:00:09 0:00:09 --:--:-- 98.7M 16 | 17 | 18 | GET compressed blocks 19 | 914MB 20 | 21 | jason@Argo:~/Development/jsfs$ curl -o ./output/test2.img http://localhost:7302/update.img 22 | % Total % Received % Xferd Average Speed Time Time Time Current 23 | Dload Upload Total Spent Left Speed 24 | 100 507M 100 507M 0 0 25.9M 0 0:00:19 0:00:19 --:--:-- 27.7M 25 | 26 | 27 | -------------------------------------------------------------------------------- /tools/jsfsck.js: -------------------------------------------------------------------------------- 1 | // jsfsck - check and repair jsfs filesystems 2 | // 3 | // node jsfsck.js 4 | // 5 | // options 6 | // -V verbose 7 | // -y automatically fix any errors encountered without prompting 8 | // -r interactively repair errors (ask before fixing each type of error) 9 | // -b re-balance blocks across configured storage locations 10 | // 11 | // if no repair options are specified, only analysis is performed 12 | 13 | // globals 14 | var fs = require("fs"); 15 | var config_path = null 16 | var mode = "i"; 17 | var good_inodes = 0; 18 | var bad_inodes = []; 19 | var pool_size_in_bytes = 0; 20 | var on_disk_size_in_bytes = 0; 21 | 22 | // parse command line parameters 23 | config_path = process.argv[2]; 24 | mode = process.argv[3]; 25 | 26 | if(!config_path || !mode){ 27 | console.log("usage: node jsfsck.js "); 28 | process.exit(0); 29 | } 30 | 31 | // load configuration 32 | var config = null; 33 | try{ 34 | config = require(config_path); 35 | if(config.STORAGE_LOCATIONS.length < 1){ 36 | console.log("No storage locations configured, exiting"); 37 | process.exit(0); 38 | } 39 | } catch(exception) { 40 | console.log("Error reading configuration: " + exception); 41 | } 42 | 43 | 44 | // load inode index from primary storage location 45 | var files = fs.readdirSync(config.STORAGE_LOCATIONS[0].path); 46 | for(file in files){ 47 | var selected_file = files[file]; 48 | if(selected_file.indexOf(".json") > -1){ 49 | 50 | // parse inode 51 | try{ 52 | var inode = JSON.parse(fs.readFileSync(config.STORAGE_LOCATIONS[0].path + selected_file)); 53 | 54 | // test each block 55 | var missing_blocks = 0; 56 | for(block in inode.blocks){ 57 | var selected_block = inode.blocks[block]; 58 | 59 | var block_location = null; 60 | for(location in config.STORAGE_LOCATIONS){ 61 | var selected_location = config.STORAGE_LOCATIONS[location]; 62 | try{ 63 | var block_stats = fs.statSync(selected_location.path + selected_block.block_hash); 64 | pool_size_in_bytes += block_stats.size; 65 | block_location = selected_location.path; 66 | } catch(ex) { 67 | // do nothing 68 | //console.log(ex); 69 | } 70 | } 71 | 72 | // keep track of missing blocks 73 | if(block_location){ 74 | //good_inodes.push(selected_file); 75 | } else { 76 | missing_blocks++; 77 | //bad_inodes.push(selected_file); 78 | } 79 | } 80 | 81 | if(missing_blocks > 0){ 82 | bad_inodes.push(selected_file); 83 | } else { 84 | good_inodes++; 85 | } 86 | } catch(ex) { 87 | bad_inodes.push(selected_file); 88 | } 89 | } else { 90 | // do nothing? 91 | } 92 | } 93 | 94 | 95 | // calculate total disk useage for pool (includes inode files) 96 | for(location in config.STORAGE_LOCATIONS){ 97 | var selected_location = config.STORAGE_LOCATIONS[location]; 98 | var files = fs.readdirSync(selected_location.path); 99 | for(file in files){ 100 | var selected_file = files[file]; 101 | var file_stats = fs.statSync(selected_location.path + selected_file); 102 | on_disk_size_in_bytes += file_stats.size; 103 | } 104 | } 105 | 106 | 107 | console.log("config_path: " + config_path); 108 | console.log("mode: " + mode); 109 | console.log("good_inodes: " + good_inodes); 110 | console.log("bad_inodes: " + bad_inodes.length); 111 | console.log("pool_size_in_bytes:" + pool_size_in_bytes); 112 | console.log("on_disk_size_in_bytes: " + on_disk_size_in_bytes); 113 | console.log("deduplication rate: " + (on_disk_size_in_bytes / pool_size_in_bytes)*100 + "%"); 114 | 115 | var total_files = good_inodes + bad_inodes.length; 116 | var pool_size_in_megabytes = (pool_size_in_bytes / 1024) / 1024; 117 | var on_disk_size_in_megabytes = (on_disk_size_in_bytes / 1024) /1024; 118 | var duplicate_percentage = 100 - ((on_disk_size_in_bytes / pool_size_in_bytes)*100); 119 | 120 | console.log(pool_size_in_megabytes + "MB in " + total_files + " files. " + bad_inodes.length + " errors, " + duplicate_percentage + "% duplicate data"); 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /tools/migrate_superblock.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var crypto = require("crypto"); 3 | 4 | // open superblock file 5 | console.log("Loading superblock..."); 6 | var superblock = JSON.parse(fs.readFileSync("superblock.json")); 7 | console.log("Superblock loaded, indexing inodes..."); 8 | var inodes_total = 0; 9 | for(inode in superblock){ 10 | if(superblock.hasOwnProperty(inode)){ 11 | inodes_total++; 12 | } 13 | } 14 | var inodes_remaining = inodes_total; 15 | console.log("Superblock indexed, " + inodes_remaining + " inodes to process"); 16 | 17 | // load next inode 18 | for(inode in superblock){ 19 | if(superblock.hasOwnProperty(inode)){ 20 | var selected_inode = superblock[inode]; 21 | 22 | // generate inode filename 23 | var shasum = crypto.createHash("sha1"); 24 | shasum.update(selected_inode.url); 25 | var inode_filename = shasum.digest("hex") + ".json"; 26 | log("Inode filename: " + inode_filename); 27 | 28 | // check if inode file exists 29 | try{ 30 | // if inode file exists, load existing file 31 | var existing_inode = JSON.parse(fs.readFileSync(inode_filename)); 32 | log("Inode file already exists, comparing versions..."); 33 | // compare versions 34 | if(inode.version > existing_inode.version){ 35 | // replace existing file if the current inode is newer 36 | log("Replacing existing inode file with newer version"); 37 | fs.writeFileSync(inode_filename, JSON.stringify(selected_inode)); 38 | } 39 | }catch(ex){ 40 | // if inode doesn't exist, save the one we have 41 | log("Writing inode file to disk"); 42 | fs.writeFileSync(inode_filename, JSON.stringify(selected_inode)); 43 | } 44 | 45 | inodes_remaining--; 46 | log("Inode " + selected_inode.fingerprint + " processed, " + inodes_remaining + " inodes remaining"); 47 | } 48 | } 49 | 50 | console.log("Superblock migration complete!"); 51 | 52 | function log(message){ 53 | console.log(inodes_total + "/" + inodes_remaining + " - " + message); 54 | } 55 | 56 | -------------------------------------------------------------------------------- /tools/movesomeblocks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | for file in $(ls -1 -f -p $1 | grep -v / | grep -v "\.json" | head -n$3) 3 | do 4 | mv $1$file $2 5 | done 6 | -------------------------------------------------------------------------------- /tools/pool_size.js: -------------------------------------------------------------------------------- 1 | // globals 2 | var fs = require("fs"); 3 | var config_path = null 4 | var stored_bytes = 0; 5 | 6 | // parse command line parameters 7 | config_path = process.argv[2]; 8 | 9 | if(!config_path){ 10 | console.log("usage: node pool_size.js "); 11 | process.exit(0); 12 | } 13 | 14 | // load configuration 15 | var config = null; 16 | try{ 17 | config = require(config_path); 18 | if(config.STORAGE_LOCATIONS.length < 1){ 19 | console.log("No storage locations configured, exiting"); 20 | process.exit(0); 21 | } 22 | } catch(exception) { 23 | console.log("Error reading configuration: " + exception); 24 | } 25 | 26 | 27 | // load inode index from primary storage location 28 | var files = fs.readdirSync(config.STORAGE_LOCATIONS[0].path); 29 | for(file in files){ 30 | var selected_file = files[file]; 31 | if(selected_file.indexOf(".json") > -1){ 32 | 33 | // parse inode 34 | try{ 35 | var inode = JSON.parse(fs.readFileSync(config.STORAGE_LOCATIONS[0].path + selected_file)); 36 | stored_bytes += inode.file_size; 37 | console.log("Bytes stored: " + stored_bytes); 38 | } catch (exception) { 39 | console.log("Error parsing inode: " + exception); 40 | } 41 | } 42 | } 43 | 44 | console.log("Processing complete"); 45 | --------------------------------------------------------------------------------