Skip to content

Commit 3c6e803

Browse files
authored
feat: symlink support (#11)
1 parent 587c741 commit 3c6e803

File tree

3 files changed

+248
-4
lines changed

3 files changed

+248
-4
lines changed

src/CacheFS.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,28 @@ module.exports = class CacheFS {
9898
}
9999
return _root;
100100
}
101-
_lookup(filepath) {
101+
_lookup(filepath, follow = true) {
102102
let dir = this._root;
103+
let partialPath = '/'
103104
for (let part of path.split(filepath)) {
104105
dir = dir.get(part);
105106
if (!dir) throw new ENOENT(filepath);
107+
// Follow symlinks
108+
if (follow) {
109+
const stat = dir.get(STAT)
110+
if (stat.type === 'symlink') {
111+
let target = stat.target
112+
if (!target.startsWith('/')) {
113+
target = path.normalize(path.join(partialPath, target))
114+
}
115+
dir = this._lookup(target)
116+
}
117+
if (!partialPath) {
118+
partialPath = part
119+
} else {
120+
partialPath = path.join(partialPath, part)
121+
}
122+
}
106123
}
107124
return dir;
108125
}
@@ -184,4 +201,40 @@ module.exports = class CacheFS {
184201
stat(filepath) {
185202
return this._lookup(filepath).get(STAT);
186203
}
204+
lstat(filepath) {
205+
return this._lookup(filepath, false).get(STAT);
206+
}
207+
readlink(filepath) {
208+
return this._lookup(filepath, false).get(STAT).target;
209+
}
210+
symlink(target, filepath) {
211+
let ino, mode;
212+
try {
213+
let oldStat = this.stat(filepath);
214+
if (mode === null) {
215+
mode = oldStat.mode;
216+
}
217+
ino = oldStat.ino;
218+
} catch (err) {}
219+
if (mode == null) {
220+
mode = 0o666;
221+
}
222+
if (ino == null) {
223+
ino = this.autoinc();
224+
}
225+
let dir = this._lookup(path.dirname(filepath));
226+
let basename = path.basename(filepath);
227+
let stat = {
228+
mode,
229+
type: "symlink",
230+
target,
231+
size: 0,
232+
mtimeMs: Date.now(),
233+
ino,
234+
};
235+
let entry = new Map();
236+
entry.set(STAT, stat);
237+
dir.set(basename, entry);
238+
return stat;
239+
}
187240
};

src/__tests__/fs.spec.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,159 @@ describe("fs module", () => {
270270
});
271271
});
272272
});
273+
274+
describe("symlink", () => {
275+
it("symlink a file and read/write to it", done => {
276+
fs.mkdir("/symlink", () => {
277+
fs.writeFile("/symlink/a.txt", "hello", () => {
278+
fs.symlink("/symlink/a.txt", "/symlink/b.txt", () => {
279+
fs.readFile("/symlink/b.txt", "utf8", (err, data) => {
280+
expect(err).toBe(null)
281+
expect(data).toBe("hello")
282+
fs.writeFile("/symlink/b.txt", "world", () => {
283+
fs.readFile("/symlink/a.txt", "utf8", (err, data) => {
284+
expect(err).toBe(null)
285+
expect(data).toBe("world");
286+
done();
287+
})
288+
})
289+
});
290+
});
291+
});
292+
});
293+
});
294+
it("symlink a file and read/write to it (relative)", done => {
295+
fs.mkdir("/symlink", () => {
296+
fs.writeFile("/symlink/a.txt", "hello", () => {
297+
fs.symlink("a.txt", "/symlink/b.txt", () => {
298+
fs.readFile("/symlink/b.txt", "utf8", (err, data) => {
299+
expect(err).toBe(null)
300+
expect(data).toBe("hello")
301+
fs.writeFile("/symlink/b.txt", "world", () => {
302+
fs.readFile("/symlink/a.txt", "utf8", (err, data) => {
303+
expect(err).toBe(null)
304+
expect(data).toBe("world");
305+
done();
306+
})
307+
})
308+
});
309+
});
310+
});
311+
});
312+
});
313+
it("symlink a directory and read/write to it", done => {
314+
fs.mkdir("/symlink", () => {
315+
fs.mkdir("/symlink/a", () => {
316+
fs.writeFile("/symlink/a/file.txt", "data", () => {
317+
fs.symlink("/symlink/a", "/symlink/b", () => {
318+
fs.readdir("/symlink/b", (err, data) => {
319+
expect(err).toBe(null)
320+
expect(data.includes("file.txt")).toBe(true);
321+
fs.readFile("/symlink/b/file.txt", "utf8", (err, data) => {
322+
expect(err).toBe(null)
323+
expect(data).toBe("data")
324+
fs.writeFile("/symlink/b/file2.txt", "world", () => {
325+
fs.readFile("/symlink/a/file2.txt", "utf8", (err, data) => {
326+
expect(err).toBe(null);
327+
expect(data).toBe("world");
328+
done();
329+
})
330+
})
331+
});
332+
});
333+
});
334+
});
335+
});
336+
});
337+
});
338+
it("symlink a directory and read/write to it (relative)", done => {
339+
fs.mkdir("/symlink", () => {
340+
fs.mkdir("/symlink/a", () => {
341+
fs.mkdir("/symlink/b", () => {
342+
fs.writeFile("/symlink/a/file.txt", "data", () => {
343+
fs.symlink("../a", "/symlink/b/c", () => {
344+
fs.readdir("/symlink/b/c", (err, data) => {
345+
expect(err).toBe(null)
346+
expect(data.includes("file.txt")).toBe(true);
347+
fs.readFile("/symlink/b/c/file.txt", "utf8", (err, data) => {
348+
expect(err).toBe(null)
349+
expect(data).toBe("data")
350+
fs.writeFile("/symlink/b/c/file2.txt", "world", () => {
351+
fs.readFile("/symlink/a/file2.txt", "utf8", (err, data) => {
352+
expect(err).toBe(null);
353+
expect(data).toBe("world");
354+
done();
355+
})
356+
})
357+
});
358+
});
359+
});
360+
});
361+
});
362+
});
363+
});
364+
});
365+
it("unlink doesn't follow symlinks", done => {
366+
fs.mkdir("/symlink", () => {
367+
fs.mkdir("/symlink/del", () => {
368+
fs.writeFile("/symlink/del/file.txt", "data", () => {
369+
fs.symlink("/symlink/del/file.txt", "/symlink/del/file2.txt", () => {
370+
fs.readdir("/symlink/del", (err, data) => {
371+
expect(err).toBe(null)
372+
expect(data.includes("file.txt")).toBe(true)
373+
expect(data.includes("file2.txt")).toBe(true)
374+
fs.unlink("/symlink/del/file2.txt", (err, data) => {
375+
expect(err).toBe(null)
376+
fs.readdir("/symlink/del", (err, data) => {
377+
expect(err).toBe(null)
378+
expect(data.includes("file.txt")).toBe(true)
379+
expect(data.includes("file2.txt")).toBe(false)
380+
done();
381+
});
382+
});
383+
});
384+
});
385+
});
386+
});
387+
});
388+
});
389+
it("lstat doesn't follow symlinks", done => {
390+
fs.mkdir("/symlink", () => {
391+
fs.mkdir("/symlink/lstat", () => {
392+
fs.writeFile("/symlink/lstat/file.txt", "data", () => {
393+
fs.symlink("/symlink/lstat/file.txt", "/symlink/lstat/file2.txt", () => {
394+
fs.stat("/symlink/lstat/file2.txt", (err, stat) => {
395+
expect(err).toBe(null)
396+
expect(stat.isFile()).toBe(true)
397+
expect(stat.isSymbolicLink()).toBe(false)
398+
fs.lstat("/symlink/lstat/file2.txt", (err, stat) => {
399+
expect(err).toBe(null)
400+
expect(stat.isFile()).toBe(false)
401+
expect(stat.isSymbolicLink()).toBe(true)
402+
done();
403+
});
404+
});
405+
});
406+
});
407+
});
408+
});
409+
});
410+
});
411+
412+
describe("readlink", () => {
413+
it("readlink returns the target path", done => {
414+
fs.mkdir("/readlink", () => {
415+
fs.writeFile("/readlink/a.txt", "hello", () => {
416+
fs.symlink("/readlink/a.txt", "/readlink/b.txt", () => {
417+
fs.readlink("/readlink/b.txt", "utf8", (err, data) => {
418+
expect(err).toBe(null)
419+
expect(data).toBe("/readlink/a.txt")
420+
done();
421+
});
422+
});
423+
});
424+
});
425+
});
426+
});
427+
273428
});

