Skip to content
This repository was archived by the owner on Mar 10, 2020. It is now read-only.

Commit 8e5d241

Browse files
committed
feat: adds support for -X symbolic mode and recursive chmod
Also adds tests for `a` symbolic mode, e.g. `a+rwx`, etc.
1 parent ec38560 commit 8e5d241

File tree

3 files changed

+176
-17
lines changed

3 files changed

+176
-17
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@
7373
"ipfs-multipart": "^0.3.0",
7474
"ipfs-unixfs": "^0.3.0",
7575
"ipfs-unixfs-exporter": "^0.40.0",
76-
"ipfs-unixfs-importer": "^0.43.1",
76+
"ipfs-unixfs-importer": "ipfs/js-ipfs-unixfs-importer#support-dagnodes-as-content",
7777
"ipfs-utils": "^0.4.2",
7878
"ipld-dag-pb": "^0.18.0",
7979
"it-last": "^1.0.1",
80+
"it-pipe": "^1.0.1",
8081
"joi-browser": "^13.4.0",
8182
"mortice": "^2.0.0",
8283
"multicodec": "^1.0.0",

src/core/chmod.js

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ const updateMfsRoot = require('./utils/update-mfs-root')
1212
const { DAGNode } = require('ipld-dag-pb')
1313
const mc = require('multicodec')
1414
const mh = require('multihashes')
15+
const pipe = require('it-pipe')
16+
const importer = require('ipfs-unixfs-importer')
17+
const exporter = require('ipfs-unixfs-exporter')
18+
const last = require('it-last')
19+
const cp = require('./cp')
20+
const rm = require('./rm')
1521

