CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/sync-client/lib/client-fs.ts
Views: 687
1
import type { ClientFs as ClientFsType } from "@cocalc/sync/client/types";
2
import Client, { Role } from "./index";
3
import ensureContainingDirectoryExists from "@cocalc/backend/misc/ensure-containing-directory-exists";
4
import { join } from "node:path";
5
import { readFile, writeFile, stat as statFileAsync } from "node:fs/promises";
6
import { exists, stat } from "fs";
7
import fs from "node:fs";
8
import type { CB } from "@cocalc/util/types/callback";
9
import { Watcher } from "@cocalc/backend/watcher";
10
11
import getLogger from "@cocalc/backend/logger";
12
13
const logger = getLogger("sync-client:client-fs");
14
15
export class ClientFs extends Client implements ClientFsType {
16
private filesystemClient = new FileSystemClient();
17
18
write_file = this.filesystemClient.write_file;
19
path_read = this.filesystemClient.path_read;
20
path_stat = this.filesystemClient.path_stat;
21
path_exists = this.filesystemClient.path_exists;
22
file_size_async = this.filesystemClient.file_size_async;
23
file_stat_async = this.filesystemClient.file_stat_async;
24
watch_file = this.filesystemClient.watch_file;
25
path_access = this.filesystemClient.path_access;
26
27
constructor({
28
project_id,
29
client_id,
30
home,
31
role,
32
}: {
33
project_id: string;
34
client_id?: string;
35
home?: string;
36
role: Role;
37
}) {
38
super({ project_id, client_id, role });
39
this.filesystemClient.setHome(home ?? process.env.HOME ?? "/home/user");
40
}
41
}
42
43
// Some functions for reading and writing files under node.js
44
// where the read and write is aware of other reading and writing,
45
// motivated by the needs of realtime sync.
46
export class FileSystemClient {
47
private _file_io_lock?: { [key: string]: number }; // file → timestamps
48
private home: string;
49
50
constructor() {
51
this.home = process.env.HOME ?? "/home/user";
52
}
53
54
setHome(home: string) {
55
this.home = home;
56
}
57
58
// Write a file to a given path (relative to this.home) on disk; will create containing directory.
59
// If file is currently being written or read in this process, will result in error (instead of silently corrupt data).
60
// WARNING: See big comment below for path_read.
61
write_file = async (opts: {
62
path: string;
63
data: string;
64
cb: CB<void>;
65
}): Promise<void> => {
66
// WARNING: despite being async, this returns nothing!
67
const path = join(this.home, opts.path);
68
if (this._file_io_lock == null) {
69
this._file_io_lock = {};
70
}
71
logger.debug("write_file", path);
72
const now = Date.now();
73
if (now - (this._file_io_lock[path] ?? 0) < 15000) {
74
// lock automatically expires after 15 seconds (see https://github.com/sagemathinc/cocalc/issues/1147)
75
logger.debug("write_file", path, "LOCK");
76
// Try again in about 1s.
77
setTimeout(() => this.write_file(opts), 500 + 500 * Math.random());
78
return;
79
}
80
logger.debug("write_file", "file_io_lock", this._file_io_lock);
81
try {
82
this._file_io_lock[path] = now;
83
logger.debug(path, "write_file -- ensureContainingDirectoryExists");
84
await ensureContainingDirectoryExists(path);
85
logger.debug(path, "write_file -- actually writing it to disk");
86
await writeFile(path, opts.data);
87
logger.debug("write_file", "success");
88
opts.cb();
89
} catch (error) {
90
const err = error;
91
logger.debug("write_file", "error", err);
92
opts.cb(err);
93
} finally {
94
if (this._file_io_lock != null) {
95
delete this._file_io_lock[path];
96
}
97
}
98
};
99
100
// Read file as a string from disk.
101
// If file is currently being written or read in this process,
102
// will retry until it isn't, so we do not get an error and we
103
// do NOT get silently corrupted data.
104
// TODO and HUGE AWARNING: Despite this function being async, it DOES NOT
105
// RETURN ANYTHING AND DOES NOT THROW EXCEPTIONS! Just use it like any
106
// other old cb function. Todo: rewrite this and anything that uses it.
107
// This is just a halfway step toward rewriting project away from callbacks and coffeescript.
108
path_read = async (opts: {
109
path: string;
110
maxsize_MB?: number; // in megabytes; if given and file would be larger than this, then cb(err)
111
cb: CB<string>; // cb(err, file content as string (not Buffer!))
112
}): Promise<void> => {
113
// WARNING: despite being async, this returns nothing!
114
let content: string | undefined = undefined;
115
const path = join(this.home, opts.path);
116
logger.debug(`path_read(path='${path}', maxsize_MB=${opts.maxsize_MB})`);
117
if (this._file_io_lock == null) {
118
this._file_io_lock = {};
119
}
120
121
const now = Date.now();
122
if (now - (this._file_io_lock[path] ?? 0) < 15000) {
123
// lock expires after 15 seconds (see https://github.com/sagemathinc/cocalc/issues/1147)
124
logger.debug(`path_read(path='${path}')`, "LOCK");
125
// Try again in 1s.
126
setTimeout(
127
async () => await this.path_read(opts),
128
500 + 500 * Math.random(),
129
);
130
return;
131
}
132
try {
133
this._file_io_lock[path] = now;
134
135
logger.debug(
136
`path_read(path='${path}')`,
137
"_file_io_lock",
138
this._file_io_lock,
139
);
140
141
// checking filesize limitations
142
if (opts.maxsize_MB != null) {
143
logger.debug(`path_read(path='${path}')`, "check if file too big");
144
let size: number | undefined = undefined;
145
try {
146
size = await this.file_size_async(path);
147
} catch (err) {
148
logger.debug("error checking", err);
149
opts.cb(err);
150
return;
151
}
152
153
if (size > opts.maxsize_MB * 1000000) {
154
logger.debug(path, "file is too big!");
155
opts.cb(
156
new Error(
157
`file '${path}' size (=${
158
size / 1000000
159
}MB) too large (must be at most ${
160
opts.maxsize_MB
161
}MB); try opening it in a Terminal with vim instead or click Help in the upper right to open a support request`,
162
),
163
);
164
return;
165
} else {
166
logger.debug(path, "file is fine");
167
}
168
}
169
170
// if the above passes, actually reading file
171
172
try {
173
const data = await readFile(path);
174
logger.debug(path, "read file");
175
content = data.toString();
176
} catch (err) {
177
logger.debug(path, "error reading file", err);
178
opts.cb(err);
179
return;
180
}
181
} finally {
182
// release lock
183
if (this._file_io_lock) {
184
delete this._file_io_lock[path];
185
}
186
}
187
188
opts.cb(undefined, content);
189
};
190
191
file_size_async = async (filename: string) => {
192
const stat = await this.file_stat_async(filename);
193
return stat.size;
194
};
195
196
file_stat_async = async (filename: string) => {
197
return await statFileAsync(filename);
198
};
199
200
path_stat = (opts: { path: string; cb: CB }) => {
201
// see https://nodejs.org/api/fs.html#fs_class_fs_stats
202
const path = join(this.home, opts.path);
203
stat(path, opts.cb);
204
};
205
206
path_exists = (opts: { path: string; cb: CB }) => {
207
const path = join(this.home, opts.path);
208
exists(path, (exists) => {
209
opts.cb(undefined, exists);
210
});
211
};
212
213
watch_file = ({
214
path: relPath,
215
// don't fire until at least this many ms after the file has REMAINED UNCHANGED
216
debounce,
217
}: {
218
path: string;
219
debounce?: number;
220
}): Watcher => {
221
const path = join(this.home, relPath);
222
logger.debug("watching file", { path, debounce });
223
return new Watcher(path, { debounce });
224
};
225
226
is_deleted = (_path: string, _project_id: string) => {
227
// not implemented yet in general
228
return undefined;
229
};
230
231
set_deleted = (_path: string, _project_id?: string) => {
232
// TODO: this should edit the listings
233
};
234
235
path_access = (opts: { path: string; mode: string; cb: CB }) => {
236
// mode: sub-sequence of 'rwxf' -- see https://nodejs.org/api/fs.html#fs_class_fs_stats
237
// cb(err); err = if any access fails; err=undefined if all access is OK
238
const path = join(this.home, opts.path);
239
let access = 0;
240
for (let s of opts.mode) {
241
access |= fs[s.toUpperCase() + "_OK"];
242
}
243
fs.access(path, access, opts.cb);
244
};
245
}
246
247