src/index.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ module.exports = class FS {
3535
this.rename = this.rename.bind(this)
3636
this.stat = this.stat.bind(this)
3737
this.lstat = this.lstat.bind(this)
38+
this.readlink = this.readlink.bind(this)
39+
this.symlink = this.symlink.bind(this)
3840
}
3941
_cleanParams(filepath, opts, cb, stopClock = null, save = false) {
4042
filepath = path.normalize(filepath);
@@ -242,8 +244,42 @@ module.exports = class FS {
242244
.catch(cb);
243245
}
244246
lstat(filepath, opts, cb) {
245-
return this.stat(filepath, opts, cb);
247+
[filepath, opts, cb] = this._cleanParams(filepath, opts, cb);
248+
this.superblockPromise
249+
.then(() => {
250+
try {
251+
let data = this._cache.lstat(filepath);
252+
return cb(null, new Stat(data));
253+
} catch (err) {
254+
return cb(err);
255+
}
256+
})
257+
.catch(cb);
258+
}
259+
readlink(filepath, opts, cb) {
260+
[filepath, opts, cb] = this._cleanParams(filepath, opts, cb);
261+
this.superblockPromise
262+
.then(() => {
263+
try {
264+
let data = this._cache.readlink(filepath);
265+
return cb(null, data);
266+
} catch (err) {
267+
return cb(err);
268+
}
269+
})
270+
.catch(cb);
271+
}
272+
symlink(target, filepath, cb) {
273+
[target, filepath, cb] = this._cleanParams2(target, filepath, cb, null, true);
274+
this.superblockPromise
275+
.then(() => {
276+
try {
277+
this._cache.symlink(target, filepath);
278+
return cb(null);
279+
} catch (err) {
280+
return cb(err);
281+
}
282+
})
283+
.catch(cb);
246284
}
247-
readlink() {}
248-
symlink() {}
249285
}

0 commit comments

Comments
 (0)