Skip to content

Commit c3ad91a

Browse files
lidelhacdias
authored andcommitted
feat: option to produce TAR archives (#15)
adds opt-in "archive mode" which produces TAR archive, matching ipfs get. --archive produces .tar file instead of an unpacked directory tree --archive --compress produces a .tar.gz file License: MIT Signed-off-by: Marcin Rataj <[email protected]>
1 parent 26f254d commit c3ad91a

File tree

3 files changed

+53
-35
lines changed

3 files changed

+53
-35
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
## Usage
99

1010
```
11-
npx ipfs-or-gateway -c cid -p path [--clean -a apiUrl]
11+
npx ipfs-or-gateway -c cid -p path [--clean --archive --compress -a apiUrl]
1212
```
1313

14+
- `--clean` – remove destination if it already exists
15+
- `--archive` – produce `.tar` archive instead of unpacked directory tree
16+
- `--compress` – compress produced archive with Gzipi, produce `.tar.gz` (requires `--archive`)
17+
1418
## Contributing
1519

1620
PRs accepted.

bin/index.js

+17-14
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,38 @@ const argv = yargs
1818
demandOption: true
1919
}).option('clean', {
2020
describe: 'clean path first',
21-
type: 'boolean'
21+
type: 'boolean',
22+
default: false
23+
}).option('archive', {
24+
describe: 'output a TAR archive',
25+
type: 'boolean',
26+
default: false
27+
}).option('compress', {
28+
describe: 'compress the archive with GZIP compression',
29+
type: 'boolean',
30+
default: false
2231
}).option('api', {
2332
alias: 'a',
2433
describe: 'api url',
25-
type: 'string'
34+
type: 'string',
35+
default: 'https://ipfs.io/api'
2636
}).option('retries', {
2737
alias: 'r',
2838
describe: 'number of retries for each gateway',
29-
type: 'number'
39+
type: 'number',
40+
default: 3
3041
}).option('timeout', {
3142
alias: 't',
3243
describe: 'timeout of request without data from the server',
33-
type: 'number'
44+
type: 'number',
45+
default: 60000
3446
})
3547
.help()
3648
.argv
3749

3850
async function run () {
3951
try {
40-
const opts = {
41-
cid: argv.cid,
42-
path: argv.path,
43-
clean: argv.clean,
44-
api: argv.api,
45-
retries: argv.retries,
46-
timeout: argv.timeout
47-
}
48-
49-
await download(opts)
52+
await download(argv)
5053
} catch (error) {
5154
console.error(error.toString())
5255
process.exit(1)

lib/index.js

+31-20
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
const fs = require('fs-extra')
22
const fetch = require('node-fetch')
33
const tar = require('tar')
4+
const { createGunzip } = require('zlib')
45
const { exec } = require('child_process')
56
const Progress = require('node-fetch-progress')
67
const AbortController = require('abort-controller')
78
const ora = require('ora')
89
const prettyBytes = require('pretty-bytes')
910

10-
function fetchIPFS ({ cid, path }) {
11+
function fetchIPFS ({ cid, path, archive, compress }) {
1112
return new Promise((resolve, reject) => {
12-
exec(`ipfs get ${cid} -o ${path}`, err => {
13+
archive = archive ? '-a' : ''
14+
compress = compress ? '-C' : ''
15+
exec(`ipfs get ${cid} -o ${path} ${archive} ${compress}`, err => {
1316
if (err) return reject(err)
1417
resolve()
1518
})
1619
})
1720
}
1821

19-
async function fetchHTTP ({ api, cid, timeout: timeoutMs, path, spinner }) {
22+
async function fetchHTTP ({ api, cid, timeout: timeoutMs, path, archive, compress, spinner }) {
2023
const url = `${api}/v0/get?arg=${cid}&archive=true&compress=true`
2124
const controller = new AbortController()
2225
const fetchPromise = fetch(url, { signal: controller.signal })
@@ -40,14 +43,18 @@ async function fetchHTTP ({ api, cid, timeout: timeoutMs, path, spinner }) {
4043
}
4144
})
4245

43-
const extractor = tar.extract({
44-
strip: 1,
45-
C: path,
46-
strict: true
47-
})
46+
const writer = archive
47+
? fs.createWriteStream(path)
48+
: tar.extract({
49+
strip: 1,
50+
C: path,
51+
strict: true
52+
})
4853

4954
await new Promise((resolve, reject) => {
50-
res.body.pipe(extractor)
55+
(compress
56+
? res.body.pipe(writer)
57+
: res.body.pipe(createGunzip()).pipe(writer))
5158
.on('error', reject)
5259
.on('finish', () => {
5360
if (progress) progress.removeAllListeners('progress')
@@ -59,31 +66,35 @@ async function fetchHTTP ({ api, cid, timeout: timeoutMs, path, spinner }) {
5966
}
6067
}
6168

62-
module.exports = async (opts) => {
63-
opts.timeout = opts.timeout || 60000
64-
opts.retries = opts.retries || 3
65-
opts.api = opts.api || 'https://ipfs.io/api'
66-
67-
const { cid, path, clean, verbose, timeout, api, retries } = opts
68-
69+
module.exports = async ({ cid, path, clean, archive, compress, verbose, timeout, api, retries }) => {
6970
if (!cid || !path) {
7071
throw new Error('cid and path must be defined')
7172
}
73+
if (compress && !archive) {
74+
throw new Error('compress requires archive mode')
75+
}
76+
77+
// match go-ipfs behaviour: 'ipfs get' adds .tar and .tar.gz if missing
78+
if (compress && !path.endsWith('.tar.gz')) { path += '.tar.gz' }
79+
if (archive && !path.includes('.tar')) { path += '.tar' }
7280

7381
if (await fs.pathExists(path)) {
7482
if (clean) {
75-
await fs.emptyDir(path)
83+
fs.lstatSync(path).isDirectory()
84+
? fs.emptyDirSync(path)
85+
: fs.unlinkSync(path) // --archive produces a file
7686
} else {
87+
// no-op if destination already exists
7788
return
7889
}
7990
}
8091

81-
await fs.ensureDir(path)
92+
if (!archive) await fs.ensureDir(path)
8293
let spinner = ora()
8394

8495
try {
8596
spinner.start('Fetching via IPFS…')
86-
await fetchIPFS({ cid, path })
97+
await fetchIPFS({ cid, path, archive, compress })
8798
spinner.succeed(`Fetched ${cid} to ${path}!`)
8899
return
89100
} catch (_error) {
@@ -97,7 +108,7 @@ module.exports = async (opts) => {
97108
spinner.start(`Fetching via IPFS HTTP gateway (attempt ${i})…`)
98109

99110
try {
100-
await fetchHTTP({ cid, path, timeout, api, verbose, spinner })
111+
await fetchHTTP({ cid, path, archive, compress, timeout, api, verbose, spinner })
101112
spinner.succeed(`Fetched ${cid} to ${path}!`)
102113
return
103114
} catch (e) {

0 commit comments

Comments
 (0)