1622
const defaultOptions = {
1723
flush: true,
@@ -21,10 +27,10 @@ const defaultOptions = {
2127
recursive: false
2228
}
2329

24-
function calculateModification (mode) {
30+
function calculateModification (mode, originalMode, isDirectory) {
2531
let modification = 0
2632

27-
if (mode.includes('x')) {
33+
if (mode.includes('x') || (mode.includes('X') && (isDirectory || (originalMode & 0o1 || originalMode & 0o10 || originalMode & 0o100)))) {
2834
modification += 1
2935
}
3036

@@ -76,7 +82,7 @@ function calculateSpecial (references, mode, modification) {
7682
}
7783

7884
// https://en.wikipedia.org/wiki/Chmod#Symbolic_modes
79-
function parseSymbolicMode (input, originalMode) {
85+
function parseSymbolicMode (input, originalMode, isDirectory) {
8086
if (!originalMode) {
8187
originalMode = 0
8288
}
@@ -98,7 +104,7 @@ function parseSymbolicMode (input, originalMode) {
98104
references = 'ugo'
99105
}
100106

101-
let modification = calculateModification(mode)
107+
let modification = calculateModification(mode, originalMode, isDirectory)
102108
modification = calculateUGO(references, modification)
103109
modification = calculateSpecial(references, mode, modification)
104110

@@ -139,6 +145,20 @@ function parseSymbolicMode (input, originalMode) {
139145
}
140146
}
141147

148+
function calculateMode (mode, metadata) {
149+
if (typeof mode === 'string' || mode instanceof String) {
150+
if (mode.match(/^\d+$/g)) {
151+
mode = parseInt(mode, 8)
152+
} else {
153+
mode = mode.split(',').reduce((curr, acc) => {
154+
return parseSymbolicMode(acc, curr, metadata.isDirectory())
155+
}, metadata.mode)
156+
}
157+
}
158+
159+
return mode
160+
}
161+
142162
module.exports = (context) => {
143163
return async function mfsChmod (path, mode, options) {
144164
options = applyDefaultOptions(options, defaultOptions)
@@ -155,20 +175,38 @@ module.exports = (context) => {
155175
throw errCode(new Error(`${path} was not a UnixFS node`), 'ERR_NOT_UNIXFS')
156176
}
157177

158-
let node = await context.ipld.get(cid)
159-
const metadata = UnixFS.unmarshal(node.Data)
160-
161-
if (typeof mode === 'string' || mode instanceof String) {
162-
if (mode.match(/^\d+$/g)) {
163-
mode = parseInt(mode, 8)
164-
} else {
165-
mode = mode.split(',').reduce((curr, acc) => {
166-
return parseSymbolicMode(acc, curr)
167-
}, metadata.mode)
168-
}
178+
if (options.recursive) {
179+
// recursively export from root CID, change perms of each entry then reimport
180+
// but do not reimport files, only manipulate dag-pb nodes
181+
const root = await pipe(
182+
async function * () {
183+
for await (const entry of exporter.recursive(cid, context.ipld)) {
184+
let node = await context.ipld.get(entry.cid)
185+
entry.unixfs.mode = calculateMode(mode, entry.unixfs)
186+
node = new DAGNode(entry.unixfs.marshal(), node.Links)
187+
188+
yield {
189+
path: entry.path,
190+
content: node
191+
}
192+
}
193+
},
194+
(source) => importer(source, context.ipld, options),
195+
(nodes) => last(nodes)
196+
)
197+
198+
// remove old path from mfs
199+
await rm(context)(path, options)
200+
201+
// add newly created tree to mfs at path
202+
await cp(context)(`/ipfs/${root.cid}`, path, options)
203+
204+
return
169205
}
170206

171-
metadata.mode = mode
207+
let node = await context.ipld.get(cid)
208+
const metadata = UnixFS.unmarshal(node.Data)
209+
metadata.mode = calculateMode(mode, metadata)
172210
node = new DAGNode(metadata.marshal(), node.Links)
173211

174212
const updatedCid = await context.ipld.put(node, mc.DAG_PB, {

test/core/chmod.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ describe('chmod', () => {
9595
await testChmod('0000', 'ugo+x', '0111')
9696
await testChmod('0000', 'ugo+w', '0222')
9797
await testChmod('0000', 'ugo+r', '0444')
98+
await testChmod('0000', 'a+x', '0111')
99+
await testChmod('0000', 'a+w', '0222')
100+
await testChmod('0000', 'a+r', '0444')
98101
})
99102

100103
it('should update modes with basic symbolic notation that removes bits', async () => {
@@ -116,6 +119,9 @@ describe('chmod', () => {
116119
await testChmod('0111', 'ugo-x', '0000')
117120
await testChmod('0222', 'ugo-w', '0000')
118121
await testChmod('0444', 'ugo-r', '0000')
122+
await testChmod('0111', 'a-x', '0000')
123+
await testChmod('0222', 'a-w', '0000')
124+
await testChmod('0444', 'a-r', '0000')
119125
})
120126

121127
it('should update modes with basic symbolic notation that overrides bits', async () => {
@@ -137,6 +143,9 @@ describe('chmod', () => {
137143
await testChmod('0777', 'ugo=x', '0111')
138144
await testChmod('0777', 'ugo=w', '0222')
139145
await testChmod('0777', 'ugo=r', '0444')
146+
await testChmod('0777', 'a=x', '0111')
147+
await testChmod('0777', 'a=w', '0222')
148+
await testChmod('0777', 'a=r', '0444')
140149
})
141150

142151
it('should update modes with multiple symbolic notation', async () => {
@@ -149,4 +158,115 @@ describe('chmod', () => {
149158
await testChmod('0000', '+t', '1000')
150159
await testChmod('0000', '+s', '6000')
151160
})
161+
162+
it('should apply special execute permissions to world', async () => {
163+
const path = `/foo-${Date.now()}`
164+
const sub = `${path}/sub`
165+
const file = `${path}/sub/foo.txt`
166+
const bin = `${path}/sub/bar`
167+
168+
await mfs.mkdir(sub, {
169+
parents: true
170+
})
171+
await mfs.touch(file)
172+
await mfs.touch(bin)
173+
174+
await mfs.chmod(path, 0o644, {
175+
recursive: true
176+
})
177+
await mfs.chmod(bin, 'u+x')
178+
179+
expect((await mfs.stat(path)).mode).to.equal(0o644)
180+
expect((await mfs.stat(sub)).mode).to.equal(0o644)
181+
expect((await mfs.stat(file)).mode).to.equal(0o644)
182+
expect((await mfs.stat(bin)).mode).to.equal(0o744)
183+
184+
await mfs.chmod(path, 'a+X', {
185+
recursive: true
186+
})
187+
188+
// directories should be world-executable
189+
expect((await mfs.stat(path)).mode).to.equal(0o755)
190+
expect((await mfs.stat(sub)).mode).to.equal(0o755)
191+
192+
// files without prior execute bit should be untouched
193+
expect((await mfs.stat(file)).mode).to.equal(0o644)
194+
195+
// files with prior execute bit should now be world-executable
196+
expect((await mfs.stat(bin)).mode).to.equal(0o755)
197+
})
198+
199+
it('should apply special execute permissions to user', async () => {
200+
const path = `/foo-${Date.now()}`
201+
const sub = `${path}/sub`
202+
const file = `${path}/sub/foo.txt`
203+
const bin = `${path}/sub/bar`
204+
205+
await mfs.mkdir(sub, {
206+
parents: true
207+
})
208+
await mfs.touch(file)
209+
await mfs.touch(bin)
210+
211+
await mfs.chmod(path, 0o644, {
212+
recursive: true
213+
})
214+
await mfs.chmod(bin, 'u+x')
215+
216+
expect((await mfs.stat(path)).mode).to.equal(0o644)
217+
expect((await mfs.stat(sub)).mode).to.equal(0o644)
218+
expect((await mfs.stat(file)).mode).to.equal(0o644)
219+
expect((await mfs.stat(bin)).mode).to.equal(0o744)
220+
221+
await mfs.chmod(path, 'u+X', {
222+
recursive: true
223+
})
224+
225+
// directories should be user executable
226+
expect((await mfs.stat(path)).mode).to.equal(0o744)
227+
expect((await mfs.stat(sub)).mode).to.equal(0o744)
228+
229+
// files without prior execute bit should be untouched
230+
expect((await mfs.stat(file)).mode).to.equal(0o644)
231+
232+
// files with prior execute bit should now be user executable
233+
expect((await mfs.stat(bin)).mode).to.equal(0o744)
234+
})
235+
236+
it('should apply special execute permissions to user and group', async () => {
237+
const path = `/foo-${Date.now()}`
238+
const sub = `${path}/sub`
239+
const file = `${path}/sub/foo.txt`
240+
const bin = `${path}/sub/bar`
241+
242+
await mfs.mkdir(sub, {
243+
parents: true
244+
})
245+
await mfs.touch(file)
246+
await mfs.touch(bin)
247+
248+
await mfs.chmod(path, 0o644, {
249+
recursive: true
250+
})
251+
await mfs.chmod(bin, 'u+x')
252+
253+
expect((await mfs.stat(path)).mode).to.equal(0o644)
254+
expect((await mfs.stat(sub)).mode).to.equal(0o644)
255+
expect((await mfs.stat(file)).mode).to.equal(0o644)
256+
expect((await mfs.stat(bin)).mode).to.equal(0o744)
257+
258+
await mfs.chmod(path, 'ug+X', {
259+
recursive: true
260+
})
261+
262+
// directories should be user and group executable
263+
expect((await mfs.stat(path)).mode).to.equal(0o754)
264+
expect((await mfs.stat(sub)).mode).to.equal(0o754)
265+
266+
// files without prior execute bit should be untouched
267+
expect((await mfs.stat(file)).mode).to.equal(0o644)
268+
269+
// files with prior execute bit should now be user and group executable
270+
expect((await mfs.stat(bin)).mode).to.equal(0o754)
271+
})
152272
})

0 commit comments

Comments
 (0)