Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: option to produce TAR archives #15

Merged
merged 1 commit into from
Oct 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
## Usage

```
npx ipfs-or-gateway -c cid -p path [--clean -a apiUrl]
npx ipfs-or-gateway -c cid -p path [--clean --archive --compress -a apiUrl]
```

- `--clean` – remove destination if it already exists
- `--archive` – produce `.tar` archive instead of unpacked directory tree
- `--compress` – compress produced archive with Gzipi, produce `.tar.gz` (requires `--archive`)

## Contributing

PRs accepted.
Expand Down
31 changes: 17 additions & 14 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,38 @@ const argv = yargs
demandOption: true
}).option('clean', {
describe: 'clean path first',
type: 'boolean'
type: 'boolean',
default: false
}).option('archive', {
describe: 'output a TAR archive',
type: 'boolean',
default: false
}).option('compress', {
describe: 'compress the archive with GZIP compression',
type: 'boolean',
default: false
}).option('api', {
alias: 'a',
describe: 'api url',
type: 'string'
type: 'string',
default: 'https://ipfs.io/api'
}).option('retries', {
alias: 'r',
describe: 'number of retries for each gateway',
type: 'number'
type: 'number',
default: 3
}).option('timeout', {
alias: 't',
describe: 'timeout of request without data from the server',
type: 'number'
type: 'number',
default: 60000
})
.help()
.argv

async function run () {
try {
const opts = {
cid: argv.cid,
path: argv.path,
clean: argv.clean,
api: argv.api,
retries: argv.retries,
timeout: argv.timeout
}

await download(opts)
await download(argv)
} catch (error) {
console.error(error.toString())
process.exit(1)
Expand Down
51 changes: 31 additions & 20 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
const fs = require('fs-extra')
const fetch = require('node-fetch')
const tar = require('tar')
const { createGunzip } = require('zlib')
const { exec } = require('child_process')
const Progress = require('node-fetch-progress')
const AbortController = require('abort-controller')
const ora = require('ora')
const prettyBytes = require('pretty-bytes')

function fetchIPFS ({ cid, path }) {
function fetchIPFS ({ cid, path, archive, compress }) {
return new Promise((resolve, reject) => {
exec(`ipfs get ${cid} -o ${path}`, err => {
archive = archive ? '-a' : ''
compress = compress ? '-C' : ''
exec(`ipfs get ${cid} -o ${path} ${archive} ${compress}`, err => {
if (err) return reject(err)
resolve()
})
})
}

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

const extractor = tar.extract({
strip: 1,
C: path,
strict: true
})
const writer = archive
? fs.createWriteStream(path)
: tar.extract({
strip: 1,
C: path,
strict: true
})

await new Promise((resolve, reject) => {
res.body.pipe(extractor)
(compress
? res.body.pipe(writer)
: res.body.pipe(createGunzip()).pipe(writer))
.on('error', reject)
.on('finish', () => {
if (progress) progress.removeAllListeners('progress')
Expand All @@ -59,31 +66,35 @@ async function fetchHTTP ({ api, cid, timeout: timeoutMs, path, spinner }) {
}
}

module.exports = async (opts) => {
opts.timeout = opts.timeout || 60000
opts.retries = opts.retries || 3
opts.api = opts.api || 'https://ipfs.io/api'

const { cid, path, clean, verbose, timeout, api, retries } = opts

module.exports = async ({ cid, path, clean, archive, compress, verbose, timeout, api, retries }) => {
if (!cid || !path) {
throw new Error('cid and path must be defined')
}
if (compress && !archive) {
throw new Error('compress requires archive mode')
}

// match go-ipfs behaviour: 'ipfs get' adds .tar and .tar.gz if missing
if (compress && !path.endsWith('.tar.gz')) { path += '.tar.gz' }
if (archive && !path.includes('.tar')) { path += '.tar' }

if (await fs.pathExists(path)) {
if (clean) {
await fs.emptyDir(path)
fs.lstatSync(path).isDirectory()
? fs.emptyDirSync(path)
: fs.unlinkSync(path) // --archive produces a file
} else {
// no-op if destination already exists
return
}
}

await fs.ensureDir(path)
if (!archive) await fs.ensureDir(path)
let spinner = ora()

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

try {
await fetchHTTP({ cid, path, timeout, api, verbose, spinner })
await fetchHTTP({ cid, path, archive, compress, timeout, api, verbose, spinner })
spinner.succeed(`Fetched ${cid} to ${path}!`)
return
} catch (e) {
Expand Down