Skip to content

Commit 89b0d7f

Browse files
authored
fix: os-native add-to-ipfs on Windows and macOS (#1976)
* fix: use new glob source API See ipfs/js-ipfs#3889 * feat: add tests and improve add-to-ipfs
1 parent 4ff45f2 commit 89b0d7f

9 files changed

+158
-40
lines changed

package-lock.json

+7-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
"is-ipfs": "6.0.2",
9393
"it-all": "^1.0.6",
9494
"it-concat": "^2.0.0",
95+
"it-last": "^1.0.6",
9596
"multiaddr": "10.0.1",
9697
"multiaddr-to-uri": "8.0.0",
9798
"portfinder": "^1.0.28",

src/add-to-ipfs.js

+61-32
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,73 @@
11
const { extname, basename } = require('path')
22
const { clipboard } = require('electron')
3+
const { globSource } = require('ipfs-http-client')
34
const i18n = require('i18next')
5+
const last = require('it-last')
6+
const fs = require('fs-extra')
47
const logger = require('./common/logger')
58
const { notify, notifyError } = require('./common/notify')
6-
const { globSource } = require('ipfs-http-client')
79

8-
async function copyFile (ipfs, cid, name) {
10+
async function copyFileToMfs (ipfs, cid, filename) {
911
let i = 0
10-
const ext = extname(name)
11-
const base = basename(name, ext)
12+
const ext = extname(filename)
13+
const base = basename(filename, ext)
1214

1315
while (true) {
1416
const newName = (i === 0 ? base : `${base} (${i})`) + ext
1517

1618
try {
1719
await ipfs.files.stat(`/${newName}`)
1820
} catch (err) {
19-
name = newName
21+
filename = newName
2022
break
2123
}
2224

2325
i++
2426
}
2527

26-
return ipfs.files.cp(`/ipfs/${cid.toString()}`, `/${name}`)
28+
return ipfs.files.cp(`/ipfs/${cid.toString()}`, `/${filename}`)
2729
}
2830

29-
async function makeShareableObject (ipfs, results) {
30-
if (results.length === 1) {
31+
async function getShareableCid (ipfs, files) {
32+
if (files.length === 1) {
3133
// If it's just one object, we link it directly.
32-
return results[0]
34+
return files[0]
3335
}
3436

35-
let baseCID = await ipfs.object.new({ template: 'unixfs-dir' })
37+
// Note: we don't use 'object patch' here, it was deprecated.
38+
// We are using MFS for creating CID of an ephemeral directory
39+
// because it handles HAMT-sharding of big directories automatically
40+
// See: https://github.com/ipfs/go-ipfs/issues/8106
41+
const dirpath = `/zzzz_${Date.now()}`
42+
await ipfs.files.mkdir(dirpath, {})
3643

37-
for (const { cid, path, size } of results) {
38-
baseCID = (await ipfs.object.patch.addLink(baseCID, {
39-
name: path,
40-
size,
41-
cid
42-
}))
44+
for (const { cid, filename } of files) {
45+
await ipfs.files.cp(`/ipfs/${cid}`, `${dirpath}/${filename}`)
4346
}
4447

45-
return { cid: baseCID, path: '' }
48+
const stat = await ipfs.files.stat(dirpath)
49+
50+
// Do not wait for this
51+
ipfs.files.rm(dirpath, { recursive: true })
52+
53+
return { cid: stat.cid, filename: '' }
4654
}
4755

48-
function sendNotification (failures, successes, launchWebUI, path) {
56+
function sendNotification (launchWebUI, hasFailures, successCount, filename) {
4957
let link, title, body, fn
5058

51-
if (failures.length === 0) {
59+
if (!hasFailures) {
5260
// All worked well!
5361
fn = notify
5462

55-
if (successes.length === 1) {
56-
link = `/files/${path}`
63+
if (successCount === 1) {
64+
link = `/files/${filename}`
5765
title = i18n.t('itemAddedNotification.title')
5866
body = i18n.t('itemAddedNotification.message')
5967
} else {
6068
link = '/files'
6169
title = i18n.t('itemsAddedNotification.title')
62-
body = i18n.t('itemsAddedNotification.message', { count: successes.length })
70+
body = i18n.t('itemsAddedNotification.message', { count: successCount })
6371
}
6472
} else {
6573
// Some/all failed!
@@ -75,9 +83,27 @@ function sendNotification (failures, successes, launchWebUI, path) {
7583
})
7684
}
7785

86+
async function addFileOrDirectory (ipfs, filepath) {
87+
const stat = fs.statSync(filepath)
88+
let cid = null
89+
90+
if (stat.isDirectory()) {
91+
const files = globSource(filepath, '**/*', { recursive: true })
92+
const res = await last(ipfs.addAll(files, { pin: false, wrapWithDirectory: true }))
93+
cid = res.cid
94+
} else {
95+
const readStream = fs.createReadStream(filepath)
96+
const res = await ipfs.add(readStream, { pin: false })
97+
cid = res.cid
98+
}
99+
100+
const filename = basename(filepath)
101+
await copyFileToMfs(ipfs, cid, filename)
102+
return { cid, filename }
103+
}
104+
78105
module.exports = async function ({ getIpfsd, launchWebUI }, files) {
79106
const ipfsd = await getIpfsd()
80-
81107
if (!ipfsd) {
82108
return
83109
}
@@ -89,23 +115,26 @@ module.exports = async function ({ getIpfsd, launchWebUI }, files) {
89115

90116
await Promise.all(files.map(async file => {
91117
try {
92-
const result = await ipfsd.api.add(globSource(file, { recursive: true }), { pin: false })
93-
await copyFile(ipfsd.api, result.cid, result.path)
94-
successes.push(result)
118+
const res = await addFileOrDirectory(ipfsd.api, file)
119+
successes.push(res)
95120
} catch (e) {
96-
failures.push(e)
121+
failures.push(e.toString())
97122
}
98123
}))
99124

100125
if (failures.length > 0) {
101-
log.fail(new Error(failures.reduce((prev, curr) => `${prev} ${curr.toString()}`, '')))
126+
log.fail(new Error(failures.join('\n')))
102127
} else {
103128
log.end()
104129
}
105130

106-
const { cid, path } = await makeShareableObject(ipfsd.api, successes)
107-
sendNotification(failures, successes, launchWebUI, path)
108-
const filename = path ? `?filename=${encodeURIComponent(path.split('/').pop())}` : ''
109-
const url = `https://dweb.link/ipfs/${cid.toString()}${filename}`
131+
const { cid, filename } = await getShareableCid(ipfsd.api, successes)
132+
sendNotification(launchWebUI, failures.length !== 0, successes.length, filename)
133+
134+
const query = filename ? `?filename=${encodeURIComponent(filename)}` : ''
135+
const url = `https://dweb.link/ipfs/${cid.toString()}${query}`
136+
110137
clipboard.writeText(url)
138+
139+
return cid
111140
}

test/unit/add-to-ipfs.spec.js

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* eslint-env mocha */
2+
3+
const chai = require('chai')
4+
const path = require('path')
5+
const { expect } = chai
6+
const dirtyChai = require('dirty-chai')
7+
8+
const mockElectron = require('./mocks/electron')
9+
const mockLogger = require('./mocks/logger')
10+
const mockNotify = require('./mocks/notify')
11+
12+
const proxyquire = require('proxyquire').noCallThru()
13+
14+
const { makeRepository } = require('./../e2e/utils/ipfsd')
15+
16+
chai.use(dirtyChai)
17+
18+
const getFixtures = (...files) => files.map(f => path.join(__dirname, 'fixtures', f))
19+
20+
describe('Add To Ipfs', function () {
21+
this.timeout(5000)
22+
23+
let electron, notify, addToIpfs, ipfsd, ctx
24+
25+
before(async () => {
26+
const repo = await makeRepository({ start: true })
27+
ipfsd = repo.ipfsd
28+
ctx = {
29+
getIpfsd: () => ipfsd,
30+
launchWebUI: () => {}
31+
}
32+
})
33+
34+
after(() => {
35+
if (ipfsd) ipfsd.stop()
36+
})
37+
38+
beforeEach(async () => {
39+
electron = mockElectron()
40+
notify = mockNotify()
41+
addToIpfs = proxyquire('../../src/add-to-ipfs', {
42+
electron: electron,
43+
'./common/notify': notify,
44+
'./common/logger': mockLogger()
45+
})
46+
})
47+
48+
it('add to ipfs single file', async () => {
49+
const cid = await addToIpfs(ctx, getFixtures('hello-world.txt'))
50+
expect(electron.clipboard.writeText.callCount).to.equal(1)
51+
expect(notify.notifyError.callCount).to.equal(0)
52+
expect(notify.notify.callCount).to.equal(1)
53+
expect(cid.toString()).to.equal('QmWGeRAEgtsHW3ec7U4qW2CyVy7eA2mFRVbk1nb24jFyks')
54+
})
55+
56+
it('add to ipfs single directory', async () => {
57+
const cid = await addToIpfs(ctx, getFixtures('dir'))
58+
expect(electron.clipboard.writeText.callCount).to.equal(1)
59+
expect(notify.notifyError.callCount).to.equal(0)
60+
expect(notify.notify.callCount).to.equal(1)
61+
expect(cid.toString()).to.equal('QmVuxXkWEyCKvQiMqVnDiwyJUUyDQZ7VsKhQDCZzPj1Yq8')
62+
})
63+
64+
it('add to ipfs multiple files', async () => {
65+
const cid = await addToIpfs(ctx, getFixtures('dir', 'hello-world.txt'))
66+
expect(electron.clipboard.writeText.callCount).to.equal(1)
67+
expect(notify.notifyError.callCount).to.equal(0)
68+
expect(notify.notify.callCount).to.equal(1)
69+
expect(cid.toString()).to.equal('QmdYASNGKMVK4HL1uzi3VCZyjQGg3M6VuLsgX5xTKL1gvH')
70+
})
71+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello, IPFS!

test/unit/fixtures/hello-world.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello, world!

test/unit/mocks/electron.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ module.exports = function mockElectron (opts = {}) {
99
BrowserWindow: {
1010
getAllWindows: sinon.stub()
1111
},
12-
app: {}
12+
app: {},
13+
clipboard: {
14+
writeText: sinon.spy()
15+
}
1316
}
1417

1518
if (opts.withDock) {

test/unit/mocks/logger.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
module.exports = function () {
22
return {
3-
start: () => {},
3+
start: () => ({
4+
fail: () => {},
5+
end: () => {}
6+
}),
47
info: () => {},
58
error: () => {}
69
}

test/unit/mocks/notify.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const sinon = require('sinon')
2+
3+
module.exports = function mockNotify () {
4+
return {
5+
notify: sinon.spy(),
6+
notifyError: sinon.spy()
7+
}
8+
}

0 commit comments

Comments
 (0)