Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit 7315aa1

Browse files
author
Alan Shaw
authored
feat: add addFromFs method (#1777)
This PR adds a new method [`addFromFs`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/FILES.md#addfromfs) allowing users to more easily add files from their file system without having to specify every single file to add. In the browser the user will receive a "not available" error. I've pulled out a module `glob-source.js` - call it with some file paths and it returns a pull stream source that can be piped to `ipfs.addPullStream`. This PR comes with the following added benefits: * `ipfs add` on the CLI uses `glob-source.js` - **nice and DRY** * `glob-source.js` uses the events that the `glob` module provides allowing the globbing to be a `pull-pushable`, which means that matched paths can begin to be added before all the globbing is done - **faster** * `ipfs add` now supports adding multiple paths, fixes #1625 - **better** * `ipfs add --progress=false` doesn't calculate the total size of the files to be added anymore! It didn't need to do that as the total was completely discarded when progress was disabled. It means we can add BIGGER directories without running into memory issues - **stronger** License: MIT Signed-off-by: Alan Shaw <[email protected]>
1 parent 236c521 commit 7315aa1

File tree

10 files changed

+216
-114
lines changed

10 files changed

+216
-114
lines changed

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"main": "src/core/index.js",
1010
"browser": {
1111
"./src/core/components/init-assets.js": false,
12+
"./src/core/runtime/add-from-fs-nodejs.js": "./src/core/runtime/add-from-fs-browser.js",
1213
"./src/core/runtime/config-nodejs.js": "./src/core/runtime/config-browser.js",
1314
"./src/core/runtime/dns-nodejs.js": "./src/core/runtime/dns-browser.js",
1415
"./src/core/runtime/fetch-nodejs.js": "./src/core/runtime/fetch-browser.js",
@@ -93,7 +94,6 @@
9394
"datastore-core": "~0.6.0",
9495
"datastore-pubsub": "~0.1.1",
9596
"debug": "^4.1.0",
96-
"deep-extend": "~0.6.0",
9797
"err-code": "^1.1.2",
9898
"file-type": "^10.2.0",
9999
"fnv1a": "^1.0.1",
@@ -159,16 +159,14 @@
159159
"promisify-es6": "^1.0.3",
160160
"protons": "^1.0.1",
161161
"pull-abortable": "^4.1.1",
162-
"pull-catch": "^1.0.0",
162+
"pull-cat": "^1.1.11",
163163
"pull-defer": "~0.2.3",
164164
"pull-file": "^1.1.0",
165165
"pull-ndjson": "~0.1.1",
166-
"pull-paramap": "^1.2.2",
167166
"pull-pushable": "^2.2.0",
168167
"pull-sort": "^1.0.1",
169168
"pull-stream": "^3.6.9",
170169
"pull-stream-to-stream": "^1.3.4",
171-
"pull-zip": "^2.0.1",
172170
"pump": "^3.0.0",
173171
"read-pkg-up": "^4.0.0",
174172
"readable-stream": "3.0.6",

src/cli/commands/add.js

Lines changed: 31 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,41 @@
11
'use strict'
22

3-
const fs = require('fs')
4-
const path = require('path')
5-
const glob = require('glob')
63
const sortBy = require('lodash/sortBy')
74
const pull = require('pull-stream')
8-
const paramap = require('pull-paramap')
9-
const zip = require('pull-zip')
105
const getFolderSize = require('get-folder-size')
116
const byteman = require('byteman')
12-
const waterfall = require('async/waterfall')
7+
const reduce = require('async/reduce')
138
const mh = require('multihashes')
149
const multibase = require('multibase')
1510
const { print, isDaemonOn, createProgressBar } = require('../utils')
1611
const { cidToString } = require('../../utils/cid')
12+
const globSource = require('../../utils/files/glob-source')
1713

18-
function checkPath (inPath, recursive) {
19-
// This function is to check for the following possible inputs
20-
// 1) "." add the cwd but throw error for no recursion flag
21-
// 2) "." -r return the cwd
22-
// 3) "/some/path" but throw error for no recursion
23-
// 4) "/some/path" -r
24-
// 5) No path, throw err
25-
// 6) filename.type return the cwd + filename
26-
27-
if (!inPath) {
28-
throw new Error('Error: Argument \'path\' is required')
29-
}
30-
31-
if (inPath === '.') {
32-
inPath = process.cwd()
33-
}
34-
35-
// Convert to POSIX format
36-
inPath = inPath
37-
.split(path.sep)
38-
.join('/')
39-
40-
// Strips trailing slash from path.
41-
inPath = inPath.replace(/\/$/, '')
42-
43-
if (fs.statSync(inPath).isDirectory() && recursive === false) {
44-
throw new Error(`Error: ${inPath} is a directory, use the '-r' flag to specify directories`)
45-
}
46-
47-
return inPath
48-
}
49-
50-
function getTotalBytes (path, recursive, cb) {
51-
if (recursive) {
52-
getFolderSize(path, cb)
53-
} else {
54-
fs.stat(path, (err, stat) => cb(err, stat.size))
55-
}
14+
function getTotalBytes (paths, cb) {
15+
reduce(paths, 0, (total, path, cb) => {
16+
getFolderSize(path, (err, size) => {
17+
if (err) return cb(err)
18+
cb(null, total + size)
19+
})
20+
}, cb)
5621
}
5722

58-
function addPipeline (index, addStream, list, argv) {
23+
function addPipeline (paths, addStream, options) {
5924
const {
25+
recursive,
6026
quiet,
6127
quieter,
6228
silent
63-
} = argv
29+
} = options
6430
pull(
65-
zip(
66-
pull.values(list),
67-
pull(
68-
pull.values(list),
69-
paramap(fs.stat.bind(fs), 50)
70-
)
71-
),
72-
pull.map((pair) => ({
73-
path: pair[0],
74-
isDirectory: pair[1].isDirectory()
75-
})),
76-
pull.filter((file) => !file.isDirectory),
77-
pull.map((file) => ({
78-
path: file.path.substring(index, file.path.length),
79-
content: fs.createReadStream(file.path)
80-
})),
31+
globSource(...paths, { recursive }),
8132
addStream,
8233
pull.collect((err, added) => {
8334
if (err) {
35+
// Tweak the error message and add more relevant infor for the CLI
36+
if (err.code === 'ERR_DIR_NON_RECURSIVE') {
37+
err.message = `'${err.path}' is a directory, use the '-r' flag to specify directories`
38+
}
8439
throw err
8540
}
8641

@@ -90,10 +45,8 @@ function addPipeline (index, addStream, list, argv) {
9045
sortBy(added, 'path')
9146
.reverse()
9247
.map((file) => {
93-
const log = [ 'added', cidToString(file.hash, { base: argv.cidBase }) ]
94-
48+
const log = [ 'added', cidToString(file.hash, { base: options.cidBase }) ]
9549
if (!quiet && file.path.length > 0) log.push(file.path)
96-
9750
return log.join(' ')
9851
})
9952
.forEach((msg) => print(msg))
@@ -102,7 +55,7 @@ function addPipeline (index, addStream, list, argv) {
10255
}
10356

10457
module.exports = {
105-
command: 'add <file>',
58+
command: 'add <file...>',
10659

10760
describe: 'Add a file to IPFS using the UnixFS data format',
10861

@@ -191,8 +144,7 @@ module.exports = {
191144
},
192145

193146
handler (argv) {
194-
const inPath = checkPath(argv.file, argv.recursive)
195-
const index = inPath.lastIndexOf('/') + 1
147+
const { ipfs } = argv
196148
const options = {
197149
strategy: argv.trickle ? 'trickle' : 'balanced',
198150
shardSplitThreshold: argv.enableShardingExperiment
@@ -210,38 +162,21 @@ module.exports = {
210162
if (options.enableShardingExperiment && isDaemonOn()) {
211163
throw new Error('Error: Enabling the sharding experiment should be done on the daemon')
212164
}
213-
const ipfs = argv.ipfs
214165

215-
let list
216-
waterfall([
217-
(next) => {
218-
if (fs.statSync(inPath).isDirectory()) {
219-
return glob('**/*', { cwd: inPath }, next)
220-
}
221-
next(null, [])
222-
},
223-
(globResult, next) => {
224-
if (globResult.length === 0) {
225-
list = [inPath]
226-
} else {
227-
list = globResult.map((f) => inPath + '/' + f)
228-
}
229-
getTotalBytes(inPath, argv.recursive, next)
230-
},
231-
(totalBytes, next) => {
232-
if (argv.progress) {
233-
const bar = createProgressBar(totalBytes)
234-
options.progress = function (byteLength) {
235-
bar.update(byteLength / totalBytes, { progress: byteman(byteLength, 2, 'MB') })
236-
}
237-
}
166+
if (!argv.progress) {
167+
return addPipeline(argv.file, ipfs.addPullStream(options), argv)
168+
}
238169

239-
next(null, ipfs.addPullStream(options))
240-
}
241-
], (err, addStream) => {
170+
getTotalBytes(argv.file, (err, totalBytes) => {
242171
if (err) throw err
243172

244-
addPipeline(index, addStream, list, argv)
173+
const bar = createProgressBar(totalBytes)
174+
175+
options.progress = byteLength => {
176+
bar.update(byteLength / totalBytes, { progress: byteman(byteLength, 2, 'MB') })
177+
}
178+
179+
addPipeline(argv.file, ipfs.addPullStream(options), argv)
245180
})
246181
}
247182
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict'
2+
3+
module.exports = (self) => require('../../runtime/add-from-fs-nodejs')(self)

src/core/components/files-regular/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module.exports = self => ({
44
add: require('./add')(self),
5+
addFromFs: require('./add-from-fs')(self),
56
addFromStream: require('./add-from-stream')(self),
67
addFromURL: require('./add-from-url')(self),
78
addPullStream: require('./add-pull-stream')(self),
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict'
2+
3+
const promisify = require('promisify-es6')
4+
5+
module.exports = self => {
6+
return promisify((...args) => {
7+
const callback = args.pop()
8+
callback(new Error('not available in the browser'))
9+
})
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict'
2+
3+
const promisify = require('promisify-es6')
4+
const pull = require('pull-stream')
5+
const globSource = require('../../utils/files/glob-source')
6+
7+
module.exports = self => {
8+
return promisify((...args) => {
9+
const callback = args.pop()
10+
const options = typeof args[args.length - 1] === 'string' ? {} : args.pop()
11+
const paths = args
12+
13+
pull(
14+
globSource(...paths, options),
15+
self.addPullStream(options),
16+
pull.collect(callback)
17+
)
18+
})
19+
}

src/utils/files/glob-source.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use strict'
2+
3+
const fs = require('fs')
4+
const Path = require('path')
5+
const pull = require('pull-stream')
6+
const glob = require('glob')
7+
const cat = require('pull-cat')
8+
const defer = require('pull-defer')
9+
const pushable = require('pull-pushable')
10+
const map = require('async/map')
11+
const errCode = require('err-code')
12+
13+
/**
14+
* Create a pull stream source that can be piped to ipfs.addPullStream for the
15+
* provided file paths.
16+
*
17+
* @param {String} ...paths File system path(s) to glob from
18+
* @param {Object} [options] Optional options
19+
* @param {Boolean} [options.recursive] Recursively glob all paths in directories
20+
* @param {Boolean} [options.hidden] Include .dot files in matched paths
21+
* @param {Array<String>} [options.ignore] Glob paths to ignore
22+
* @param {Boolean} [options.followSymlinks] follow symlinks
23+
* @returns {Function} pull stream source
24+
*/
25+
module.exports = (...args) => {
26+
const options = typeof args[args.length - 1] === 'string' ? {} : args.pop()
27+
const paths = args
28+
const deferred = defer.source()
29+
30+
const globSourceOptions = {
31+
recursive: options.recursive,
32+
glob: {
33+
dot: Boolean(options.hidden),
34+
ignore: Array.isArray(options.ignore) ? options.ignore : [],
35+
follow: options.followSymlinks != null ? options.followSymlinks : true
36+
}
37+
}
38+
39+
// Check the input paths comply with options.recursive and convert to glob sources
40+
map(paths, pathAndType, (err, results) => {
41+
if (err) return deferred.abort(err)
42+
43+
try {
44+
const sources = results.map(res => toGlobSource(res, globSourceOptions))
45+
deferred.resolve(cat(sources))
46+
} catch (err) {
47+
deferred.abort(err)
48+
}
49+
})
50+
51+
return pull(
52+
deferred,
53+
pull.map(({ path, contentPath }) => ({
54+
path,
55+
content: fs.createReadStream(contentPath)
56+
}))
57+
)
58+
}
59+
60+
function toGlobSource ({ path, type }, options) {
61+
options = options || {}
62+
63+
const baseName = Path.basename(path)
64+
65+
if (type === 'file') {
66+
return pull.values([{ path: baseName, contentPath: path }])
67+
}
68+
69+
if (type === 'dir' && !options.recursive) {
70+
throw errCode(
71+
new Error(`'${path}' is a directory and recursive option not set`),
72+
'ERR_DIR_NON_RECURSIVE',
73+
{ path }
74+
)
75+
}
76+
77+
const globOptions = Object.assign({}, options.glob, {
78+
cwd: path,
79+
nodir: true,
80+
realpath: false,
81+
absolute: false
82+
})
83+
84+
// TODO: want to use pull-glob but it doesn't have the features...
85+
const pusher = pushable()
86+
87+
glob('**/*', globOptions)
88+
.on('match', m => pusher.push(m))
89+
.on('end', () => pusher.end())
90+
.on('abort', () => pusher.end())
91+
.on('error', err => pusher.end(err))
92+
93+
return pull(
94+
pusher,
95+
pull.map(p => ({
96+
path: `${baseName}/${toPosix(p)}`,
97+
contentPath: Path.join(path, p)
98+
}))
99+
)
100+
}
101+
102+
function pathAndType (path, cb) {
103+
fs.stat(path, (err, stat) => {
104+
if (err) return cb(err)
105+
cb(null, { path, type: stat.isDirectory() ? 'dir' : 'file' })
106+
})
107+
}
108+
109+
const toPosix = path => path.replace(/\\/g, '/')

test/cli/files.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ describe('files', () => runOnAndOff((thing) => {
143143
})
144144
})
145145

146+
it('add multiple', function () {
147+
this.timeout(30 * 1000)
148+
149+
return ipfs('add', 'src/init-files/init-docs/readme', 'test/fixtures/odd-name-[v0]/odd name [v1]/hello', '--wrap-with-directory')
150+
.then((out) => {
151+
expect(out)
152+
.to.include('added QmPZ9gcCEpqKTo6aq61g2nXGUhM4iCL3ewB6LDXZCtioEB readme\n')
153+
expect(out)
154+
.to.include('added QmT78zSuBmuS4z925WZfrqQ1qHaJ56DQaTfyMUF7F8ff5o hello\n')
155+
})
156+
})
157+
146158
it('add alias', function () {
147159
this.timeout(30 * 1000)
148160

@@ -278,7 +290,7 @@ describe('files', () => runOnAndOff((thing) => {
278290
it('add --quieter', function () {
279291
this.timeout(30 * 1000)
280292

281-
return ipfs('add -Q -w test/fixtures/test-data/hello test/test-data/node.json')
293+
return ipfs('add -Q -w test/fixtures/test-data/hello')
282294
.then((out) => {
283295
expect(out)
284296
.to.eql('QmYRMUVULBfj7WrdPESnwnyZmtayN6Sdrwh1nKcQ9QgQeZ\n')

0 commit comments

Comments
 (0)