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/hub/copy-path.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// Copy Operations Provider
7
// Used in the "Client"
8
9
const access = require("./access");
10
import { callback2 } from "@cocalc/util/async-utils";
11
import * as message from "@cocalc/util/message";
12
import { one_result } from "@cocalc/database";
13
import { is_valid_uuid_string, to_json } from "@cocalc/util/misc";
14
import { ProjectControlFunction } from "@cocalc/server/projects/control";
15
import getLogger from "@cocalc/backend/logger";
16
import { delay } from "awaiting";
17
18
const log = getLogger("hub:copy-path");
19
20
type WhereQueries = ({ [query: string]: string } | string)[];
21
22
interface CopyOp {
23
copy_path_id: any;
24
time: any;
25
source_project_id: any;
26
source_path: any;
27
target_project_id: any;
28
target_path: any;
29
overwrite_newer: any;
30
delete_missing: any;
31
backup: any;
32
started: any;
33
finished: any;
34
scheduled: any;
35
error: any;
36
exclude: any;
37
}
38
39
// this is specific to queries built here
40
function sanitize(
41
val: number | string,
42
deflt: number,
43
max: number,
44
name,
45
): number {
46
if (val != null) {
47
const o = typeof val == "string" ? parseInt(val) : val;
48
if (isNaN(o) || o < 0 || o > max) {
49
throw new Error(
50
`ILLEGAL VALUE ${name}='${val}' (must be in [0, ${max}])`,
51
);
52
}
53
return o;
54
} else {
55
return deflt;
56
}
57
}
58
59
// thrown errors are an object, but the response needs a string
60
function err2str(err: string | { message?: string }) {
61
if (typeof err === "string") {
62
return err;
63
} else if (err.message != null) {
64
return err.message;
65
} else {
66
return `ERROR: ${to_json(err)}`;
67
}
68
}
69
export const test_err2str = err2str;
70
71
// transforms copy_op data from the database to the specific object we want to return
72
function row_to_copy_op(copy_op): CopyOp {
73
return {
74
copy_path_id: copy_op.id,
75
time: copy_op.time,
76
source_project_id: copy_op.source_project_id,
77
source_path: copy_op.source_path,
78
target_project_id: copy_op.target_project_id,
79
target_path: copy_op.target_path,
80
overwrite_newer: copy_op.overwrite_newer,
81
delete_missing: copy_op.delete_missing,
82
backup: copy_op.backup,
83
started: copy_op.started,
84
finished: copy_op.finished,
85
scheduled: copy_op.scheduled,
86
error: copy_op.error,
87
exclude: copy_op.exclude,
88
};
89
}
90
91
export class CopyPath {
92
private client: any;
93
private dbg: (method: string) => (msg: string) => void;
94
private err: (method: string) => (msg: string) => void;
95
private throw: (msg: string) => void;
96
97
constructor(client) {
98
this.client = client;
99
this._init_errors();
100
this.copy = this.copy.bind(this);
101
this.status = this.status.bind(this);
102
this.delete = this.delete.bind(this);
103
this._status_query = this._status_query.bind(this);
104
this._status_single = this._status_single.bind(this);
105
this._get_status = this._get_status.bind(this);
106
this._read_access = this._read_access.bind(this);
107
this._write_access = this._write_access.bind(this);
108
}
109
110
private _init_errors(): void {
111
// client.dbg returns a function
112
this.dbg = function (method: string): (msg: string) => void {
113
return this.client.dbg(`CopyPath::${method}`);
114
};
115
this.err = function (method: string): (msg: string) => void {
116
return (msg) => {
117
throw new Error(`CopyPath::${method}: ${msg}`);
118
};
119
};
120
this.throw = (msg: string) => {
121
throw new Error(msg);
122
};
123
}
124
125
async copy(mesg): Promise<void> {
126
log.debug(mesg);
127
this.client.touch();
128
129
try {
130
// prereq checks
131
if (!is_valid_uuid_string(mesg.src_project_id)) {
132
this.throw(`src_project_id='${mesg.src_project_id}' not valid`);
133
}
134
if (!is_valid_uuid_string(mesg.target_project_id)) {
135
this.throw(`target_project_id='${mesg.target_project_id}' not valid`);
136
}
137
if (mesg.src_path == null) {
138
this.throw("src_path must be defined");
139
}
140
141
// check read/write access
142
const write = this._write_access(mesg.target_project_id);
143
const read = this._read_access(mesg.src_project_id);
144
await Promise.all([write, read]);
145
146
// get the "project" for issuing commands
147
const projectControl: ProjectControlFunction = this.client.projectControl;
148
const project = projectControl(mesg.src_project_id);
149
150
// do the copy
151
const copy_id = await project.copyPath({
152
path: mesg.src_path,
153
target_project_id: mesg.target_project_id,
154
target_path: mesg.target_path,
155
overwrite_newer: mesg.overwrite_newer,
156
delete_missing: mesg.delete_missing,
157
backup: mesg.backup,
158
timeout: mesg.timeout,
159
wait_until_done: mesg.wait_until_done ?? true, // default to true or we do not see the error
160
scheduled: mesg.scheduled,
161
exclude: mesg.exclude,
162
});
163
164
// for debugging
165
if (mesg.debug_delay_s) {
166
log.debug(mesg.debug_delay_s, "second delay for debugging...");
167
await delay(mesg.debug_delay_s * 1000);
168
log.debug("done with delay for debugging...");
169
}
170
171
// if we're still here, the copy was ok!
172
if (copy_id != null) {
173
// we only expect a copy_id in kucalc mode
174
const resp = message.copy_path_between_projects_response({
175
id: mesg.id,
176
copy_path_id: copy_id,
177
});
178
this.client.push_to_client(resp);
179
} else {
180
this.client.push_to_client(message.success({ id: mesg.id }));
181
}
182
} catch (err) {
183
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
184
}
185
}
186
187
async status(mesg): Promise<void> {
188
this.client.touch();
189
//const dbg = this.dbg("status");
190
// src_project_id, target_project_id and optionally src_path + offset (limit is 1000)
191
const search_many =
192
mesg.src_project_id != null || mesg.target_project_id != null;
193
if (!search_many && mesg.copy_path_id == null) {
194
this.client.error_to_client({
195
id: mesg.id,
196
error:
197
"'copy_path_id' (UUID) of a copy operation or 'src_project_id/target_project_id' must be defined",
198
});
199
return;
200
}
201
if (search_many) {
202
await this._status_query(mesg);
203
} else {
204
await this._status_single(mesg);
205
}
206
}
207
208
private async _status_query(mesg): Promise<void> {
209
const dbg = this.dbg("status_query");
210
const err = this.err("status_query");
211
212
try {
213
// prereq checks -- at least src or target must be set
214
if (mesg.src_project_id == null && mesg.target_project_id == null) {
215
// serious error: this should never happen, actually
216
err(
217
`At least one of "src_project_id" or "target_project_id" must be given!`,
218
);
219
}
220
221
// constructing the query
222
const where: WhereQueries = [];
223
224
if (mesg.src_project_id != null) {
225
await this._read_access(mesg.src_project_id);
226
where.push({ "source_project_id = $::UUID": mesg.src_project_id });
227
}
228
if (mesg.target_project_id != null) {
229
await this._write_access(mesg.target_project_id);
230
where.push({ "target_project_id = $::UUID": mesg.target_project_id });
231
}
232
233
if (mesg.src_path != null) {
234
where.push({ "source_path = $": mesg.src_path });
235
}
236
237
// all failed ones are implicitly also finished
238
if (mesg.failed === true || mesg.failed === "true") {
239
where.push("error IS NOT NULL");
240
mesg.pending = false;
241
}
242
243
if (mesg.pending === true || mesg.pending === "true") {
244
where.push("finished IS NULL");
245
} else {
246
where.push("finished IS NOT NULL");
247
}
248
249
// … and also sanitizing input!
250
const offset = sanitize(mesg.offset, 0, 100 * 1000, "offset");
251
const limit = sanitize(mesg.limit, 1000, 1000, "limit");
252
dbg(`offset=${offset} limit=${limit}`);
253
254
// essentially, we want to fill up and return this array
255
const copy_ops: CopyOp[] = [];
256
257
const status_data = await callback2(this.client.database._query, {
258
query: "SELECT * FROM copy_paths",
259
where,
260
offset,
261
limit,
262
order_by: "time DESC", // most recent first
263
});
264
265
if (status_data == null) {
266
this.throw(
267
"Can't find copy operations for given src_project_id/target_project_id",
268
);
269
}
270
for (const row of Array.from(status_data.rows)) {
271
// be explicit about what we return
272
copy_ops.push(row_to_copy_op(row));
273
}
274
275
// we're good
276
this.client.push_to_client(
277
message.copy_path_status_response({
278
id: mesg.id,
279
data: copy_ops,
280
}),
281
);
282
} catch (err) {
283
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
284
}
285
}
286
287
private async _get_status(mesg): Promise<CopyOp | undefined> {
288
if (mesg.copy_path_id == null) {
289
this.throw("ERROR: copy_path_id missing");
290
}
291
292
const dbg = this.dbg("_get_status");
293
294
const where: WhereQueries = [{ "id = $::UUID": mesg.copy_path_id }];
295
// not_yet_done is set internally for deleting a scheduled copy op
296
if (mesg.not_yet_done) {
297
where.push("scheduled IS NOT NULL");
298
where.push("finished IS NULL");
299
}
300
301
// get the status info
302
const statuses = await callback2(this.client.database._query, {
303
query: "SELECT * FROM copy_paths",
304
where,
305
});
306
307
const copy_op: CopyOp = (() => {
308
let copy_op;
309
one_result((_, x) => {
310
if (x == null) {
311
if (mesg.not_yet_done) {
312
this.throw(
313
`Copy operation '${mesg.copy_path_id}' either does not exist or already finished`,
314
);
315
} else {
316
this.throw(
317
`Can't find copy operation with ID=${mesg.copy_path_id}`,
318
);
319
}
320
} else {
321
copy_op = x;
322
dbg(`copy_op=${to_json(copy_op)}`);
323
}
324
})(undefined, statuses);
325
return copy_op;
326
})();
327
328
if (copy_op == null) {
329
this.throw(`Can't find copy operation with ID=${mesg.copy_path_id}`);
330
return;
331
}
332
333
// check read/write access
334
const write = this._write_access(copy_op.target_project_id);
335
const read = this._read_access(copy_op.source_project_id);
336
await Promise.all([write, read]);
337
338
return copy_op;
339
}
340
341
private async _status_single(mesg): Promise<void> {
342
try {
343
const copy_op = await this._get_status(mesg);
344
// be explicit about what we return
345
const data = row_to_copy_op(copy_op);
346
this.client.push_to_client(
347
message.copy_path_status_response({ id: mesg.id, data }),
348
);
349
} catch (err) {
350
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
351
}
352
}
353
354
async delete(mesg): Promise<void> {
355
this.client.touch();
356
const dbg = this.dbg("delete");
357
// this filters possible results
358
mesg.not_yet_done = true;
359
try {
360
const copy_op = await this._get_status(mesg);
361
362
if (copy_op == null) {
363
this.client.error_to_client({
364
id: mesg.id,
365
error: `copy op '${mesg.copy_path_id}' cannot be deleted.`,
366
});
367
} else {
368
await callback2(this.client.database._query, {
369
query: "DELETE FROM copy_paths",
370
where: { "id = $::UUID": mesg.copy_path_id },
371
});
372
// no error
373
this.client.push_to_client(
374
message.copy_path_status_response({
375
id: mesg.id,
376
data: `copy_path_id = '${mesg.copy_path_id}' deleted`,
377
}),
378
);
379
}
380
} catch (err) {
381
dbg(`status err=${err2str(err)}`);
382
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
383
}
384
}
385
386
private async _read_access(src_project_id): Promise<boolean> {
387
if (!is_valid_uuid_string(src_project_id)) {
388
this.throw(`invalid src_project_id=${src_project_id}`);
389
}
390
391
const read_ok = await callback2(access.user_has_read_access_to_project, {
392
project_id: src_project_id,
393
account_id: this.client.account_id,
394
account_groups: this.client.groups,
395
database: this.client.database,
396
});
397
// this.dbg("_read_access")(read_ok);
398
if (!read_ok) {
399
this.throw(
400
`ACCESS BLOCKED -- No read access to source project -- ${src_project_id}`,
401
);
402
return false;
403
}
404
return true;
405
}
406
407
private async _write_access(target_project_id): Promise<boolean> {
408
if (!is_valid_uuid_string(target_project_id)) {
409
this.throw(`invalid target_project_id=${target_project_id}`);
410
}
411
412
const write_ok = await callback2(access.user_has_write_access_to_project, {
413
database: this.client.database,
414
project_id: target_project_id,
415
account_id: this.client.account_id,
416
account_groups: this.client.groups,
417
});
418
// this.dbg("_write_access")(write_ok);
419
if (!write_ok) {
420
this.throw(
421
`ACCESS BLOCKED -- No write access to target project -- ${target_project_id}`,
422
);
423
return false;
424
}
425
return true;
426
}
427
}
428
429