Skip to content

Commit 3e57982

Browse files
committed
file-server: implement more of the fs api and unit tests, and even stats.isDirectory(), etc.
1 parent cbe8860 commit 3e57982

File tree

5 files changed

+176
-20
lines changed

5 files changed

+176
-20
lines changed

src/packages/conat/core/client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,11 @@ export class Client extends EventEmitter {
11331133
return new Proxy(
11341134
{},
11351135
{
1136-
get: (_, name) => {
1136+
get: (target, name) => {
1137+
const s = target[String(name)];
1138+
if (s !== undefined) {
1139+
return s;
1140+
}
11371141
if (typeof name !== "string") {
11381142
return undefined;
11391143
}

src/packages/conat/files/fs.ts

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ import { conat } from "@cocalc/conat/client";
44
export interface Filesystem {
55
appendFile: (path: string, data: string | Buffer, encoding?) => Promise<void>;
66
chmod: (path: string, mode: string | number) => Promise<void>;
7+
constants: () => Promise<{ [key: string]: number }>;
78
copyFile: (src: string, dest: string) => Promise<void>;
89
cp: (src: string, dest: string, options?) => Promise<void>;
910
exists: (path: string) => Promise<void>;
1011
link: (existingPath: string, newPath: string) => Promise<void>;
12+
lstat: (path: string) => Promise<IStats>;
1113
mkdir: (path: string, options?) => Promise<void>;
1214
readFile: (path: string, encoding?: any) => Promise<string | Buffer>;
1315
readdir: (path: string) => Promise<string[]>;
1416
realpath: (path: string) => Promise<string>;
1517
rename: (oldPath: string, newPath: string) => Promise<void>;
1618
rm: (path: string, options?) => Promise<void>;
1719
rmdir: (path: string, options?) => Promise<void>;
18-
stat: (path: string) => Promise<Stats>;
20+
stat: (path: string) => Promise<IStats>;
1921
symlink: (target: string, path: string) => Promise<void>;
2022
truncate: (path: string, len?: number) => Promise<void>;
2123
unlink: (path: string) => Promise<void>;
@@ -27,7 +29,7 @@ export interface Filesystem {
2729
writeFile: (path: string, data: string | Buffer) => Promise<void>;
2830
}
2931

30-
export interface Stats {
32+
interface IStats {
3133
dev: number;
3234
ino: number;
3335
mode: number;
@@ -48,6 +50,48 @@ export interface Stats {
4850
birthtime: Date;
4951
}
5052

53+
class Stats {
54+
dev: number;
55+
ino: number;
56+
mode: number;
57+
nlink: number;
58+
uid: number;
59+
gid: number;
60+
rdev: number;
61+
size: number;
62+
blksize: number;
63+
blocks: number;
64+
atimeMs: number;
65+
mtimeMs: number;
66+
ctimeMs: number;
67+
birthtimeMs: number;
68+
atime: Date;
69+
mtime: Date;
70+
ctime: Date;
71+
birthtime: Date;
72+
73+
constructor(private constants: { [key: string]: number }) {}
74+
75+
isSymbolicLink = () =>
76+
(this.mode & this.constants.S_IFMT) === this.constants.S_IFLNK;
77+
78+
isFile = () => (this.mode & this.constants.S_IFMT) === this.constants.S_IFREG;
79+
80+
isDirectory = () =>
81+
(this.mode & this.constants.S_IFMT) === this.constants.S_IFDIR;
82+
83+
isBlockDevice = () =>
84+
(this.mode & this.constants.S_IFMT) === this.constants.S_IFBLK;
85+
86+
isCharacterDevice = () =>
87+
(this.mode & this.constants.S_IFMT) === this.constants.S_IFCHR;
88+
89+
isFIFO = () => (this.mode & this.constants.S_IFMT) === this.constants.S_IFIFO;
90+
91+
isSocket = () =>
92+
(this.mode & this.constants.S_IFMT) === this.constants.S_IFSOCK;
93+
}
94+
5195
interface Options {
5296
service: string;
5397
client?: Client;
@@ -64,18 +108,24 @@ export async function fsServer({ service, fs, client }: Options) {
64108
async chmod(path: string, mode: string | number) {
65109
await (await fs(this.subject)).chmod(path, mode);
66110
},
111+
async constants(): Promise<{ [key: string]: number }> {
112+
return await (await fs(this.subject)).constants();
113+
},
67114
async copyFile(src: string, dest: string) {
68115
await (await fs(this.subject)).copyFile(src, dest);
69116
},
70117
async cp(src: string, dest: string, options?) {
71118
await (await fs(this.subject)).cp(src, dest, options);
72119
},
73120
async exists(path: string) {
74-
await (await fs(this.subject)).exists(path);
121+
return await (await fs(this.subject)).exists(path);
75122
},
76123
async link(existingPath: string, newPath: string) {
77124
await (await fs(this.subject)).link(existingPath, newPath);
78125
},
126+
async lstat(path: string): Promise<IStats> {
127+
return await (await fs(this.subject)).lstat(path);
128+
},
79129
async mkdir(path: string, options?) {
80130
await (await fs(this.subject)).mkdir(path, options);
81131
},
@@ -89,35 +139,35 @@ export async function fsServer({ service, fs, client }: Options) {
89139
return await (await fs(this.subject)).realpath(path);
90140
},
91141
async rename(oldPath: string, newPath: string) {
92-
return await (await fs(this.subject)).rename(oldPath, newPath);
142+
await (await fs(this.subject)).rename(oldPath, newPath);
93143
},
94144
async rm(path: string, options?) {
95-
return await (await fs(this.subject)).rm(path, options);
145+
await (await fs(this.subject)).rm(path, options);
96146
},
97147
async rmdir(path: string, options?) {
98-
return await (await fs(this.subject)).rmdir(path, options);
148+
await (await fs(this.subject)).rmdir(path, options);
99149
},
100-
async stat(path: string): Promise<Stats> {
150+
async stat(path: string): Promise<IStats> {
101151
return await (await fs(this.subject)).stat(path);
102152
},
103153
async symlink(target: string, path: string) {
104-
return await (await fs(this.subject)).symlink(target, path);
154+
await (await fs(this.subject)).symlink(target, path);
105155
},
106156
async truncate(path: string, len?: number) {
107-
return await (await fs(this.subject)).truncate(path, len);
157+
await (await fs(this.subject)).truncate(path, len);
108158
},
109159
async unlink(path: string) {
110-
return await (await fs(this.subject)).unlink(path);
160+
await (await fs(this.subject)).unlink(path);
111161
},
112162
async utimes(
113163
path: string,
114164
atime: number | string | Date,
115165
mtime: number | string | Date,
116166
) {
117-
return await (await fs(this.subject)).utimes(path, atime, mtime);
167+
await (await fs(this.subject)).utimes(path, atime, mtime);
118168
},
119169
async writeFile(path: string, data: string | Buffer) {
120-
return await (await fs(this.subject)).writeFile(path, data);
170+
await (await fs(this.subject)).writeFile(path, data);
121171
},
122172
},
123173
);
@@ -130,5 +180,30 @@ export function fsClient({
130180
client?: Client;
131181
subject: string;
132182
}) {
133-
return (client ?? conat()).call<Filesystem>(subject);
183+
let call = (client ?? conat()).call<Filesystem>(subject);
184+
185+
let constants: any = null;
186+
const stat0 = call.stat.bind(call);
187+
call.stat = async (path: string) => {
188+
const s = await stat0(path);
189+
constants = constants ?? (await call.constants());
190+
const stats = new Stats(constants);
191+
for (const k in s) {
192+
stats[k] = s[k];
193+
}
194+
return stats;
195+
};
196+
197+
const lstat0 = call.lstat.bind(call);
198+
call.lstat = async (path: string) => {
199+
const s = await lstat0(path);
200+
constants = constants ?? (await call.constants());
201+
const stats = new Stats(constants);
202+
for (const k in s) {
203+
stats[k] = s[k];
204+
}
205+
return stats;
206+
};
207+
208+
return call;
134209
}

src/packages/file-server/conat/test/local-path.test.ts

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,102 @@ import { mkdtemp, rm } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { join } from "path";
55
import { fsClient } from "@cocalc/conat/files/fs";
6+
import { randomId } from "@cocalc/conat/names";
67

78
let tempDir;
89
beforeAll(async () => {
910
tempDir = await mkdtemp(join(tmpdir(), "cocalc-local-path"));
1011
});
1112

1213
describe("use the simple fileserver", () => {
13-
let service;
14+
const service = `fs-${randomId()}`;
15+
let server;
1416
it("creates the simple fileserver service", async () => {
15-
service = await localPathFileserver({ service: "fs", path: tempDir });
17+
server = await localPathFileserver({ service, path: tempDir });
1618
});
1719

1820
const project_id = "6b851643-360e-435e-b87e-f9a6ab64a8b1";
1921
let fs;
2022
it("create a client", () => {
21-
fs = fsClient({ subject: `fs.project-${project_id}` });
23+
fs = fsClient({ subject: `${service}.project-${project_id}` });
2224
});
2325

24-
it("checks appendFile works", async () => {
26+
it("appendFile works", async () => {
27+
await fs.writeFile("a", "");
2528
await fs.appendFile("a", "foo");
2629
expect(await fs.readFile("a", "utf8")).toEqual("foo");
2730
});
2831

29-
it("checks chmod works", async () => {
32+
it("chmod works", async () => {
3033
await fs.writeFile("b", "hi");
3134
await fs.chmod("b", 0o755);
3235
const s = await fs.stat("b");
3336
expect(s.mode.toString(8)).toBe("100755");
3437
});
3538

39+
it("constants work", async () => {
40+
const constants = await fs.constants();
41+
expect(constants.O_RDONLY).toBe(0);
42+
expect(constants.O_WRONLY).toBe(1);
43+
expect(constants.O_RDWR).toBe(2);
44+
});
45+
46+
it("copyFile works", async () => {
47+
await fs.writeFile("c", "hello");
48+
await fs.copyFile("c", "d.txt");
49+
expect(await fs.readFile("d.txt", "utf8")).toEqual("hello");
50+
});
51+
52+
it("cp works on a directory", async () => {
53+
await fs.mkdir("folder");
54+
await fs.writeFile("folder/a.txt", "hello");
55+
await fs.cp("folder", "folder2", { recursive: true });
56+
expect(await fs.readFile("folder2/a.txt", "utf8")).toEqual("hello");
57+
});
58+
59+
it("exists works", async () => {
60+
expect(await fs.exists("does-not-exist")).toBe(false);
61+
await fs.writeFile("does-exist", "");
62+
expect(await fs.exists("does-exist")).toBe(true);
63+
});
64+
65+
it("creating a hard link works", async () => {
66+
await fs.writeFile("source", "the source");
67+
await fs.link("source", "target");
68+
expect(await fs.readFile("target", "utf8")).toEqual("the source");
69+
// hard link, not symlink
70+
expect(await fs.realpath("target")).toBe("target");
71+
72+
await fs.appendFile("source", " and more");
73+
expect(await fs.readFile("target", "utf8")).toEqual("the source and more");
74+
});
75+
76+
it("mkdir works", async () => {
77+
await fs.mkdir("xyz");
78+
const s = await fs.stat("xyz");
79+
expect(s.isDirectory()).toBe(true);
80+
});
81+
82+
it("creating a symlink works", async () => {
83+
await fs.writeFile("source1", "the source");
84+
await fs.symlink("source1", "target1");
85+
expect(await fs.readFile("target1", "utf8")).toEqual("the source");
86+
// symlink, not hard
87+
expect(await fs.realpath("target1")).toBe("source1");
88+
await fs.appendFile("source1", " and more");
89+
expect(await fs.readFile("target1", "utf8")).toEqual("the source and more");
90+
const stats = await fs.stat("target1");
91+
expect(stats.isSymbolicLink()).toBe(false);
92+
93+
const lstats = await fs.lstat("target1");
94+
expect(lstats.isSymbolicLink()).toBe(true);
95+
96+
const stats0 = await fs.stat("source1");
97+
expect(stats0.isSymbolicLink()).toBe(false);
98+
});
99+
36100
it("closes the service", () => {
37-
service.close();
101+
server.close();
38102
});
39103
});
40104

src/packages/file-server/fs/sandbox.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import {
1515
appendFile,
1616
chmod,
1717
cp,
18+
constants,
1819
copyFile,
1920
link,
21+
lstat,
2022
readdir,
2123
readFile,
2224
realpath,
@@ -56,6 +58,10 @@ export class SandboxedFilesystem {
5658
await chmod(this.safeAbsPath(path), mode);
5759
};
5860

61+
constants = async (): Promise<{ [key: string]: number }> => {
62+
return constants;
63+
};
64+
5965
copyFile = async (src: string, dest: string) => {
6066
await copyFile(this.safeAbsPath(src), this.safeAbsPath(dest));
6167
};
@@ -86,6 +92,10 @@ export class SandboxedFilesystem {
8692
});
8793
};
8894

95+
lstat = async (path: string) => {
96+
return await lstat(this.safeAbsPath(path));
97+
};
98+
8999
mkdir = async (path: string, options?) => {
90100
await mkdir(this.safeAbsPath(path), options);
91101
};

src/packages/frontend/conat/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
deleteRememberMe,
4747
setRememberMe,
4848
} from "@cocalc/frontend/misc/remember-me";
49+
import { fsClient } from "@cocalc/conat/files/fs";
4950

5051
export interface ConatConnectionStatus {
5152
state: "connected" | "disconnected";
@@ -515,6 +516,8 @@ export class ConatClient extends EventEmitter {
515516
};
516517

517518
refCacheInfo = () => refCacheInfo();
519+
520+
fsClient = (subject: string) => fsClient({ subject, client: this.conat() });
518521
}
519522

520523
function setDeleted({ project_id, path, deleted }) {

0 commit comments

Comments
 (0)