Skip to content
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
55 changes: 54 additions & 1 deletion src/CacheFS.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,28 @@ module.exports = class CacheFS {
}
return _root;
}
_lookup(filepath) {
_lookup(filepath, follow = true) {
let dir = this._root;
let partialPath = '/'
for (let part of path.split(filepath)) {
dir = dir.get(part);
if (!dir) throw new ENOENT(filepath);
// Follow symlinks
if (follow) {
const stat = dir.get(STAT)
if (stat.type === 'symlink') {
let target = stat.target
if (!target.startsWith('/')) {
target = path.normalize(path.join(partialPath, target))
}
dir = this._lookup(target)
}
if (!partialPath) {
partialPath = part
} else {
partialPath = path.join(partialPath, part)
}
}
}
return dir;
}
Expand Down Expand Up @@ -184,4 +201,40 @@ module.exports = class CacheFS {
stat(filepath) {
return this._lookup(filepath).get(STAT);
}
lstat(filepath) {
return this._lookup(filepath, false).get(STAT);
}
readlink(filepath) {
return this._lookup(filepath, false).get(STAT).target;
}
symlink(target, filepath) {
let ino, mode;
try {
let oldStat = this.stat(filepath);
if (mode === null) {
mode = oldStat.mode;
}
ino = oldStat.ino;
} catch (err) {}
if (mode == null) {
mode = 0o666;
}
if (ino == null) {
ino = this.autoinc();
}
let dir = this._lookup(path.dirname(filepath));
let basename = path.basename(filepath);
let stat = {
mode,
type: "symlink",
target,
size: 0,
mtimeMs: Date.now(),
ino,
};
let entry = new Map();
entry.set(STAT, stat);
dir.set(basename, entry);
return stat;
}
};
155 changes: 155 additions & 0 deletions src/__tests__/fs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,4 +270,159 @@ describe("fs module", () => {
});
});
});

describe("symlink", () => {
it("symlink a file and read/write to it", done => {
fs.mkdir("/symlink", () => {
fs.writeFile("/symlink/a.txt", "hello", () => {
fs.symlink("/symlink/a.txt", "/symlink/b.txt", () => {
fs.readFile("/symlink/b.txt", "utf8", (err, data) => {
expect(err).toBe(null)
expect(data).toBe("hello")
fs.writeFile("/symlink/b.txt", "world", () => {
fs.readFile("/symlink/a.txt", "utf8", (err, data) => {
expect(err).toBe(null)
expect(data).toBe("world");
done();
})
})
});
});
});
});
});
it("symlink a file and read/write to it (relative)", done => {
fs.mkdir("/symlink", () => {
fs.writeFile("/symlink/a.txt", "hello", () => {
fs.symlink("a.txt", "/symlink/b.txt", () => {
fs.readFile("/symlink/b.txt", "utf8", (err, data) => {
expect(err).toBe(null)
expect(data).toBe("hello")
fs.writeFile("/symlink/b.txt", "world", () => {
fs.readFile("/symlink/a.txt", "utf8", (err, data) => {
expect(err).toBe(null)
expect(data).toBe("world");
done();
})
})
});
});
});
});
});
it("symlink a directory and read/write to it", done => {
fs.mkdir("/symlink", () => {
fs.mkdir("/symlink/a", () => {
fs.writeFile("/symlink/a/file.txt", "data", () => {
fs.symlink("/symlink/a", "/symlink/b", () => {
fs.readdir("/symlink/b", (err, data) => {
expect(err).toBe(null)
expect(data.includes("file.txt")).toBe(true);
fs.readFile("/symlink/b/file.txt", "utf8", (err, data) => {
expect(err).toBe(null)
expect(data).toBe("data")
fs.writeFile("/symlink/b/file2.txt", "world", () => {
fs.readFile("/symlink/a/file2.txt", "utf8", (err, data) => {
expect(err).toBe(null);
expect(data).toBe("world");
done();
})
})
});
});
});
});
});
});
});
it("symlink a directory and read/write to it (relative)", done => {
fs.mkdir("/symlink", () => {
fs.mkdir("/symlink/a", () => {
fs.mkdir("/symlink/b", () => {
fs.writeFile("/symlink/a/file.txt", "data", () => {
fs.symlink("../a", "/symlink/b/c", () => {
fs.readdir("/symlink/b/c", (err, data) => {
expect(err).toBe(null)
expect(data.includes("file.txt")).toBe(true);
fs.readFile("/symlink/b/c/file.txt", "utf8", (err, data) => {
expect(err).toBe(null)
expect(data).toBe("data")
fs.writeFile("/symlink/b/c/file2.txt", "world", () => {
fs.readFile("/symlink/a/file2.txt", "utf8", (err, data) => {
expect(err).toBe(null);
expect(data).toBe("world");
done();
})
})
});
});
});
});
});
});
});
});
it("unlink doesn't follow symlinks", done => {
fs.mkdir("/symlink", () => {
fs.mkdir("/symlink/del", () => {
fs.writeFile("/symlink/del/file.txt", "data", () => {
fs.symlink("/symlink/del/file.txt", "/symlink/del/file2.txt", () => {
fs.readdir("/symlink/del", (err, data) => {
expect(err).toBe(null)
expect(data.includes("file.txt")).toBe(true)
expect(data.includes("file2.txt")).toBe(true)
fs.unlink("/symlink/del/file2.txt", (err, data) => {
expect(err).toBe(null)
fs.readdir("/symlink/del", (err, data) => {
expect(err).toBe(null)
expect(data.includes("file.txt")).toBe(true)
expect(data.includes("file2.txt")).toBe(false)
done();
});
});
});
});
});
});
});
});
it("lstat doesn't follow symlinks", done => {
fs.mkdir("/symlink", () => {
fs.mkdir("/symlink/lstat", () => {
fs.writeFile("/symlink/lstat/file.txt", "data", () => {
fs.symlink("/symlink/lstat/file.txt", "/symlink/lstat/file2.txt", () => {
fs.stat("/symlink/lstat/file2.txt", (err, stat) => {
expect(err).toBe(null)
expect(stat.isFile()).toBe(true)
expect(stat.isSymbolicLink()).toBe(false)
fs.lstat("/symlink/lstat/file2.txt", (err, stat) => {
expect(err).toBe(null)
expect(stat.isFile()).toBe(false)
expect(stat.isSymbolicLink()).toBe(true)
done();
});
});
});
});
});
});
});
});

