diff --git a/.gitignore b/.gitignore index 119db73..504b5c6 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ node_modules # Temporary / data folders .tmp +.data data/ # Build directory diff --git a/.npmignore b/.npmignore index 6b8f416..4954381 100644 --- a/.npmignore +++ b/.npmignore @@ -36,6 +36,7 @@ src/ # Test directory test/ +.data .eslintrc .babelrc diff --git a/README.md b/README.md index 3b679c7..f01eb3e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/blockai/fs-tus-store.svg?branch=master)](https://travis-ci.org/blockai/fs-tus-store) -WIP +WIP. ## Install @@ -14,4 +14,21 @@ Requires Node v6+ ## Usage -See [./test](./test) directory for usage examples. \ No newline at end of file +See [./test](./test) directory for usage examples. + +All methods return promises. + +### Creating client + +```javascript +import initFsStore from 'tus-fs-store' +const store = initFsStore({ directory: './path/to/base/directory' }) +``` + +### info(key) + +Resolves to an `{ uploadOffset[, uploadLength, uploadMetadata] }` object. + +### create(key[, { uploadLength, uploadMetadata }]) + +### write(key, readStream) diff --git a/package.json b/package.json index fc28860..bd01562 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,11 @@ "blue-tape": "^1.0.0", "cz-conventional-changelog": "^1.2.0", "eslint-config-blockai": "^1.0.1", + "mkdirp": "^0.5.1", "nodemon": "^1.10.2", "rimraf": "^2.5.4", - "semantic-release": "^4.3.5" + "semantic-release": "^4.3.5", + "string-to-stream": "^1.1.0" }, "release": { "debug": false, @@ -51,5 +53,8 @@ "commitizen": { "path": "cz-conventional-changelog" } + }, + "dependencies": { + "pump": "^1.0.1" } -} \ No newline at end of file +} diff --git a/src/index.js b/src/index.js index 45a9011..9814f5d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,77 @@ -export default () => { +import path from 'path' +import fs from 'fs' +import pump from 'pump' +// promisify +const readFile = (file) => new Promise((resolve, reject) => { + fs.readFile(file, (err, data) => { + if (err) return reject(err) + resolve(data) + }) +}) +const writeFile = (file, data) => new Promise((resolve, reject) => { + fs.writeFile(file, data, (err) => { + if (err) return reject(err) + resolve(data) + }) +}) +const stat = (file) => new Promise((resolve, reject) => { + fs.stat(file, (err, statObj) => { + if (err) return reject(err) + resolve(statObj) + }) +}) + +export default ({ + directory = '.fs-tus-store', +} = {}) => { + const absoluteDir = path.resolve(directory) + // Returns { uploadOffset[, uploadLength] } + + // TODO: convert key to base64 or validate it? + const keyPath = (key) => path.join(absoluteDir, key) + const keyInfoPath = (key) => `${keyPath(key)}.info` + + const getKeyInfo = (key) => readFile(keyInfoPath(key)) + .then(data => JSON.parse(data)) + + const setKeyInfo = (key, infoObj) => Promise.resolve() + .then(() => JSON.stringify(infoObj)) + .then(data => writeFile(keyInfoPath(key), data)) + + const getKeyOffset = (key) => stat(keyPath(key)) + .then(({ size }) => size) + + const touchKey = (key) => writeFile(keyPath(key), '') + + // TODO: fail if key already exists? + const create = (key, { uploadLength, uploadMetadata } = {}) => ( + setKeyInfo(key, { uploadLength, uploadMetadata }) + .then(() => touchKey(key)) + ) + + const info = key => getKeyInfo(key) + .then(infoObj => ( + getKeyOffset(key) + .then((uploadOffset) => ({ + ...infoObj, + uploadOffset, + })) + )) + + const write = (key, rs) => new Promise((resolve, reject) => { + const ws = fs.createWriteStream(keyPath(key), { + flags: 'a', // append to file + }) + pump(rs, ws, (err) => { + if (err) return reject(err) + resolve() + }) + }) + + return { + info, + create, + write, + } } diff --git a/test/index.test.js b/test/index.test.js index 75c2fde..b973a8f 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,7 +1,72 @@ import test from 'blue-tape' -import init from '../src' +import rimraf from 'rimraf' +import mkdirp from 'mkdirp' +import str from 'string-to-stream' -test('todo', (t) => { - t.ok(!!init) - t.end() +import initFsStore from '../src' + +const resetStore = () => { + const directory = `${__dirname}/.data` + rimraf.sync(directory) + mkdirp.sync(directory) + return initFsStore({ directory }) +} + +const store = resetStore() + +test('info - unknown key', (t) => { + store + .info('unknown-key') + .catch((err) => { + t.ok(err instanceof Error) + t.end() + }) }) + +test('info - foo- unknown key', (t) => { + store + .info('foo') + .catch((err) => { + t.ok(err instanceof Error) + t.end() + }) +}) + +test('create foo', () => ( + store + .create('foo', { uploadLength: 'bar'.length }) +)) + +test('info foo', (t) => ( + store + .info('foo') + .then(({ uploadOffset, uploadLength }) => { + t.equal(uploadOffset, 0) + t.equal(uploadLength, 3) + }) +)) + +test('write ba to foo', () => ( + store + .write('foo', str('ba')) +)) + +test('info foo', (t) => ( + store + .info('foo') + .then(({ uploadOffset, uploadLength }) => { + t.equal(uploadOffset, 2) + t.equal(uploadLength, 3) + }) +)) + +test('write r to foo', () => store.write('foo', str('r'))) + +test('info foo', (t) => ( + store + .info('foo') + .then(({ uploadOffset, uploadLength }) => { + t.equal(uploadOffset, 3) + t.equal(uploadLength, 3) + }) +))