diff --git a/.gitignore b/.gitignore index 123ae94d..a84acb54 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,7 @@ coverage # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release +build # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git diff --git a/README.md b/README.md index cb2d9dd9..c48bf5e7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,109 @@ # js-ipfs-repo + Implementation of the IPFS repo spec (https://github.com/ipfs/specs/tree/master/repo) in JavaScript + +## API + +### `Repo` + +Constructor, accepts a path and options: + +```js +var Repo = require('js-ipfs-repo') +var repo = new Repo('/Users/someone/.ipfs', {adaptor: 'fs'}) +``` + +Options: + + - `adaptor`: String with the adaptor. Defaults to `fs` + +### `#version` + +Read/Write the version number of that repository. + +```js +repo.version().read(function (err, num) { + console.log(err, num) // => 2 +}) + +repo.version().write(3, function (err) { + console.log(err) +}) +``` + +### `#api` + +Read/Write the JSON configuration for that repository. + +```js +repo.api().read(function (err, multiaddr) { + console.log(err, multiaddr) +}) + +repo.api().write('/ip4/127.0.0.1/tcp/5001', function (err) { + console.log(err) +}) +``` + +### `#config` + +Read/Write the JSON configuration for that repository. + +```js +repo.config().read(function (err, json) { + console.log(err, json) +}) + +repo.config().write({foo: 'bar'}, function (err) { + console.log(err) +}) +``` + +### `#blocks` + +Store data on the block store. + +```js +repo.blocks().read('12200007d4e3a319cd8c7c9979280e150fc5dbaae1ce54e790f84ae5fd3c3c1a0475', function (buff, err) { + console.log(err) +}) +``` + +```js +repo.blocks().write(buff, function (buff, err) { + console.log(buff.toString('utf-8'), err) +}) +``` + +### `#repo` + +Read/Write the `repo.lock` file. + +```js +repo.repo().read(function (err, content) { + console.log(err, content) +}) + +repo.repo().write('foo', function (err) { + console.log(err) +}) +``` + +## Adaptors + +By default it will use the `fs-repo` adaptor. Eventually we can write other adaptors +and make those available on configuration. + +### `fs-repo` + +The default adaptor. Uses the `repo.lock` file to ensure there are no simultaneous reads +nor writes. Uses the `fs-blob-store`. + +### `memory-repo` + +Ideal for testing purposes. Uses the `abstract-blob-store`. + +## Tests + +Not there yet! Should ran both in node and in Phantom with compatible +adaptors. diff --git a/package.json b/package.json new file mode 100644 index 00000000..901b289e --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "js-ipfs-repo", + "version": "0.0.1", + "description": "IPFS Repo implementation", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "lint": "./node_modules/.bin/standard" + }, + "repository": { + "type": "git", + "url": "https://github.com/diasdavid/js-ipfs-repo.git" + }, + "keywords": [ + "IPFS" + ], + "pre-commit": [ + "lint" + ], + "homepage": "https://github.com/diasdavid/js-libp2p-record", + "devDependencies": { + "pre-commit": "^1.1.1", + "standard": "^5.1.1" + }, + "dependencies": { + "abstract-blob-store": "^3.2.0", + "concat-stream": "^1.5.1", + "fs-blob-store": "^5.2.0", + "level-js": "^2.2.2", + "lockfile": "^1.0.1", + "multihashes": "^0.2.0" + } +} diff --git a/src/adaptors/fs-repo/index.js b/src/adaptors/fs-repo/index.js new file mode 100644 index 00000000..93550b42 --- /dev/null +++ b/src/adaptors/fs-repo/index.js @@ -0,0 +1,121 @@ +var concat = require('concat-stream') +var fs = require('fs-blob-store') +var path = require('path') +var lockFile = require('lockfile') + +function BlobStore (base_path) { + this.store = fs(base_path) + this.LOCK_PATH = path.join(base_path, 'repo.lock') +} + +BlobStore.prototype = { + + /** + * Read a blob on a given path + * It holds the repo.lock while reading + * + * @param {String} key + * @param {Function} cb + * @return {ReadableStream} + */ + read: function (key, cb) { + var store = this.store + var LOCK_PATH = this.LOCK_PATH + var rs = store.createReadStream(key) + + function onFinish (buff) { + lockFile.unlock(LOCK_PATH, function (err) { + if (err) return cb(err) + + cb(null, buff.toString('utf8')) + }) + } + + function onLock (err) { + if (err) return cb(err) + + rs.on('error', cb) + rs.pipe(concat(onFinish)) + } + + lockFile.lock(LOCK_PATH, {}, onLock) + + return rs + }, + + /** + * Read a blob on a given path + * It does not lock + * + * @param {String} key + * @param {Function} cb + * @return {ReadableStream} + */ + readWithoutLock: function (key, cb) { + var rs = this.store.createReadStream(key) + + rs.on('error', cb) + rs.pipe(concat(function (buff) { + cb(null, buff.toString('utf8')) + })) + + return rs + }, + + /** + * Write the contents to the blob in the given path + * It holds the repo.lock while reading + * + * @param {String} key + * @param {Function} cb + * @return {WritableStream} + */ + write: function (key, content, cb) { + var store = this.store + var LOCK_PATH = this.LOCK_PATH + var ws = store.createWriteStream(key) + + function onFinish (err) { + if (err) return cb(err) + + lockFile.unlock(LOCK_PATH, cb) + } + + function onLock (err) { + if (err) return cb(err) + + ws.on('error', cb) + ws.on('finish', onFinish) + + ws.write(content) + ws.end() + } + + lockFile.lock(LOCK_PATH, {}, onLock) + + return ws + }, + + /** + * Writes content to a blob on a given path + * It does not lock + * + * @param {String} key + * @param {String} content + * @param {Function} cb + * @return {WritableStream} + */ + writeWithoutLock: function (key, content, cb) { + var ws = this.store.createWriteStream(key) + + ws.on('error', cb) + ws.on('finish', cb) + + ws.write(content) + ws.end() + + return ws + } +} + +module.exports = BlobStore diff --git a/src/adaptors/index.js b/src/adaptors/index.js new file mode 100644 index 00000000..35755d25 --- /dev/null +++ b/src/adaptors/index.js @@ -0,0 +1,4 @@ +module.exports = { + 'fs-repo': require('./fs-repo'), + 'memory-repo': require('./memory-repo') +} diff --git a/src/adaptors/memory-repo/index.js b/src/adaptors/memory-repo/index.js new file mode 100644 index 00000000..a65e587b --- /dev/null +++ b/src/adaptors/memory-repo/index.js @@ -0,0 +1,74 @@ +var concat = require('concat-stream') +var ms = require('abstract-blob-store') + +function BlobStore () { + this.store = ms() +} + +BlobStore.prototype = { + + /** + * Read a blob on a given path + * + * @param {String} key + * @param {Function} cb + * @return {ReadableStream} + */ + read: function (key, cb) { + var store = this.store + var rs = store.createReadStream(key) + + function onFinish (buff) { + cb(null, buff.toString('utf8')) + } + + rs.on('error', cb) + rs.pipe(concat(onFinish)) + + return rs + }, + + /** + * Read a blob on a given path + * + * @param {String} key + * @param {Function} cb + * @return {ReadableStream} + */ + readWithoutLock: function (key, cb) { + return this.read(key, cb) + }, + + /** + * Write the contents to the blob in the given path + * + * @param {String} key + * @param {Function} cb + * @return {WritableStream} + */ + write: function (key, content, cb) { + var store = this.store + var ws = store.createWriteStream(key) + + ws.on('error', cb) + ws.on('finish', cb) + + ws.write(content) + ws.end() + + return ws + }, + + /** + * Write the contents to the blob in the given path + * + * @param {String} key + * @param {Function} cb + * @return {WritableStream} + */ + writeWithoutLock: function (key, cb) { + return this.write(key, cb) + } +} + +module.exports = BlobStore diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..f9ccc0be --- /dev/null +++ b/src/index.js @@ -0,0 +1,46 @@ +var stores = require('./stores') +var adaptors = require('./adaptors') + +/** + * Constructor + * + * @param {String} root_path + * @param {Maybe} options + */ +function Repo (root_path, options) { + this.root_path = root_path + this.options = options || {} + + var Adaptor = this._chooseAdaptor() + this.store = new Adaptor(this.root_path) +} + +Repo.prototype = { + _chooseAdaptor: function () { + var adaptor = adaptors[this.options.adaptor || 'fs-repo'] + + if (!adaptor) { + throw new Error('Adaptor "' + this.options.adaptor + '" not supported') + } + + return adaptor + }, + + api: function () { + return stores.config(this.store) + }, + + config: function () { + return stores.config(this.store) + }, + + version: function () { + return stores.version(this.store) + }, + + blocks: function () { + return stores.blocks(this.store) + } +} + +module.exports = Repo diff --git a/src/stores/api.js b/src/stores/api.js new file mode 100644 index 00000000..4944ed8e --- /dev/null +++ b/src/stores/api.js @@ -0,0 +1,11 @@ +module.exports = function (store) { + return { + read: function (cb) { + return store.readWithoutLock('api', cb) + }, + + write: function (content, cb) { + return store.writeWithoutLock('api', content, cb) + } + } +} diff --git a/src/stores/blocks.js b/src/stores/blocks.js new file mode 100644 index 00000000..974e3594 --- /dev/null +++ b/src/stores/blocks.js @@ -0,0 +1,23 @@ +// TODO: This may end up being configurable +// TODO: This should belong to the `fs` implementation +var PREFIX_LENGTH = 8 +var multihash = require('multihashes') +var path = require('path') + +module.exports = function (store) { + function hashToPath (hash) { + var folder = hash.slice(0, PREFIX_LENGTH) + return path.join(folder, hash) + '.data' + } + + return { + read: function (hash, cb) { + return store.read(hashToPath(hash), cb) + }, + + write: function (buf, cb) { + var mh = multihash.encode(buf, 'hex') + return store.write(hashToPath(mh), buf, cb) + } + } +} diff --git a/src/stores/config.js b/src/stores/config.js new file mode 100644 index 00000000..d28d9264 --- /dev/null +++ b/src/stores/config.js @@ -0,0 +1,19 @@ +module.exports = function (store) { + return { + read: function (cb) { + return store.read('config', function (err, content) { + if (err) return cb(err) + + try { + cb(null, JSON.parse(content)) + } catch (e) { + cb(e) + } + }) + }, + + write: function (content, cb) { + return store.write('config', JSON.stringify(content), cb) + } + } +} diff --git a/src/stores/index.js b/src/stores/index.js new file mode 100644 index 00000000..7e6503a8 --- /dev/null +++ b/src/stores/index.js @@ -0,0 +1,7 @@ +module.exports = { + version: require('./version'), + config: require('./config'), + logs: require('./logs'), + api: require('./api'), + repo: require('./repo') +} diff --git a/src/stores/repo.js b/src/stores/repo.js new file mode 100644 index 00000000..831b2206 --- /dev/null +++ b/src/stores/repo.js @@ -0,0 +1,11 @@ +module.exports = function (store) { + return { + read: function (cb) { + return store.readWithoutLock('repo.lock', cb) + }, + + write: function (content, cb) { + return store.writeWithoutLock('repo.lock', content, cb) + } + } +} diff --git a/src/stores/version.js b/src/stores/version.js new file mode 100644 index 00000000..783a345e --- /dev/null +++ b/src/stores/version.js @@ -0,0 +1,15 @@ +module.exports = function (store) { + return { + read: function (cb) { + return store.read('version', function (err, num) { + if (err) return cb(err) + + cb(null, num.split('\n')[0]) + }) + }, + + write: function (content, cb) { + return store.write('version', content, cb) + } + } +}