Comparing version 0.1.0 to 0.1.1
# Disklet | ||
## 0.1.1 | ||
* Clean up the documentation | ||
* Add missing typings for `mapFolders` | ||
* Add the `makeLoggedFolder` helper | ||
## 0.1.0 | ||
* Initial release |
@@ -75,9 +75,8 @@ 'use strict'; | ||
* The file names and expanded into paths, and the result is a flat array. | ||
* The result order is nondeterministic, since everything runs in parallel. | ||
*/ | ||
function mapAllFiles (folder, f) { | ||
function recurse (folder, prefix, f) { | ||
function recurse (folder, f, prefix) { | ||
return Promise.all([ | ||
mapFiles(folder, function (file, name) { return f(file, prefix + name, folder); }), | ||
mapFolders(folder, function (folder, name) { return recurse(folder, prefix + name + '/', f); } | ||
mapFolders(folder, function (folder, name) { return recurse(folder, f, prefix + name + '/'); } | ||
) | ||
@@ -92,3 +91,3 @@ ]).then(function (ref) { | ||
return recurse(folder, '', f) | ||
return recurse(folder, f, '') | ||
} | ||
@@ -219,2 +218,88 @@ | ||
function logConsole (path, operation) { | ||
console.log((operation + " \"" + path + "\"")); | ||
} | ||
function log (path, operation, opts) { | ||
var f = opts.callback != null ? opts.callback : logConsole; | ||
if (opts.verbose || /set|delete/.test(operation)) { | ||
f(path, operation); | ||
} | ||
} | ||
var LoggedFile = function LoggedFile (file, path, opts) { | ||
this._file = file; | ||
this._path = path; | ||
this._opts = opts; | ||
}; | ||
LoggedFile.prototype.delete = function delete$1 () { | ||
log(this._path, 'delete file', this._opts); | ||
return this._file.delete() | ||
}; | ||
LoggedFile.prototype.getData = function getData () { | ||
log(this._path, 'get data', this._opts); | ||
return this._file.getData() | ||
}; | ||
LoggedFile.prototype.getText = function getText () { | ||
log(this._path, 'get text', this._opts); | ||
return this._file.getText() | ||
}; | ||
LoggedFile.prototype.setData = function setData (data) { | ||
log(this._path, 'set data', this._opts); | ||
return this._file.setData(data) | ||
}; | ||
LoggedFile.prototype.setText = function setText (text) { | ||
log(this._path, 'set text', this._opts); | ||
return this._file.setText(text) | ||
}; | ||
var LoggedFolder = function LoggedFolder (folder, path, opts) { | ||
this._folder = folder; | ||
this._path = path; | ||
this._opts = opts; | ||
}; | ||
LoggedFolder.prototype.delete = function delete$2 () { | ||
log(this._path, 'delete folder', this._opts); | ||
return this._folder.delete() | ||
}; | ||
LoggedFolder.prototype.file = function file (name) { | ||
return new LoggedFile( | ||
this._folder.file(name), | ||
this._path + name, | ||
this._opts | ||
) | ||
}; | ||
LoggedFolder.prototype.folder = function folder (name) { | ||
return new LoggedFolder( | ||
this._folder.folder(name), | ||
this._path + name + '/', | ||
this._opts | ||
) | ||
}; | ||
LoggedFolder.prototype.listFiles = function listFiles () { | ||
log(this._path, 'list files', this._opts); | ||
return this._folder.listFiles() | ||
}; | ||
LoggedFolder.prototype.listFolders = function listFolders () { | ||
log(this._path, 'list folders', this._opts); | ||
return this._folder.listFolders() | ||
}; | ||
function makeLoggedFolder (folder, opts) { | ||
if ( opts === void 0 ) opts = {}; | ||
return new LoggedFolder(folder, '', opts) | ||
} | ||
/** | ||
@@ -607,2 +692,3 @@ * A single file stored in memory. | ||
exports.makeLocalStorageFolder = makeLocalStorageFolder; | ||
exports.makeLoggedFolder = makeLoggedFolder; | ||
exports.makeMemoryFolder = makeMemoryFolder; | ||
@@ -609,0 +695,0 @@ exports.makeNodeFolder = makeNodeFolder; |
@@ -69,9 +69,8 @@ import { base64 } from 'rfc4648'; | ||
* The file names and expanded into paths, and the result is a flat array. | ||
* The result order is nondeterministic, since everything runs in parallel. | ||
*/ | ||
function mapAllFiles (folder, f) { | ||
function recurse (folder, prefix, f) { | ||
function recurse (folder, f, prefix) { | ||
return Promise.all([ | ||
mapFiles(folder, function (file, name) { return f(file, prefix + name, folder); }), | ||
mapFolders(folder, function (folder, name) { return recurse(folder, prefix + name + '/', f); } | ||
mapFolders(folder, function (folder, name) { return recurse(folder, f, prefix + name + '/'); } | ||
) | ||
@@ -86,3 +85,3 @@ ]).then(function (ref) { | ||
return recurse(folder, '', f) | ||
return recurse(folder, f, '') | ||
} | ||
@@ -213,2 +212,88 @@ | ||
function logConsole (path, operation) { | ||
console.log((operation + " \"" + path + "\"")); | ||
} | ||
function log (path, operation, opts) { | ||
var f = opts.callback != null ? opts.callback : logConsole; | ||
if (opts.verbose || /set|delete/.test(operation)) { | ||
f(path, operation); | ||
} | ||
} | ||
var LoggedFile = function LoggedFile (file, path, opts) { | ||
this._file = file; | ||
this._path = path; | ||
this._opts = opts; | ||
}; | ||
LoggedFile.prototype.delete = function delete$1 () { | ||
log(this._path, 'delete file', this._opts); | ||
return this._file.delete() | ||
}; | ||
LoggedFile.prototype.getData = function getData () { | ||
log(this._path, 'get data', this._opts); | ||
return this._file.getData() | ||
}; | ||
LoggedFile.prototype.getText = function getText () { | ||
log(this._path, 'get text', this._opts); | ||
return this._file.getText() | ||
}; | ||
LoggedFile.prototype.setData = function setData (data) { | ||
log(this._path, 'set data', this._opts); | ||
return this._file.setData(data) | ||
}; | ||
LoggedFile.prototype.setText = function setText (text) { | ||
log(this._path, 'set text', this._opts); | ||
return this._file.setText(text) | ||
}; | ||
var LoggedFolder = function LoggedFolder (folder, path, opts) { | ||
this._folder = folder; | ||
this._path = path; | ||
this._opts = opts; | ||
}; | ||
LoggedFolder.prototype.delete = function delete$2 () { | ||
log(this._path, 'delete folder', this._opts); | ||
return this._folder.delete() | ||
}; | ||
LoggedFolder.prototype.file = function file (name) { | ||
return new LoggedFile( | ||
this._folder.file(name), | ||
this._path + name, | ||
this._opts | ||
) | ||
}; | ||
LoggedFolder.prototype.folder = function folder (name) { | ||
return new LoggedFolder( | ||
this._folder.folder(name), | ||
this._path + name + '/', | ||
this._opts | ||
) | ||
}; | ||
LoggedFolder.prototype.listFiles = function listFiles () { | ||
log(this._path, 'list files', this._opts); | ||
return this._folder.listFiles() | ||
}; | ||
LoggedFolder.prototype.listFolders = function listFolders () { | ||
log(this._path, 'list folders', this._opts); | ||
return this._folder.listFolders() | ||
}; | ||
function makeLoggedFolder (folder, opts) { | ||
if ( opts === void 0 ) opts = {}; | ||
return new LoggedFolder(folder, '', opts) | ||
} | ||
/** | ||
@@ -595,3 +680,3 @@ * A single file stored in memory. | ||
export { locateFile, locateFolder, mapAllFiles, mapFiles, mapFolders, makeLocalStorageFolder, makeMemoryFolder, makeNodeFolder, makeUnionFolder$$1 as makeUnionFolder }; | ||
export { locateFile, locateFolder, mapAllFiles, mapFiles, mapFolders, makeLocalStorageFolder, makeLoggedFolder, makeMemoryFolder, makeNodeFolder, makeUnionFolder$$1 as makeUnionFolder }; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "disklet", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "A tiny, composable filesystem API", | ||
@@ -5,0 +5,0 @@ "author": "Airbitz, Inc.", |
163
readme.md
@@ -5,3 +5,3 @@ # Disklet | ||
Every Javascript environment seems to provide its own unique storage solution. If you are writing a cross-platform Javascript application, and just want to store data in ordinary files, there is no standard API to do so. Disklet aims to solve this problem by providing a consistent API across the following platforms: | ||
If you are writing a cross-platform Javascript application, and would like to store data in ordinary files, Disklet provides a nice little API for you to use. It provides a consistent storage interface across the following backends: | ||
@@ -12,2 +12,4 @@ * Browser LocalStorage | ||
Disklet tries to stay out of the way so you can focus on you data, not path manipulation or other filesystem details. If you try to write to a folder that doesn't exist, for example, Disklet will automatically create it for you. | ||
The API is intentionally minimalistic, and doesn't support advanced features like symlinks or permissions. It provides: | ||
@@ -18,3 +20,3 @@ | ||
Here's how it looks in action (using ES2017 `async/await` syntax): | ||
Here's how Disklet looks in action (using ES2017 `async/await` syntax): | ||
@@ -25,35 +27,34 @@ ```javascript | ||
async function demo () { | ||
// Let's make an in-memory filesystem: | ||
const root = makeMemoryFolder() | ||
// Navigate to `notes/todo.txt` (the folder doesn't need to exist): | ||
const todoFile = root.folder('notes').file('todo.txt') | ||
// Writing a file automatically creates any necessary folders: | ||
await todoFile.setText('Buy groceries') | ||
await root.folder('notes').file('hi.txt').setText('Hello') | ||
// If you would rather use paths, we have helpers for that too: | ||
await locateFile(root, 'notes/hi.txt').getText() // 'Hello' | ||
// Folders can list their own contents: | ||
await root.folder('notes').listFiles() // ['hi.txt'] | ||
await root.listFolders() // ['notes'] | ||
await root.listFiles() // [] | ||
await root.folder('notes').listFiles() // ['todo.txt'] | ||
// If you would rather use paths, we have a helper for that: | ||
await locateFile(root, 'notes/message.txt').setText('Hello') | ||
// We can easily iterate over files and folders: | ||
await root.file('a.txt').setText('a') | ||
await root.file('b.txt').setText('b') | ||
await mapAllFiles(root, file => file.getText()) // ['a', 'b', 'Hello'] | ||
// We also have iteration helpers: | ||
await mapAllFiles(root, async function (file, path, folder) { | ||
console.log(await file.getText()) | ||
}) | ||
// Delete the `notes/todo.txt` file: | ||
await root.folder('notes').file('todo.txt').delete() | ||
// Delete the `notes` folder: | ||
await root.folder('notes').delete() | ||
// Removing folders is easy: | ||
await root.delete() | ||
} | ||
``` | ||
The library has tree-shaking support, so tools like [rollup.js](https://rollupjs.org/) or [Webpack 2](https://webpack.js.org/) can automatically trim away any features you don't use. | ||
Disklet folders are designed to be easily composable: | ||
Disklet requires a `Promise` implementation, but is otherwise plain ES5. | ||
* `makeUnionFolder` creates a merged view of the files in two folders. | ||
* `makeLoggedFolder` adds logging to any `Folder` instance (*great* for debugging). | ||
The library has tree-shaking support, so tools like [rollup.js](https://rollupjs.org/) or [Webpack 2](https://webpack.js.org/) can automatically trim away any features you don't use. Even with all features present, the library is only about 2.5K (min + gzip). | ||
Disklet does require a `Promise` implementation, but is plain ES5 otherwise. The library also includes TypeScript typings if you need them. | ||
## API overview | ||
@@ -88,2 +89,3 @@ | ||
* makeLocalStorageFolder(storage = window.localStorage, opts = {}) | ||
* makeLoggedFolder(folder, opts = {}) | ||
* makeMemoryFolder(storage = {}) | ||
@@ -93,44 +95,20 @@ * makeNodeFolder(path) | ||
## Design goals | ||
## Design concepts | ||
Disklet is optimized for managing application data, as opposed to user documents. Application data is generally hidden from the user, and has an application-controlled layout. Since the application knows where things are supposed to be, it typically assembles the file paths itself, rather than receiving them from the user. | ||
### Path handling | ||
In Disklet, navigating between folders usually involves calling functions rather than concatenating strings. Besides being more convenient, this also helps applications code be more modular. In a path-based system, every component must have global knowledge of where it's files are located, all the way down to the current working directory or filesystem root. With Disklet, each component can simply receive a `Folder` object telling it where to put its data. The component doesn't need to know where or how the `Folder` stores its data; that's the outer component's job. This is great for unit testing, where the tests can just create their components with memory-based folders instead of a disk-based ones. | ||
In Disklet, navigating between folders involves calling functions rather than concatenating strings. Besides being simpler, this also helps your application become more composable. In a path-based system, every component must have global knowledge of where it's files are located, all the way down to the current working directory or filesystem root. With Disklet, each component can simply receive a `Folder` object telling it where to put its data. The component doesn't need to know where or how the `Folder` stores its data; that's the outer component's job. This is great for unit testing, where the tests can just create their components with memory-based folders instead of a disk-based ones. | ||
This is also why Disklet's folder navigation only runs in one direction, from outer folders to inner folders. If you pass a `Folder` object into a component, you automatically know that it will only write files to that location. The only way to "escape" is to bypass Disklet entirely. This makes programs easier to reason about. | ||
This is also why Disklet's folder navigation only runs in one direction, from outer folders to inner folders. If you pass a `Folder` object into a component, you automatically know that it will only write files to that location. This makes programs easier to reason about, since the only way to "escape" is to bypass Disklet entirely. | ||
Disklet also knows how to create and delete folders recursively. This is a small thing, but it makes the API much easier to use. | ||
### Text vs. Data | ||
Disklet folders are also composable. With only 5 methods each, it is easy to wrap the `File` and `Folder` interfaces with new functionality. One example of this is the union folder, which provides a combined view of two folders. It is simple to write wrappers that provide automatic encryption, logging, or other services while providing the same basic API. The Airbitz core, where Disklet was first developed, uses this capability extensively. | ||
The one wart in the Disklet API is the distinction between binary files and text files. The `localStorage` backend can only store text, so it converts binary data to base64. The disk-based backends can only store binary data, so they encode text to utf8. If you save a file in one format and load it in the other, the results depend on the platform. This is an unfortunate side-effect of running in multiple environments. | ||
## API details | ||
### Composition | ||
### Folder types | ||
Disklet is designed to be composable. The core `File` and `Folder` objects are as simple as possible, making it easy to wrap them with new functionality such as logging or encryption. Disklet itself uses this capability in its `makeLoggedFolder` and `makeUnionFolder` functions. This is also why helpers like `mapFiles` are plain functions and not `Folder` methods — every `Folder` type automatically works with every helper function. | ||
#### `makeLocalStorageFolder(storage = window.localStorage, opts = {}): Folder` | ||
## API reference | ||
A localStorage folder keeps it's contents in the browser's localStorage. | ||
The file paths are the localStorage keys, and the values are strings. If a `prefix` is provided via the `opts` parameter, then all localStorage keys will begin with the provided string. Binary data is transformed to base64, since localStorage can only handle strings. | ||
#### `makeMemoryFolder(storage = {}): Folder` | ||
A memory folder stores its contents in a Javascript object. | ||
The file paths are the object keys, and the file contents are stored as-is (arrays for `setData` and strings for `setText`). All paths start with `/`, so they will never conflict with "magic" Javascript names like `__proto__`. | ||
#### `makeNodeFolder(path: string): Folder` | ||
The Node.js folder backend writes files and folders directly to disk. It requires a starting path, which must be a folder that actually exists. Everything else will be stored inside this folder. | ||
Binary data is written as-is, while text is stored in utf-8. | ||
#### `makeUnionFolder(master: Folder, fallback: Folder): Folder` | ||
This folder creates a unified view of two sub-folders. When reading files, the union tries the `master` folder first, and then the `fallback` folder if anything goes wrong. All modifications go to the `master` folder. | ||
To implement deletions, the union folder uses "whiteout" files. These are zero-length files with the extension `._x_`. If a whiteout file exists, the normal file with the corresponding name will not be shown, even if it exists in the fallback folder. | ||
### Folder methods | ||
@@ -144,15 +122,15 @@ | ||
Navigates to the named file. The file name must not contain slashes. Otherwise, this method will never fail, even if the file does not exist. | ||
Navigates to the named file. The file does not need to exist yet. | ||
#### `folder(name: string): Folder` | ||
Navigates to the named folder. The folder name must not contain slashes. Otherwise, this method will never fail, even if the folder does not exist. | ||
Navigates to the named folder. The folder does not need to exist yet. | ||
#### `listFiles(): Promise<Array<string>>` | ||
Lists the file names in this folder. | ||
Lists the file names in this folder. Returns an empty list if this folder doesn't exist yet. | ||
#### `listFolders(): Promise<Array<string>>` | ||
Lists the sub-folder names in this folder. | ||
Lists the sub-folder names in this folder. Returns an empty list if this folder doesn't exist yet. | ||
@@ -163,3 +141,3 @@ ### File methods | ||
Deletes this file. | ||
Deletes this file. Does nothing if the file doesn't exist. | ||
@@ -176,3 +154,3 @@ #### `getData(): Promise<Uint8Array>` | ||
Writes the file to disk from an array of binary data. Will recursively create any folders needed to hold the file. | ||
Writes the file to disk from an array of bytes. This will recursively create any folders needed to hold the file. | ||
@@ -189,3 +167,3 @@ #### `setData(text: string): Promise<void>` | ||
The path can contain `./` and `../` components, although it is an error to try to "escape" the parent folder using too many `../` components. | ||
The path can contain `.` and `..` components, although trying to "escape" the parent folder using too many `..` components will raise an error. | ||
@@ -196,14 +174,63 @@ #### `locateFolder(folder: Folder, path: string): Folder` | ||
The path can contain `./` and `../` components, although it is an error to try to "escape" the parent folder using too many `../` components. | ||
The path can contain `.` and `..` components, although trying to "escape" the parent folder using too many `..` components will raise an error. | ||
#### `mapFiles(folder: Folder, callback: (file: File, name: string, folder: Folder) => any): Promise<Array<any>>` | ||
#### `mapAllFiles(folder: Folder, callback: (file: File, path: string, parent: Folder) => any): Promise<Array<any>>` | ||
This function applies the provided callback function to each file in a folder. The file object, file name, and parent folder are provided to the callback, mimicking the `Array.map` callback order. | ||
This function applies the provided callback function to each file in a folder, recursively. The callback receives a file object, file path (including folders), and direct parent folder, mimicking the `Array.map` callback order. | ||
The callback method can be asynchronous, and the results will be combined into a single array for the return value. | ||
The callback method can be asynchronous, and any results will be combined into a single array for the return value. | ||
#### `mapAllFiles(folder: Folder, callback: (file: File, path: string, folder: Folder) => any): Promise<Array<any>>` | ||
#### `mapFiles(folder: Folder, callback: (file: File, name: string, parent: Folder) => any): Promise<Array<any>>` | ||
This function applies the provided callback function to each file in a folder, recursively. The file object, file path (including folders), and direct parent folder are provided to the callback, mimicking the `Array.map` callback order. | ||
This function applies the provided callback function to each file in a folder without recursion. The callback receives a file object, file name, and parent folder, mimicking the `Array.map` callback order. | ||
The callback method can be asynchronous, and the results will be combined into a single array for the return value. | ||
The callback method can be asynchronous, and any results will be combined into a single array for the return value. | ||
#### `mapFolders(folder: Folder, callback: (folder: Folder, name: string, parent: Folder) => any): Promise<Array<any>>` | ||
This function applies the provided callback function to each file in a folder without recursion. The callback receives a file object, file name, and parent folder, mimicking the `Array.map` callback order. | ||
The callback method can be asynchronous, and any results will be combined into a single array for the return value. | ||
### Creating Folders | ||
#### `makeLocalStorageFolder(storage = window.localStorage, opts = {}): Folder` | ||
A localStorage folder keeps it's contents in the browser's localStorage. | ||
The file paths are the localStorage keys, and the values are strings. If a `prefix` is provided via the `opts` parameter, then all localStorage keys will begin with the provided string. Binary data is transformed to base64, since localStorage can only handle strings. | ||
#### `makeLoggedFolder(folder, opts = {}): Folder` | ||
This function wraps a folder object with logging. | ||
By default, only changes will be logged. To log everything, set `opts.verbose` to `true`. | ||
If you would like to send the logs somewhere other than `console.log`, pass a callback function as `opts.callback`. The callback's parameters are a path and an operation name, which is one of: | ||
* "delete file" | ||
* "delete folder" | ||
* "get data" | ||
* "get text" | ||
* "list files" | ||
* "list folders" | ||
* "set data" | ||
* "set text" | ||
#### `makeMemoryFolder(storage = {}): Folder` | ||
A memory folder stores its contents in a Javascript object. | ||
The file paths are the object keys, and the file contents are stored as-is (arrays for `setData` and strings for `setText`). All paths start with `/`, so they will never conflict with "magic" Javascript names like `__proto__`. | ||
#### `makeNodeFolder(path: string): Folder` | ||
The Node.js folder backend writes files and folders directly to disk. It requires a starting path that everything will be located under. | ||
Binary data is written as-is, while text is stored in utf-8. | ||
#### `makeUnionFolder(master: Folder, fallback: Folder): Folder` | ||
This folder creates a unified view of two sub-folders. When reading files, the union tries the `master` folder first, and then the `fallback` folder if anything goes wrong. All modifications go to the `master` folder. | ||
The union folder uses "whiteout" files to mark deletions. These are empty files with the extension `._x_`. This way, files in the fallback folder won't show through when the corresponding file is deleted in the master folder. |
@@ -17,22 +17,55 @@ export interface Folder { | ||
interface LocalStorageOpts { | ||
prefix?: string | ||
} | ||
// Helper functions: | ||
export function makeMemoryFolder(storage?: object): Folder | ||
export function makeLocalStorageFolder(storage?: object, opts?: LocalStorageOpts): Folder | ||
export function makeNodeFolder(path: string): Folder | ||
export function makeUnionFolder(master: Folder, fallback: Folder): Folder | ||
export function locateFile(folder: Folder, path: string): File | ||
export function locateFolder(folder: Folder, path: string): Folder | ||
export function mapAllFiles( | ||
folder: Folder, | ||
callback: (file: File, path: string, parentFolder: Folder) => any | ||
): Promise<Array<any>> | ||
export function mapFiles( | ||
folder: Folder, | ||
callback: (file: File, name: string, folder: Folder) => any | ||
callback: (file: File, name: string, parent: Folder) => any | ||
): Promise<Array<any>> | ||
export function mapAllFiles( | ||
export function mapFolders( | ||
folder: Folder, | ||
callback: (file: File, path: string, parentFolder: Folder) => any | ||
callback: (folder: Folder, name: string, parent: Folder) => any | ||
): Promise<Array<any>> | ||
// Folder types: | ||
interface LocalStorageOpts { | ||
prefix?: string | ||
} | ||
type LoggedFolderOperations = | ||
"delete file" | | ||
"delete folder" | | ||
"get data" | | ||
"get text" | | ||
"list files" | | ||
"list folders" | | ||
"set data" | | ||
"set text" | ||
interface LoggedFolderOpts { | ||
callback?: (path: string, operation: LoggedFolderOperations) => void | ||
verbose?: boolean | ||
} | ||
export function makeLocalStorageFolder( | ||
storage?: object, | ||
opts?: LocalStorageOpts | ||
): Folder | ||
export function makeLoggedFolder( | ||
folder: Folder, | ||
opts?: LoggedFolderOpts | ||
): Folder | ||
export function makeMemoryFolder(storage?: object): Folder | ||
export function makeNodeFolder(path: string): Folder | ||
export function makeUnionFolder(master: Folder, fallback: Folder): Folder |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
120344
1189
226