{"id":23740112,"url":"https://github.com/cobbzilla/mobiletto","last_synced_at":"2025-09-04T15:32:00.265Z","repository":{"id":57827397,"uuid":"525462763","full_name":"cobbzilla/mobiletto","owner":"cobbzilla","description":"A storage abstraction layer","archived":false,"fork":false,"pushed_at":"2023-07-21T02:15:43.000Z","size":1593,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-12-01T04:49:54.954Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cobbzilla.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-08-16T16:37:59.000Z","updated_at":"2024-10-12T19:39:49.000Z","dependencies_parsed_at":"2022-09-01T10:01:59.402Z","dependency_job_id":null,"html_url":"https://github.com/cobbzilla/mobiletto","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fmobiletto","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fmobiletto/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fmobiletto/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fmobiletto/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cobbzilla","download_url":"https://codeload.github.com/cobbzilla/mobiletto/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":231970948,"owners_count":18453930,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-31T09:47:06.427Z","updated_at":"2024-12-31T09:47:07.075Z","avatar_url":"https://github.com/cobbzilla.png","language":"JavaScript","funding_links":["https://www.patreon.com/cobbzilla"],"categories":[],"sub_categories":[],"readme":"Mobiletto\n=========\n\nMobiletto is a JavaScript storage abstraction layer, with optional transparent client-side encryption.\n\n# Contents\n* [Why Mobiletto?](#Why-Mobiletto?)\n* [Quick start](#Quick-start)\n* [Mobiletto CLI](#mobiletto-cli)\n* [Source](#Source)\n* [Installation](#Installation)\n  * [Minimal install](#Minimal-install)\n* [Support and Funding](#Support-and-Funding)\n* [Basic usage](#Basic-usage)\n* [Metadata](#Metadata)\n* [Alternate import style](#Alternate-import-style)\n* [Caching](#Caching)\n* [Mirroring](#Mirroring)\n* [Transparent encryption](#Transparent-encryption)\n* [Key rotation](#Key-rotation)\n* [Driver interface](#Driver-interface)\n* [Logging](#Logging)\n\n# Read this in another language\nThis README.md document has been translated, via [hokeylization](https://github.com/cobbzilla/hokeylization), into\n**[every language supported by Google Translate](https://cloud.google.com/translate/docs/languages)!**\n\nI'm certain it's not perfect, but I hope it's better than nothing!\n\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇸🇦 Arabic](docs/ar/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇧🇩 Bengali](docs/bn/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇩🇪 German](docs/de/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇺🇸 English](docs/en/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇪🇸 Spanish](docs/es/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇫🇷 French](docs/fr/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇹🇩 Hausa](docs/ha/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇮🇳 Hindi](docs/hi/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇮🇩 Indonesian](docs/id/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇮🇹 Italian](docs/it/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇯🇵 Japanese](docs/ja/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇰🇷 Korean](docs/ko/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇮🇳 Marathi](docs/mr/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇵🇱 Polish](docs/pl/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇧🇷 Portuguese](docs/pt/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇷🇺 Russian](docs/ru/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇰🇪 Swahili](docs/sw/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇵🇭 Tagalog](docs/tl/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇹🇷 Turkish](docs/tr/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇵🇰 Urdu](docs/ur/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇻🇳 Vietnamese](docs/vi/README.md)\n\u0026nbsp;\u0026nbsp;\u0026nbsp;[🇨🇳 Chinese](docs/zh/README.md)\n\u003cbr/\u003e\n\u0026nbsp;\u0026nbsp;\u0026nbsp;**[📚 ... All Languages ...](docs/README.md)**\n----\n\n### Is there a problem with this translation of the README?\nThis particular translation of the original [README](https://github.com/cobbzilla/mobiletto/blob/master/README.md)\nmay be flawed -- *corrections are very welcome!* Please send a [pull request on GitHub](https://github.com/cobbzilla/mobiletto/pulls),\nor if you're not comfortable doing that, [open an issue](https://github.com/cobbzilla/mobiletto/issues)\n\nWhen you create a new GitHub issue about a translation, please do:\n* include the page URL (copy/paste from browser address bar)\n* include the exact text that is wrong (copy/paste from browser)\n* please describe what is wrong -- is the translation incorrect? is the formatting broken somehow?\n* kindly offer a suggestion of a better translation, or how the text should be properly formatted\n* **Thank you!**\n\n## Why Mobiletto?\n\n### Goodbye vendor lock-in!\nThe various cloud storage providers have incompatible APIs. Even those that strive for \"S3 compatibility\"\nhave idiosyncratic behaviors.\n\nWhen you choose a particular storage vendor for your app, if you code directly to their API, your app\nis now dependent on that service. As time goes by and code accumulates, changing vendors becomes\nincreasingly untenable. Welcome to the fun world of vendor lock-in!\n\nMobiletto was designed to solve this problem. By coding your app to mobiletto's API, you can easily\nchange storage providers and know that your app's storage layer will behave identically.\n\n### Extensive testing\nAll drivers are tested for identical behavior with 60+ tests for each driver.\nWe test all drivers with every combination of:\n* Encryption: both enabled and disabled\n* Redis cache: both enabled and disabled\n\nThis approach gives us peace-of-mind that mobiletto will behave the same regardless of which driver you use,\nand regardless of whether you enable caching and/or encryption.\n\n### Driver support\nCurrent Mobiletto storage drivers:\n* `s3`: Amazon S3\n* `b2`: Backblaze B2\n* `local`: local filesystem\n* `indexeddb`: IndexedDB storage\n\n*Contributions to support more cloud storage providers are very welcome!*\n\n## Quick Start\nA short example using the mobiletto `s3` driver.\n\nThis code would run the same with any other mobiletto storage driver.\n\n    const storage = require('mobiletto')\n    const bucket = await storage.connect('s3', aws_key, aws_secret, {bucket: 'bk'})\n\n    // list objects: returns array of metadata objects\n    const listing = await bucket.list()\n    const dirList = await bucket.list('some/dir/')\n    const everything = await bucket.list('', {recursive: true})\n\n    // write an entire file\n    let bytesWritten = await bucket.writeFile('some/path', someBufferOfData)\n\n    // write a file from a stream/generator\n    bytesWritten = await bucket.write('some/path', streamOrGenerator)\n\n    // read an entire file\n    // returns null if an exception would otherwise be thrown\n    const bufferOrNull = await bucket.safeReadFile('some/path')\n\n    // stream-read a file, passing data to callback\n    const bytesRead = await bucket.read('some/path', (chunk) =\u003e { ...do something with chunk... } )\n\n    // remove a file, returns the path removed\n    let removed = await bucket.remove('some/path')  // removed is a string\n\n    // remove a directory, returns array of paths removed\n    removed = await bucket.remove('some/directory', {recursive: true})  // removed is now an array!\n\n## mobiletto-cli\nMobiletto is intended to be used as a library by other JavaScript code.\n\nTo work with mobiletto at the command-line, use [mobiletto-cli](https://www.npmjs.com/package/mobiletto-cli)\n\n## Source\n* [mobiletto on GitHub](https://github.com/cobbzilla/mobiletto)\n* [mobiletto on npm](https://www.npmjs.com/package/mobiletto)\n\n## Support and Funding\nI would be very thankful for any [contribution via Patreon](https://www.patreon.com/cobbzilla)\n\n## Installation\nInstall using `npm` or `yarn`. You probably want the `lite` version that does not include all the\ntranslated README files:\n\n    npm install mobiletto-lite\n    yarn add mobiletto-lite\n\nIf you really want the README files in every language, install the full version:\n\n    npm install mobiletto\n    yarn add mobiletto\n\n### Minimal install\nThe `mobiletto` and `mobiletto-lite` packages include support for all mobiletto storage drivers.\n\nIf you know the storage driver(s) that you will need ahead of time, you can lighten your app's\nfootprint by installing the `mobiletto-base` package, and then one or more of the `mobiletto-driver-` packages:\n\n#### Minimal installation with npm\n\n    npm install mobiletto-base                # install driver-less mobiletto\n\n    npm install mobiletto-driver-s3           # install Amazon S3 driver\n    npm install mobiletto-driver-b2           # install Backblaze B2 driver\n    npm install mobiletto-driver-local        # install local filesystem driver\n    npm install mobiletto-driver-indexeddb    # install IndexedDB driver\n\n#### Minimal installation with yarn\n\n    yarn add mobiletto-base                # install driver-less mobiletto\n\n    yarn add mobiletto-driver-s3           # install Amazon S3 driver\n    yarn add mobiletto-driver-b2           # install Backblaze B2 driver\n    yarn add mobiletto-driver-local        # install local filesystem driver\n    yarn add mobiletto-driver-indexeddb    # install IndexedDB driver\n\n----\n## Basic usage\nA much more extensive example, showing most of the features offered:\n\n    // When using mobiletto or mobiletto-lite, all drivers are included\n    const { mobiletto } = require('mobiletto')\n\n    // When using mobiletto-base, use registerDriver to load a storage driver before connecting \n    const { registerDriver, mobiletto } = = require('mobiletto-base')\n    registerDriver('s3', require('mobiletto-driver-s3'))\n    registerDriver('b2', require('mobiletto-driver-b2'))\n    registerDriver('local', require('mobiletto-driver-local'))\n    registerDriver('indexeddb', require('mobiletto-driver-indexeddb'))\n\n    // General usage\n    const api = await mobiletto(driverName, key, secret, opts)\n\n    // To use 'local' driver:\n    //   * key: base directory\n    //   * secret: ignored, can be null\n    //   * opts object:\n    //     * readOnly: optional, never change anything on the filesystem; default is false\n    //     * fileMode: optional, permissions used when creating new files, default is 0600. can be string or integer\n    //     * dirMode: optional, permissions used when creating new directories, default is 0700. can be string or integer\n    const local = await mobiletto('local', '/home/ubuntu/tmp', null, {fileMode: 0o0600, dirMode: '0700'})\n\n    // To use 's3' driver:\n    //   * key: AWS Access Key ID\n    //   * secret: AWS Secret Key\n    //   * opts object:\n    //     * readOnly: optional, never change anything on the bucket; default is false\n    //     * bucket: required, name of the S3 bucket\n    //     * region: optional, the AWS region to communicate with, default is us-east-1\n    //     * prefix: optional, all read/writes within the S3 bucket will be under this prefix\n    //     * delimiter: optional, directory delimiter, default is '/' (note: always '/' when encryption is enabled)\n    const s3 = await mobiletto('s3', aws_key, aws_secret, {bucket: 'bk', region: 'us-east-1'})\n\n    // To use 'b2' driver:\n    //   * key: Backblaze Key ID\n    //   * secret: Backblaze Application Key\n    //   * opts object:\n    //     * readOnly: optional, never change anything on the bucket; default is false\n    //     * bucket: required, the ID (**not the name**) of the B2 bucket\n    //     * prefix: optional, all read/writes within the B2 bucket will be under this prefix\n    //     * delimiter: optional, directory delimiter, default is '/' (note: always '/' when encryption is enabled)\n    //     * partSize: optional, large files will be split into chunks of this size when uploading\n    const b3 = await mobiletto('b2', b2_key_id, b2_app_key, {bucket: 'bk', partSize: 10000000})\n\n    // To use 'indexeddb' driver:\n    //   * key: IndexedDB database name\n    //   * secret: ignored, can be null\n    //   * opts object:\n    //     * indexedDB: required, the indexedDB object (an instance of IDBFactory)\n    const idb = await mobiletto('indexeddb', 'my_db', null, { indexedDB })\n\n    // List files\n    api.list()  // --\u003e returns an array of metadata objects\n\n    // List files recursively\n    api.list({ recursive: true })\n\n    // List files in a directory\n    const path = 'some/path'\n    api.list(path)\n    api.list(path, { recursive: true }) // also supports recursive flag\n\n    // Visit files in a directory -- visitor function must be async\n    api.list(path, { visitor: myAsyncFunc })\n    api.list(path, { visitor: myAsyncFunc, recursive: true })\n\n    // The `list` method throws MobilettoNotFoundError if the path does not exist\n    // When you call `safeList` on a non-existent path, it returns an empty array\n    api.safeList('/path/that/does/not/exist') // returns []\n    \n    // Read metadata for a file\n    api.metadata(path)    // returns metadata object\n\n    // The `metadata` method throws MobilettoNotFoundError if the path does not exist\n    // When you call `safeMetadata` on a non-existent path, it returns null\n    api.safeMetadata('/tmp/does_not_exist') // returns null\n    \n    // Read a file\n    // Provide a callback that writes the data someplace\n    const callback = (chunk) =\u003e { ... write chunk somewhere ...  } \n    api.read(path, callback)  // returns count of bytes read\n\n    // Read an entire file at once\n    const data = await api.readFile(path)  // returns a byte Buffer of the file contents\n    \n    // Read an entire file at once\n    // returns null if an exception would otherwise be thrown\n    const bufferOrNull = await bucket.safeReadFile('some/path')\n\n    // Write a file\n    // Provide a generator function that yields chunks of data \n    const generator = function* () {\n      while ( ... more-data-to-return ... ) {\n        data = ... load-data ...\n        yield data\n      }\n    } \n    local.api(path, generator)  // returns count of bytes written\n\n    // Write an entire file at once (convenience method)\n    await api.writeFile(path, bufferOrString)   // returns count of bytes written\n\n    // Delete a file\n    // Quiet param is optional (default false), when set errors will not be thrown if the path does not exist\n    // Always returns a value or throws an error.\n    // Return value may be a single string of the file removed, or an array of all files removed (driver-dependent)\n    const quiet = true\n    api.remove(path, {quiet}) // returns single path removed\n\n    // Recursively delete a directory and do it quietly (do not report errors)\n    const recursive = true\n    const quiet = true\n    api.remove(path, {recursive, quiet}) // returns array of paths removed\n\n## Metadata\nThe `metadata` command returns metadata about a single filesystem entry.\nLikewise, the return value from the `list` command is an array of metadata objects.\n\nA metadata object looks like this:\n\n    {\n      \"name\": \"fully/qualified/path/to/file\",\n      \"type\": \"entry-type\",\n      \"size\": size-in-bytes,\n      \"ctime\": creation-time-epoch-millis,\n      \"mtime\": modification-time-epoch-millis\n    }\n\nThe `type` property can be `file`, `dir`, `link`, or `special`.\n\nDepending on the type of driver, a `list` command may not return all fields. The `name` and `type` properties\nshould always be present. A subsequent `metadata` command will return all available properties.\n\n## Alternate import style\nImport the fully-scoped module and use the `connect` function:\n\n    const storage = require('mobiletto')\n    const opts = {bucket: 'bk', region: 'us-east-1'}\n    const s3 = await storage.connect('s3', aws_key, aws_secret, opts)\n    const objectData = await s3.readFile('some/path')\n\n## Caching\nMobiletto works best with a \u003ca href=\"https://redis.io\"\u003eredis\u003c/a\u003e cache.\n\nMobiletto will attempt to connect to a redis instance on 127.0.0.1:6379\n\nYou can override either of these:\n* Set the `MOBILETTO_REDIS_HOST` env var, mobiletto connect here instead of localhost\n* Set the `MOBILETTO_REDIS_PORT` env var, this port will be used\n\nMobiletto will store all of its redis keys with the prefix `_mobiletto__`. You can change this\nby setting the `MOBILETTO_REDIS_PREFIX` env var.\n\nYou can also set per-connection caching with the `opts.redisConfig` object:\n\n    const redisConfig = {\n        enabled: true,     // optional, default is true. if false other props are ignored\n        host: '127.0.0.1',\n        port: 6379,\n        prefix: '_mobiletto__'\n    }\n    const opts = { redisConfig, bucket: 'bk', region: 'us-east-1' }\n    const s3 = await storage.connect('s3', aws_key, aws_secret, opts)\n\n### Don't want redis caching?\nTo disable: pass `enabled: false` in your `opts.redisConfig` object when you establish your connection.\n\nAs discussed below, disabling caching will have an adverse effect on performance and incur more requests\nto storage than you really need to.\n\n### Caching guidance\n**Encrypted storage**: reading/writing encrypted storage is only a little slower than normal,\nbut navigating around directories (which some things do) is fairly expensive. Using a redis cache\nwill give you a significant performance boost.\n\nThe default cache is safe, but doesn't perform well if you have a lot of write/remove operations.\nAny write or remove operation invalidates the entire cache, ensuring subsequent reads will see the\nlatest changes.\n\n### CLI tools\nIf you're using a CLI tool like [mobiletto-cli](https://www.npmjs.com/package/mobiletto-cli),\nyou'll definitely want the redis cache enabled, as it lasts across invocations of the `mo` command.\n\n## Mirroring\n\n    // Copy a local filesystem mobiletto to S3\n    s3.mirror(local)\n\n    // Mirror a local subdirectory from one mobiletto to an S3 mobiletto, with it's own subdirectory\n    local.mirror(s3, 'some/local-folder', 'some/s3-folder')\n\nThe `mirror` command performs a one-time copy of all files from one mobiletto to another.\nIt does not run any process to maintain the mirror over time. Run the `mirror` command again\nto synchronize any missing files.\n\nThe return value from `mirror` is a simple object with counters for how many files were successfully\nmirrored and how many files had errors:\n\n    {\n      success: count-of-files-mirrored,\n      errors: count-of-files-with-errors\n    }\n\nWARNING: Mirroring large data sets can be very time-consuming and bandwidth-intensive\n\nWith the `mirror` call semantics it can sometimes be confusing to understand who is the\nreader and who is the writer. Imagine it like an assignment statement: the \"left-hand mobiletto\"\nis the thing being assigned to (mirrored data written), and the \"right-hand mobiletto\" (the\nargument to the `mirror` method) is the value being assigned (mirrored data is read).\n\n## Transparent encryption\nEnable transparent client-side encryption:\n\n    // Pass encryption parameters\n    const encryption = {\n      // key is required, must be \u003e= 16 chars\n      key: randomstring.generate(128),\n\n      // optional, the default is to derive IV from key\n      // when set, IV must be \u003e= 16 chars\n      iv: randomstring.generate(128),\n\n      // optional, the default is aes-256-cbc\n      algo: 'aes-256-cbc'\n    }\n    const api = await mobiletto(driverName, key, secret, opts, encryption)\n\n    // Subsequent write operations will encrypt data (client side) when writing\n    // Subsequent read operations will decrypt data (client side) when reading\n\nWhat's happening? A separate \"directory entry\" (dirent) directory (encrypted) tracks what files are in that\ndirectory (aka the dirent directory).\n* The `list` command reads the directory entry files, decrypts each path listed; then returns metadata for each file\n  * `list` commands are more inefficient, especially for directories with a large number of files\n* The `write` command writes dirent files in each parent's dirent directory, recursively; then writes the file\n  * `write` commands will incur O(N) writes, with N = depth in the directory hierarchy\n* The `remove` command removes the corresponding dirent file, and its parent if empty, recursively; then removes the file\n  * Non-recursive `remove` commands will incur O(N) reads and potentially as many deletes, with N = depth in the directory hierarchy\n  * Recursive `remove` commands on large and deep filesystems can be expensive\n\nNote that even with client-side encryption enabled, an adversary with full visibility into your encrypted server-side\nstorage, even without the key, can still see the total number of directories and how many files are in each, and with\nsome effort, discover some or all of the overall structure of the directory hierarchy.\n*Note: Use a relatively flat structure for better security.*\nThe adversary would not know the names of the directories/files unless they also knew your encryption\nkey or had otherwise successfully cracked the encryption. All bets are off then!\n\n### Performance and caching\nOperations on encrypted storage can be slow. Recursive listings and removals can be very slow.\nCaching via redis helps tremendously, but note that the cache is flushed upon any writes or removes.\n\n## Key rotation\nCreate a mobiletto with your new key, then mirror the old data into it:\n\n    const storage = require('mobiletto')\n\n    const oldEncryption = { key: .... }\n    const oldStorage = await storage.connect('s3', aws_key, aws_secret, {bucket: 'bk', region: 'us-east-1'}, oldEncryption)\n\n    const newEncryption = { key: .... }\n    const newStorage = await storage.connect('s3', aws_key, aws_secret, {bucket: 'zz', region: 'us-east-1'}, newEncryption)\n\n    newStorage.mirror(oldStorage) // if oldStorage is very large, this may take a looooooong time...\n\n## Driver interface\nA driver is any JS file that exports a 'storageClient' function with this signature:\n\n    function storageClient (key, secret, opts)\n\n* `key`: a string, your API key (for the `local` driver this is the base directory)\n* `secret`: a string, your API secret (can be omitted for the `local` driver)\n* `opts`: an object, the properties are per-driver:\n  * For `local`, the `fileMode` and `dirMode` properties determine how new creating files and directories are created\n  * For `s3`, the `bucket` property is required. Optional properties are:\n    * `region`: the S3 region, default is us-east-1\n    * `prefix`: a prefix to prepend to all S3 paths, default is the empty string\n    * `delimiter`: the directory delimiter, default is '/'\n\nThe object that the storageClient function returns must define these functions:\n\n    // Test the driver before using, ensure proper configuration\n    async testConfig ()\n\n    // List files in path (or from base-directory)\n    // If recursive is true, list recursively\n    // If visitor is defined, it will be an async function. await the visitor function on each file found\n    // Otherwise, perform the listing and return an array of objects\n    async list (path, recursive = false, visitor = null)  // path may be omitted\n    \n    // Read metadata for a path\n    async metadata (path)\n    \n    // Read a file, return bytes read\n    // callback receives a chunk of data. endCallback is called at end-of-stream\n    async read (path, callback, endCallback = null)\n\n    // Write a file, return bytes written\n    // driver must be able to handle a generator or a stream\n    async write (path, generatorOrReadableStream)\n\n    // Remove a file, or recursively delete a directory\n    // returns a string of a single path removed, or an array of multiple paths removed\n    async remove (path, recursive = false, quiet = false)\n\n## Logging\nMobiletto uses the [winston](https://www.npmjs.com/package/winston) logging library.\n\nLogs **will** contain file paths and error messages, but will **never** contain keys, secrets,\nor any other connection configuration information.\n\n### Log level\nUse the `MOBILETTO_LOG_LEVEL` environment variable to set the log level, using one\nof the `npm` levels defined in [https://www.npmjs.com/package/winston#logging-levels](https://www.npmjs.com/package/winston#logging-levels)\n\nThe default level is `error`. The most verbose level is `silly`, although currently mobiletto\ndoes not log at levels below `debug`\n\n    MOBILETTO_LOG_LEVEL=silly   # maximum logs!\n\n### Log file\nBy default, the logger writes to the console. To send logs to a file, set the `MOBILETTO_LOG_FILE`\nenvironment variable. When logging to a file, logs will no longer be written to the console.\n\n    MOBILETTO_LOG_FILE=/var/my_mobiletto_log\n\nTo turn off logging:\n\n    MOBILETTO_LOG_FILE=/dev/null\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcobbzilla%2Fmobiletto","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcobbzilla%2Fmobiletto","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcobbzilla%2Fmobiletto/lists"}