├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── build.js ├── lib ├── core.js ├── directory_entry.js ├── fs.js └── global.js ├── package.json └── test ├── global.html ├── index.html ├── picture.jpg ├── test.dir.js ├── test.js ├── test.read.js ├── test.remove.js └── test.write.js /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | build 3 | node_modules/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | build.js 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 0.12 4 | script: npm test 5 | before_install: 6 | - "export DISPLAY=:99.0" 7 | - "sh -e /etc/init.d/xvfb start" 8 | addons: 9 | firefox: "42.0" 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fs-web 2 | 3 | Bringing a file system abstraction to the browser. **fs** is a module that allows you to store data in the (modern) browser using an API similar to that of Node's [fs module](http://nodejs.org/api/fs.html) 4 | 5 | Implemented in a cross-browser fashion, using [IndexedDB](http://www.w3.org/TR/IndexedDB/). 6 | 7 | ## Installation 8 | 9 | Install via npm: 10 | 11 | ```shell 12 | npm install fs-web --save 13 | ``` 14 | 15 | ## Example 16 | 17 | Writing from a file input. 18 | 19 | ```javascript 20 | import { writeFile } from 'fs-web'; 21 | 22 | let input = document.querySelector('input[type="file"]'); 23 | input.addEventListener('change', function(e) { 24 | let file = this.files[0]; // file is a File object. 25 | 26 | writeFile(file.name, file).then(function() { 27 | // All done! File has been saved. 28 | }); 29 | }); 30 | ``` 31 | 32 | Writing and reading. 33 | 34 | ```js 35 | import * as fs from 'fs-web'; 36 | 37 | fs.writeFile('foo/some-file.txt', 'foo') 38 | .then(function(){ 39 | return fs.readdir('foo'); 40 | }) 41 | .then(function(files){ 42 | files // -> [ {some-file.txt} ] 43 | }); 44 | ``` 45 | 46 | ## API 47 | 48 | All methods return a Promise. 49 | 50 | ### fs.writeFile(fileName, data) 51 | 52 | Saves the file ``data`` with the name ``fileName`` and returns a Promise. If an error is encountered, the Promise will be rejected with an ``Error`` object. 53 | 54 | ### fs.readFile(fileName) 55 | 56 | Retrieves the file with the name ``fileName`` and returns a Promise. The Promise will resolve with the file's data as an ``ArrayBuffer``. 57 | 58 | ### fs.readString(fileName) 59 | 60 | Retrieves the file with the name ``fileName`` and returns a Promise. The Promise will resolve with a string representation of `fileName`. 61 | 62 | ### fs.removeFile(fileName) 63 | 64 | Removes the file with the name ``fileName`` from storage and returns a Promise. The Promise will resolve even if the fileName doesn't exist. 65 | 66 | ### fs.readdir(fullPath) 67 | 68 | Gets the contents of ``fullPath`` and returns a Promise. The Promise will resolve with an array of ``DirectoryEntry`` objects (see below). 69 | 70 | ### fs.mkdir(fullPath) 71 | 72 | Creates a directory at ``fullPath`` and returns a Promise. 73 | 74 | ### fs.rmdir(fullPath) 75 | 76 | Removes the directory at ``fullPath``, recursively removing any files/subdirectories contained within. Returns a Promise that will resolve when the fullPath is removed. 77 | 78 | ### DirectoryEntry 79 | 80 | A ``DirectoryEntry`` object is resolved from ``fs.readdir`` and represents either a **file** or a **directory**. A DirectoryEntry instance contains these properties/methods: 81 | 82 | ### DirectoryEntry#path 83 | 84 | The ``path`` property is the full path (including file name) for the given file/directory entry. 85 | 86 | ### DirectoryEntry#name 87 | 88 | The ``name`` of the given entry, either the file or directory name. 89 | 90 | ### DirectoryEntry#dir 91 | 92 | The given directory that the file/directory sits in. 93 | 94 | ### DirectoryEntry#type 95 | 96 | The ``type`` of the entry, either **file** or **directory**. 97 | 98 | ### DirectoryEntry#readFile() 99 | 100 | A convenience method for calling ``readFile(fileName)``. Throws a TypeError if the entry is not of ``type`` **file**. 101 | 102 | ## License 103 | 104 | BSD 2 Clause 105 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | var stealExport = require("steal-tools").export; 2 | 3 | var denpm = function(name){ 4 | if(name.indexOf("@") > 0) { 5 | var pkgName = name.substr(0, name.indexOf("@")); 6 | var modulePath = name.substr(name.indexOf("#")+1); 7 | return pkgName + "/" + modulePath; 8 | } 9 | return name; 10 | }; 11 | 12 | stealExport({ 13 | system: { 14 | config: __dirname + "/package.json!npm", 15 | main: ["fs-web", "fs-web/global"] 16 | }, 17 | outputs: { 18 | "global": { 19 | modules: ["fs-web/global"], 20 | ignore: false, 21 | dest: __dirname + "/dist/fs.js", 22 | transpile: "global", 23 | normalize: denpm 24 | }, 25 | "global minified": { 26 | modules: ["fs-web/global"], 27 | ignore: false, 28 | minify: true, 29 | dest: __dirname + "/dist/fs.min.js", 30 | transpile: "global", 31 | normalize: denpm 32 | }, 33 | "+cjs": {} 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import DirectoryEntry from './directory_entry'; 3 | 4 | function ab2str(buf) { 5 | return String.fromCharCode.apply(null, new Uint16Array(buf)); 6 | } 7 | function str2ab(str) { 8 | const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char 9 | const bufView = new Uint16Array(buf); 10 | for (let i = 0, strLen = str.length; i < strLen; i++) { 11 | bufView[i] = str.charCodeAt(i); 12 | } 13 | return buf; 14 | } 15 | 16 | const DB_NAME = window.location.host + '_filesystem', 17 | OS_NAME = 'files', 18 | DIR_IDX = 'dir'; 19 | 20 | function init(callback) { 21 | const req = window.indexedDB.open(DB_NAME, 1); 22 | 23 | req.onupgradeneeded = function (e) { 24 | const db = e.target.result; 25 | 26 | const objectStore = db.createObjectStore(OS_NAME, { keyPath: 'path' }); 27 | objectStore.createIndex(DIR_IDX, 'dir', { unique: false }); 28 | }; 29 | 30 | req.onsuccess = function (e) { 31 | callback(e.target.result); 32 | }; 33 | } 34 | 35 | function initOS(type, callback) { 36 | init(function (db) { 37 | const trans = db.transaction([OS_NAME], type), 38 | os = trans.objectStore(OS_NAME); 39 | 40 | callback(os); 41 | }); 42 | } 43 | 44 | let readFrom = function (fileName) { 45 | return new Promise(function(resolve, reject){ 46 | initOS('readonly', function (os) { 47 | const req = os.get(fileName); 48 | 49 | req.onerror = reject; 50 | 51 | req.onsuccess = function (e) { 52 | const res = e.target.result; 53 | 54 | if (res && res.data) { 55 | resolve(res.data); 56 | } else { 57 | reject('File not found'); 58 | } 59 | }; 60 | }); 61 | }); 62 | }; 63 | 64 | export function readFile(fileName) { 65 | return readFrom(fileName).then(function (data) { 66 | if (!(data instanceof ArrayBuffer)) { 67 | data = str2ab(data.toString()); 68 | } 69 | return data; 70 | }); 71 | } 72 | 73 | export function readString(fileName) { 74 | return readFrom(fileName).then(function(data) { 75 | if ((data instanceof ArrayBuffer)) { 76 | data = ab2str(data); 77 | } 78 | return data; 79 | }); 80 | }; 81 | 82 | export function writeFile(fileName, data) { 83 | return new Promise(function(resolve, reject){ 84 | initOS('readwrite', function (os) { 85 | const req = os.put({ 86 | "path": fileName, 87 | "dir": path.dirname(fileName), 88 | "type": "file", 89 | "data": data 90 | }); 91 | 92 | req.onerror = reject; 93 | 94 | req.onsuccess = function (e) { 95 | resolve(); 96 | }; 97 | }); 98 | }); 99 | }; 100 | 101 | export function removeFile(fileName) { 102 | return new Promise(function(resolve){ 103 | initOS('readwrite', function (os) { 104 | const req = os.delete(fileName); 105 | 106 | req.onerror = req.onsuccess = function (e) { 107 | resolve(); 108 | }; 109 | }); 110 | }); 111 | }; 112 | 113 | function withTrailingSlash(path) { 114 | const directoryWithTrailingSlash = path[path.length - 1] === '/' 115 | ? path 116 | : path + '/'; 117 | return directoryWithTrailingSlash; 118 | } 119 | 120 | export function readdir(directoryName) { 121 | return new Promise(function(resolve, reject){ 122 | initOS('readonly', function (os) { 123 | const dir = path.dirname(withTrailingSlash(directoryName)); 124 | 125 | const idx = os.index(DIR_IDX); 126 | const range = IDBKeyRange.only(dir); 127 | const req = idx.openCursor(range); 128 | 129 | req.onerror = function (e) { 130 | reject(e); 131 | }; 132 | 133 | const results = []; 134 | req.onsuccess = function (e) { 135 | const cursor = e.target.result; 136 | if (cursor) { 137 | const value = cursor.value; 138 | const entry = new DirectoryEntry(value.path, value.type); 139 | results.push(entry); 140 | cursor.continue(); 141 | } else { 142 | resolve(results); 143 | } 144 | }; 145 | }); 146 | }); 147 | }; 148 | 149 | export function mkdir(fullPath) { 150 | return new Promise(function(resolve, reject){ 151 | initOS('readwrite', function (os) { 152 | const dir = withTrailingSlash(path); 153 | 154 | const req = os.put({ 155 | "path": fullPath, 156 | "dir": path.dirname(dir), 157 | "type": "directory" 158 | }); 159 | 160 | req.onerror = reject; 161 | 162 | req.onsuccess = function (e) { 163 | resolve(); 164 | }; 165 | }); 166 | }); 167 | }; 168 | 169 | export function rmdir(fullPath) { 170 | return readdir(fullPath) 171 | .then(function removeFiles(files) { 172 | if (!files || !files.length) { 173 | return removeFile(fullPath); 174 | } 175 | 176 | const file = files.shift(), 177 | func = file.type === 'directory' 178 | ? rmdir 179 | : removeFile; 180 | 181 | return func(file.name).then(function () { 182 | return removeFiles(files); 183 | }); 184 | }); 185 | }; 186 | -------------------------------------------------------------------------------- /lib/directory_entry.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | function DirectoryEntry(fullPath, type) { 4 | this.path = fullPath; 5 | this.name = path.basename(fullPath); 6 | this.dir = path.dirname(fullPath); 7 | this.type = type; 8 | } 9 | 10 | export { DirectoryEntry as default }; 11 | -------------------------------------------------------------------------------- /lib/fs.js: -------------------------------------------------------------------------------- 1 | import { readFile } from './core'; 2 | import DirectoryEntry from './directory_entry'; 3 | 4 | DirectoryEntry.prototype.readFile = function (callback) { 5 | if (this.type !== 'file') { 6 | throw new TypeError('Not a file.'); 7 | } 8 | return readFile(this.path, callback); 9 | }; 10 | 11 | export * from './core'; 12 | export { DirectoryEntry }; 13 | -------------------------------------------------------------------------------- /lib/global.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-web'; 2 | 3 | window.fsWeb = fs; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fs-web", 3 | "version": "1.0.1", 4 | "description": "Node's fs, for the browser", 5 | "main": "dist/cjs/fs.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "build": "node build.js", 11 | "test": "DEBUG=testee:* testee test/index.html --browsers firefox --reporter Spec" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/matthewp/fs.git" 16 | }, 17 | "keywords": [ 18 | "fs", 19 | "web" 20 | ], 21 | "author": "Matthew Phillips", 22 | "license": "BSD-2-Clause", 23 | "bugs": { 24 | "url": "https://github.com/matthewp/fs/issues" 25 | }, 26 | "homepage": "https://github.com/matthewp/fs#readme", 27 | "devDependencies": { 28 | "chai": "^3.4.1", 29 | "steal": "^0.12.7", 30 | "steal-mocha": "0.0.3", 31 | "steal-tools": "^0.12.3", 32 | "testee": "^0.2.2" 33 | }, 34 | "dependencies": { 35 | "path": "git://github.com/component/path#7b4f23c38833a5232cd5e3d50ccb8cd13dbcd2f4" 36 | }, 37 | "system": { 38 | "main": "fs", 39 | "directories": { 40 | "lib": "lib" 41 | }, 42 | "map": { 43 | "chai": "chai/chai" 44 | }, 45 | "transpiler": "babel" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/global.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | fs tests 2 | 5 | -------------------------------------------------------------------------------- /test/picture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthewp/fs/869bb9509549a74b39d8d9efcbee025de479ec72/test/picture.jpg -------------------------------------------------------------------------------- /test/test.dir.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import 'steal-mocha'; 3 | import { 4 | DirectoryEntry, 5 | readdir, 6 | writeFile, 7 | mkdir, 8 | rmdir 9 | } from 'fs-web'; 10 | 11 | const { assert } = chai; 12 | 13 | describe('Empty directory', function () { 14 | var err, files; 15 | before(function (done) { 16 | readdir('dir-not-exist').then(function (f) { 17 | files = f; 18 | }, function (e) { err = e }).then(done); 19 | }); 20 | 21 | describe('Listing an empty directory.', function () { 22 | it('The error should be undefined', function (done) { 23 | done(assert(err === undefined)); 24 | }); 25 | 26 | it('Should have an array for the files', function (done) { 27 | done(assert(Array.isArray(files))); 28 | }); 29 | 30 | it('Should be an empty array', function (done) { 31 | done(assert(!files.length)); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | describe('Directory with files', function () { 38 | var err, files; 39 | before(function (done) { 40 | writeFile('foo/dir-file-one.txt', 'Foo').then(function() { 41 | return writeFile('foo/dir-file-two.txt', 'bar'); 42 | }).then(function(){ 43 | return readdir('foo'); 44 | }).then(function(f) { files = f; }, function(e){ err = e; }) 45 | .then(done); 46 | }); 47 | 48 | describe('Listing a directory.', function () { 49 | it('The error should be undefined', function () { 50 | assert(err === undefined); 51 | }); 52 | 53 | it('Should have an array for the files', function () { 54 | assert(Array.isArray(files)); 55 | }); 56 | 57 | it('Should contain DirectoryEntry objects', function () { 58 | assert(files.every(function (item) { 59 | return item instanceof DirectoryEntry; 60 | })); 61 | }); 62 | 63 | it('Should have 2 files in the directory.', function () { 64 | assert(files.length === 2); 65 | }); 66 | }); 67 | 68 | }); 69 | 70 | describe('Root directory', function() { 71 | describe('Listing contents', function() { 72 | it('Should be able to list', function(done) { 73 | readdir('').then(function() { 74 | done(assert(true)); 75 | }); 76 | }); 77 | }); 78 | }); 79 | 80 | describe('Manipulating directories', function () { 81 | describe('Creating a directory.', function () { 82 | var dirName = 'dir-foobar'; 83 | 84 | it('Should not return an error', function (done) { 85 | mkdir(dirName).then(function () { 86 | return writeFile('dir-remfile1', 'foo bar'); 87 | }).then(function(){ 88 | return writeFile('dir-remfile2', 'baz buz'); 89 | }).then(done, done); 90 | }); 91 | }); 92 | 93 | describe('Removing a directory with files.', function () { 94 | var dirName = 'dir-remme'; 95 | 96 | beforeEach(function (done) { 97 | mkdir(dirName).then(done, done); 98 | }); 99 | 100 | it('Should call the callback.', function () { 101 | rmdir(dirName).then(assert.bind(null, true)); 102 | }); 103 | 104 | it('Should return an empty array for the files', function (done) { 105 | readdir(dirName).then(function(files){ 106 | done(assert(!files.length)); 107 | }); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import './test.dir'; 2 | import './test.read'; 3 | import './test.write'; 4 | import './test.remove'; 5 | -------------------------------------------------------------------------------- /test/test.read.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import * as fs from 'fs-web'; 3 | import 'steal-mocha'; 4 | 5 | const { assert } = chai; 6 | 7 | describe('Read', function () { 8 | describe('Reading a file that doesn\'t exist', function () { 9 | it('Should return an error', function (done) { 10 | fs.readFile('some-fake-file.txt').then(null, function (err) { 11 | done(assert(err !== null)); 12 | }); 13 | }); 14 | }); 15 | 16 | describe('Reading a text file', function () { 17 | var fileName = 'read-some-file.txt', 18 | contents = 'Foo bar baz'; 19 | 20 | before(function (done) { 21 | fs.writeFile(fileName, contents).then(done); 22 | }); 23 | 24 | it('readString should return a string', function (done) { 25 | fs.readString(fileName).then(function(res) { 26 | done(assert(res === contents)); 27 | }); 28 | }); 29 | 30 | it('readFile should return an ArrayBuffer', function (done) { 31 | fs.readFile(fileName).then(function(res) { 32 | done(assert(res instanceof ArrayBuffer)); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('Reading a binary file', function () { 38 | function getPicture() { 39 | var req = new XMLHttpRequest(); 40 | req.open('GET', 'picture.jpg', true); 41 | req.responseType = 'arraybuffer'; 42 | 43 | return new Promise(function(resolve){ 44 | req.onload = function (e) { 45 | resolve(e.target.response); 46 | }; 47 | 48 | req.send(); 49 | }); 50 | } 51 | 52 | it('Should return an ArrayBuffer', function (done) { 53 | var fileName = 'read-picture.jpg'; 54 | 55 | getPicture().then(function (ab) { 56 | return fs.writeFile(fileName, ab); 57 | }).then(function(){ 58 | return fs.readFile(fileName); 59 | }).then(function(res){ 60 | done(assert(res instanceof ArrayBuffer)); 61 | }); 62 | }); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/test.remove.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import * as fs from 'fs-web'; 3 | import 'steal-mocha'; 4 | 5 | const { assert } = chai; 6 | 7 | describe('Remove', function () { 8 | var TEST_FILE = 'rem-test.txt'; 9 | before(function (done) { 10 | fs.writeFile(TEST_FILE, 'Foo bar').then(done); 11 | }); 12 | 13 | function exists(obj) { 14 | return typeof obj !== 'undefined' && obj !== null; 15 | } 16 | 17 | describe('Removing a file that exists.', function () { 18 | it('Should call the callback.', function (done) { 19 | fs.removeFile(TEST_FILE).then(function() { 20 | done(assert(true)); 21 | }); 22 | }); 23 | }); 24 | 25 | describe('Removing a file that doesn\'t exist.', function () { 26 | it('Should call the callback.', function(done) { 27 | fs.removeFile('rem-does-not-exist.txt').then(function() { 28 | done(assert(true)); 29 | }); 30 | }); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/test.write.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-web'; 2 | import chai from 'chai'; 3 | import 'steal-mocha'; 4 | 5 | const { assert } = chai; 6 | 7 | describe('Write', function () { 8 | var TEST_FILE = 'write-test.txt'; 9 | before(function (done) { 10 | fs.writeFile(TEST_FILE, 'Foo bar').then(done); 11 | }); 12 | 13 | function exists(obj) { 14 | return typeof obj !== 'undefined' && obj !== null; 15 | } 16 | 17 | describe('Writing data to a file.', function () { 18 | it('Should not have an error.', function (done) { 19 | fs.writeFile(TEST_FILE, 'Foo bar').then(function () { 20 | done(assert(true)); 21 | }); 22 | }); 23 | 24 | it('Retrieving that file should\'nt have an error.', function (done) { 25 | fs.readFile(TEST_FILE).then(function (data) { 26 | done(assert(true)); 27 | }); 28 | }); 29 | 30 | it('Retrieving should include data.', function (done) { 31 | fs.readFile(TEST_FILE).then(function(data) { 32 | done(assert(exists(data))); 33 | }); 34 | }); 35 | }); 36 | 37 | 38 | function getPicture(callback) { 39 | var req = new XMLHttpRequest(); 40 | req.open('GET', 'picture.jpg', true); 41 | req.responseType = 'arraybuffer'; 42 | 43 | return new Promise(function(resolve){ 44 | req.onload = function (e) { 45 | resolve(e.target.response); 46 | }; 47 | 48 | req.send(); 49 | }); 50 | } 51 | 52 | describe('Writing binary data to a file.', function () { 53 | it('Should return without an error.', function (done) { 54 | getPicture().then(function (data) { 55 | fs.writeFile('write-picture.jpg', data).then(function () { 56 | done(assert(true)); 57 | }); 58 | }); 59 | }); 60 | }); 61 | }); 62 | --------------------------------------------------------------------------------