diff --git a/SPEC/FILES.md b/SPEC/FILES.md index 5f808307..ce5ac4ef 100644 --- a/SPEC/FILES.md +++ b/SPEC/FILES.md @@ -387,6 +387,7 @@ pull( A great source of [examples][] can be found in the tests for this API. + #### `ls` > Lists a directory from IPFS that is addressed by a valid IPFS Path. @@ -427,7 +428,7 @@ If no `callback` is passed, a promise is returned. ```JavaScript const validCID = 'QmQ2r6iMNpky5f1m4cnm3Yqw8VSvjuKpTcK1X7dBR1LkJF' -ipfs.files.ls(validCID, function (err, files) { +ipfs.ls(validCID, function (err, files) { files.forEach((file) => { console.log(file.path) }) @@ -539,6 +540,297 @@ pull( A great source of [examples][] can be found in the tests for this API. +-------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------- + +Mutable File System +=================== + +The Mutable File System (MFS) is a virtual file system on top of IPFS that exposes a Unix like API over a virtual directory. It enables users to write and read from paths without having to worry about updating the graph. It enables things like [ipfs-blob-store](https://github.com/ipfs/ipfs-blob-store) to exist. + + +#### `cp` + +> Copy files. + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.cp([from, to], [callback]) + +Where: + +- `from` is the path of the source object to copy. +- `to` is the path of the destination object to copy to. + +`callback` must follow the `function (err) {}` signature, where `err` is an Error if the operation was not successful. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +ipfs.files.cp(['/src-file', '/dst-file'], (err) => { + if (err) { + console.error(err) + } +}) +``` + +#### `mkdir` + +> Make a directory. + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.mkdir(path, [options, callback]) + +Where: + +- `path` is the path to the directory to make. +- `options` is an optional Object that might contain the following keys: + - `parents` is a Boolean value to decide whether or not to make the parent directories if they don't exist. + +`callback` must follow the `function (err) {}` signature, where `err` is an Error if the operation was not successful. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +ipfs.files.mkdir('/my/beautiful/directory', (err) => { + if (err) { + console.error(err) + } +}) +``` + +#### `stat` + +> Get file or directory status. + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.stat(path, [options, callback]) + +Where: + +- `path` is the path to the directory to make. +- `options` is an optional Object that might contain the following keys: + - `hash` is a Boolean value to return only the hash. + - `size` is a Boolean value to return only the size. + +`callback` must follow the `function (err, stat) {}` signature, where `err` is an Error if the operation was not successful and `stat` is an Object with the following keys: + +- `hash` is a string with the hash. +- `size` is an integer with the size in Bytes. +- `cumulativeSize` is an integer with the cumulative size in Bytes. +- `blocks` is an integer indicating the number of blocks. +- `type` is a string that can be either `directory` or `file`. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +ipfs.files.stat('/file.txt', (err, stats) => { + console.log(stats) +}) + +// { +// hash: 'QmXmJBmnYqXVuicUfn9uDCC8kxCEEzQpsAbeq1iJvLAmVs', +// size: 60, +// cumulativeSize: 118, +// blocks: 1, +// type: 'file' +// } +``` + +#### `rm` + +> Remove a file or directory. + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.rm(path, [options, callback]) + +Where: + +- `path` is the path of the object to remove. +- `options` is an optional Object that might contain the following keys: + - `recursive` is a Boolean value to decide whether or not to remove directories recursively. + +`callback` must follow the `function (err) {}` signature, where `err` is an Error if the operation was not successful. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +// To remove a file +ipfs.files.mkdir('/my/beautiful/file.txt', (err) => { + if (err) { + console.error(err) + } +}) + +// To remove a directory +ipfs.files.mkdir('/my/beautiful/directory', { recursive: true }, (err) => { + if (err) { + console.error(err) + } +}) +``` + +#### `read` + +> Read a file. + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.read(path, [options, callback]) + +Where: + +- `path` is the path of the object to read. +- `options` is an optional Object that might contain the following keys: + - `offset` is an Integer with the byte offset to begin reading from. + - `count` is an Integer with the maximum number of bytes to read. + +`callback` must follow the `function (err, buf) {}` signature, where `err` is an Error if the operation was not successful and `buf` is a Buffer with the contents of `path`. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +ipfs.files.read('/hello-world', (err, buf) => { + console.log(buf.toString()) +}) + +// Hello, World! +``` + +#### `write` + +> Write to a file. + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.write(path, content, [options, callback]) + +Where: + +- `path` is the path of the object to write. +- `content` can be: + - a Buffer instance. + - a Path (caveat: will only work in Node.js). +- `options` is an optional Object that might contain the following keys: + - `offset` is an Integer with the byte offset to begin writing at. + - `create` is a Boolean to indicate to create the file if it doesn't exist. + - `truncate` is a Boolean to indicate if the file should be truncated to size 0 before writing. + - `count` is an Integer with the maximum number of bytes to read. + +`callback` must follow the `function (err) {}` signature, where `err` is an Error if the operation was not successful. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +ipfs.files.write('/hello-world', Buffer.from('Hello, world!'), (err) => { + console.log(err) +}) +``` + +#### `mv` + +> Move files. + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.mv([from, to], [callback]) + +Where: + +- `from` is the path of the source object to move. +- `to` is the path of the destination object to move to. + +`callback` must follow the `function (err) {}` signature, where `err` is an Error if the operation was not successful. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +ipfs.files.mv(['/src-file', '/dst-file'], (err) => { + if (err) { + console.error(err) + } +}) +``` + +#### `flush` + +> Flush a given path's data to the disk + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.flush([path, callback]) + +Where: + +- `path` is the path to flush. Default is `/`. + +`callback` must follow the `function (err) {}` signature, where `err` is an Error if the operation was not successful. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +ipfs.files.flush('/', (err) => { + if (err) { + console.error(err) + } +}) +``` + +#### `ls` + +> List directories in the local mutable namespace. + +##### `Go` **WIP** + +##### `JavaScript` - ipfs.files.ls([path, options, callback]) + +Where: + +- `path` is the path to show listing for. Defaults to `/`. +- `options` is an optional Object that might contain the following keys: + - `l` is a Boolean value o use long listing format. + +`callback` must follow `function (err, files) {}` signature, where `err` is an error if the operation was not successful. `files` is an array containing Objects that contain the following keys: + +- `name` which is the file's name. +- `type` which i the object's type (`directory` or `file`). +- `size` the size of the file in bytes. +- `hash` the hash of the file. + +If no `callback` is passed, a promise is returned. + +**Example:** + +```JavaScript +ipfs.files.ls('/screenshots', function (err, files) { + files.forEach((file) => { + console.log(file.name) + }) +}) + +// 2018-01-22T18:08:46.775Z.png +// 2018-01-22T18:08:49.184Z.png +``` + [examples]: https://github.com/ipfs/interface-ipfs-core/blob/master/src/files.js [b]: https://www.npmjs.com/package/buffer [rs]: https://www.npmjs.com/package/readable-stream diff --git a/src/files.js b/src/files.js index 41725156..6815a62b 100644 --- a/src/files.js +++ b/src/files.js @@ -18,8 +18,9 @@ const through = require('through2') const path = require('path') const bl = require('bl') const isNode = require('detect-node') +const mfs = require('./mfs') -module.exports = (common) => { +const tests = (common) => { describe('.files', function () { this.timeout(40 * 1000) @@ -982,3 +983,8 @@ module.exports = (common) => { }) }) } + +module.exports = (common) => { + tests(common) + mfs(common) +} diff --git a/src/mfs.js b/src/mfs.js new file mode 100644 index 00000000..dc8f00e9 --- /dev/null +++ b/src/mfs.js @@ -0,0 +1,427 @@ +/* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 8] */ + +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) + +module.exports = (common) => { + describe('.files (MFS Specific)', function () { + this.timeout(40 * 1000) + + let ipfs + let withGo + + before(function (done) { + // CI takes longer to instantiate the daemon, so we need to increase the + // timeout for the before step + this.timeout(60 * 1000) + + common.setup((err, factory) => { + expect(err).to.not.exist() + factory.spawnNode((err, node) => { + expect(err).to.not.exist() + ipfs = node + node.id((err, id) => { + withGo = id.agentVersion.startsWith('go-ipfs') + done() + }) + }) + }) + }) + + after((done) => common.teardown(done)) + + describe('.mkdir', function () { + it('make directory on root', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.mkdir('/test', (err) => { + expect(err).to.not.exist() + done() + }) + }) + + it('make directory and its parents', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.mkdir('/test/lv1/lv2', { p: true }, (err) => { + expect(err).to.not.exist() + done() + }) + }) + + it('make already existent directory', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.mkdir('/', (err) => { + expect(err).to.exist() + done() + }) + }) + }) + + describe('.write', function () { + it('expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.write('/test/a', Buffer.from('Hello, world!'), (err) => { + expect(err).to.exist() + done() + }) + }) + + it('expect no error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.write('/test/a', Buffer.from('Hello, world!'), {create: true}, (err) => { + expect(err).to.not.exist() + done() + }) + }) + }) + + describe('.cp', function () { + it('copy file, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.cp(['/test/c', '/test/b'], (err) => { + expect(err).to.exist() + done() + }) + }) + + it('copy file, expect no error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.cp(['/test/a', '/test/b'], (err) => { + expect(err).to.not.exist() + done() + }) + }) + + it('copy dir, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.cp(['/test/lv1/lv3', '/test/lv1/lv4'], (err) => { + expect(err).to.exist() + done() + }) + }) + + it('copy dir, expect no error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.cp(['/test/lv1/lv2', '/test/lv1/lv3'], (err) => { + expect(err).to.not.exist() + done() + }) + }) + }) + + describe('.mv', function () { + it('move file, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.mv(['/test/404', '/test/a'], (err) => { + expect(err).to.exist() + done() + }) + }) + + it('move file, expect no error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.mv(['/test/a', '/test/c'], (err) => { + expect(err).to.not.exist() + done() + }) + }) + + it('move dir, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.mv(['/test/lv1/404', '/test/lv1'], (err) => { + expect(err).to.exist() + done() + }) + }) + + it('move dir, expect no error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.mv(['/test/lv1/lv2', '/test/lv1/lv4'], (err) => { + expect(err).to.not.exist() + done() + }) + }) + }) + + describe('.rm', function () { + it('remove file, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.rm('/test/a', (err) => { + expect(err).to.exist() + done() + }) + }) + + it('remove file, expect no error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.rm('/test/c', (err) => { + expect(err).to.not.exist() + done() + }) + }) + + it('remove dir, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.rm('/test/lv1/lv4', (err) => { + expect(err).to.exist() + done() + }) + }) + + it('remove dir, expect no error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.rm('/test/lv1/lv4', {recursive: true}, (err) => { + expect(err).to.not.exist() + done() + }) + }) + }) + + describe('.stat', function () { + it('stat not found, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.stat('/test/404', (err) => { + expect(err).to.exist() + done() + }) + }) + + it('stat file', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.stat('/test/b', (err, stat) => { + expect(err).to.not.exist() + expect(stat).to.eql({ + type: 'file', + blocks: 1, + size: 13, + hash: 'QmcZojhwragQr5qhTeFAmELik623Z21e3jBTpJXoQ9si1T', + cumulativeSize: 71 + }) + done() + }) + }) + + it('stat dir', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.stat('/test', (err, stat) => { + expect(err).to.not.exist() + expect(stat).to.eql({ + type: 'directory', + blocks: 2, + size: 0, + hash: 'QmVrkkNurBCeJvPRohW5JTvJG4AxGrFg7FnmsZZUS6nJto', + cumulativeSize: 216 + }) + done() + }) + }) + }) + + describe('.read', function () { + it('read not found, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.read('/test/404', (err, buf) => { + expect(err).to.exist() + expect(buf).to.not.exist() + done() + }) + }) + + it('read file', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.read('/test/b', (err, buf) => { + expect(err).to.not.exist() + expect(buf).to.eql(Buffer.from('Hello, world!')) + done() + }) + }) + }) + + describe('.ls', function () { + it('ls not found, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.ls('/test/404', (err, info) => { + expect(err).to.exist() + expect(info).to.not.exist() + done() + }) + }) + + it('ls directory', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.ls('/test', (err, info) => { + expect(err).to.not.exist() + expect(info).to.eql([ + { name: 'b', type: 0, size: 0, hash: '' }, + { name: 'lv1', type: 0, size: 0, hash: '' } + ]) + done() + }) + }) + + it('ls -l directory', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.ls('/test', { l: true }, (err, info) => { + expect(err).to.not.exist() + expect(info).to.eql([ + { + name: 'b', + type: 0, + size: 13, + hash: 'QmcZojhwragQr5qhTeFAmELik623Z21e3jBTpJXoQ9si1T' + }, + { + name: 'lv1', + type: 1, + size: 0, + hash: 'QmaSPtNHYKPjNjQnYX9pdu5ocpKUQEL3itSz8LuZcoW6J5' + } + ]) + done() + }) + }) + }) + + describe('.flush', function () { + it('flush not found, expect error', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.flush('/test/404', (err) => { + expect(err).to.exist() + done() + }) + }) + + it('flush root', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.flush((err) => { + expect(err).to.not.exist() + done() + }) + }) + + it('flush specific dir', (done) => { + if (!withGo) { + console.log('Not supported in js-ipfs yet') + return done() + } + + ipfs.files.flush('/test', (err) => { + expect(err).to.not.exist() + done() + }) + }) + }) + }) +}