describe("readlink", () => {
it("readlink returns the target path", done => {
fs.mkdir("/readlink", () => {
fs.writeFile("/readlink/a.txt", "hello", () => {
fs.symlink("/readlink/a.txt", "/readlink/b.txt", () => {
fs.readlink("/readlink/b.txt", "utf8", (err, data) => {
expect(err).toBe(null)
expect(data).toBe("/readlink/a.txt")
done();
});
});
});
});
});
});

});
42 changes: 39 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ module.exports = class FS {
this.rename = this.rename.bind(this)
this.stat = this.stat.bind(this)
this.lstat = this.lstat.bind(this)
this.readlink = this.readlink.bind(this)
this.symlink = this.symlink.bind(this)
}
_cleanParams(filepath, opts, cb, stopClock = null, save = false) {
filepath = path.normalize(filepath);
Expand Down Expand Up @@ -242,8 +244,42 @@ module.exports = class FS {
.catch(cb);
}
lstat(filepath, opts, cb) {
return this.stat(filepath, opts, cb);
[filepath, opts, cb] = this._cleanParams(filepath, opts, cb);
this.superblockPromise
.then(() => {
try {
let data = this._cache.lstat(filepath);
return cb(null, new Stat(data));
} catch (err) {
return cb(err);
}
})
.catch(cb);
}
readlink(filepath, opts, cb) {
[filepath, opts, cb] = this._cleanParams(filepath, opts, cb);
this.superblockPromise
.then(() => {
try {
let data = this._cache.readlink(filepath);
return cb(null, data);
} catch (err) {
return cb(err);
}
})
.catch(cb);
}
symlink(target, filepath, cb) {
[target, filepath, cb] = this._cleanParams2(target, filepath, cb, null, true);
this.superblockPromise
.then(() => {
try {
this._cache.symlink(target, filepath);
return cb(null);
} catch (err) {
return cb(err);
}
})
.catch(cb);
}
readlink() {}
symlink() {}
}