const access = require("./access");
import { callback2 } from "@cocalc/util/async-utils";
import * as message from "@cocalc/util/message";
import { one_result } from "@cocalc/database";
import { is_valid_uuid_string, to_json } from "@cocalc/util/misc";
import { ProjectControlFunction } from "@cocalc/server/projects/control";
import getLogger from "@cocalc/backend/logger";
import { delay } from "awaiting";
const log = getLogger("hub:copy-path");
type WhereQueries = ({ [query: string]: string } | string)[];
interface CopyOp {
copy_path_id: any;
time: any;
source_project_id: any;
source_path: any;
target_project_id: any;
target_path: any;
overwrite_newer: any;
delete_missing: any;
backup: any;
started: any;
finished: any;
scheduled: any;
error: any;
exclude: any;
}
function sanitize(
val: number | string,
deflt: number,
max: number,
name,
): number {
if (val != null) {
const o = typeof val == "string" ? parseInt(val) : val;
if (isNaN(o) || o < 0 || o > max) {
throw new Error(
`ILLEGAL VALUE ${name}='${val}' (must be in [0, ${max}])`,
);
}
return o;
} else {
return deflt;
}
}
function err2str(err: string | { message?: string }) {
if (typeof err === "string") {
return err;
} else if (err.message != null) {
return err.message;
} else {
return `ERROR: ${to_json(err)}`;
}
}
export const test_err2str = err2str;
function row_to_copy_op(copy_op): CopyOp {
return {
copy_path_id: copy_op.id,
time: copy_op.time,
source_project_id: copy_op.source_project_id,
source_path: copy_op.source_path,
target_project_id: copy_op.target_project_id,
target_path: copy_op.target_path,
overwrite_newer: copy_op.overwrite_newer,
delete_missing: copy_op.delete_missing,
backup: copy_op.backup,
started: copy_op.started,
finished: copy_op.finished,
scheduled: copy_op.scheduled,
error: copy_op.error,
exclude: copy_op.exclude,
};
}
export class CopyPath {
private client: any;
private dbg: (method: string) => (msg: string) => void;
private err: (method: string) => (msg: string) => void;
private throw: (msg: string) => void;
constructor(client) {
this.client = client;
this._init_errors();
this.copy = this.copy.bind(this);
this.status = this.status.bind(this);
this.delete = this.delete.bind(this);
this._status_query = this._status_query.bind(this);
this._status_single = this._status_single.bind(this);
this._get_status = this._get_status.bind(this);
this._read_access = this._read_access.bind(this);
this._write_access = this._write_access.bind(this);
}
private _init_errors(): void {
this.dbg = function (method: string): (msg: string) => void {
return this.client.dbg(`CopyPath::${method}`);
};
this.err = function (method: string): (msg: string) => void {
return (msg) => {
throw new Error(`CopyPath::${method}: ${msg}`);
};
};
this.throw = (msg: string) => {
throw new Error(msg);
};
}
async copy(mesg): Promise<void> {
log.debug(mesg);
this.client.touch();
try {
if (!is_valid_uuid_string(mesg.src_project_id)) {
this.throw(`src_project_id='${mesg.src_project_id}' not valid`);
}
if (!is_valid_uuid_string(mesg.target_project_id)) {
this.throw(`target_project_id='${mesg.target_project_id}' not valid`);
}
if (mesg.src_path == null) {
this.throw("src_path must be defined");
}
const write = this._write_access(mesg.target_project_id);
const read = this._read_access(mesg.src_project_id);
await Promise.all([write, read]);
const projectControl: ProjectControlFunction = this.client.projectControl;
const project = projectControl(mesg.src_project_id);
const copy_id = await project.copyPath({
path: mesg.src_path,
target_project_id: mesg.target_project_id,
target_path: mesg.target_path,
overwrite_newer: mesg.overwrite_newer,
delete_missing: mesg.delete_missing,
backup: mesg.backup,
timeout: mesg.timeout,
wait_until_done: mesg.wait_until_done ?? true,
scheduled: mesg.scheduled,
exclude: mesg.exclude,
});
if (mesg.debug_delay_s) {
log.debug(mesg.debug_delay_s, "second delay for debugging...");
await delay(mesg.debug_delay_s * 1000);
log.debug("done with delay for debugging...");
}
if (copy_id != null) {
const resp = message.copy_path_between_projects_response({
id: mesg.id,
copy_path_id: copy_id,
});
this.client.push_to_client(resp);
} else {
this.client.push_to_client(message.success({ id: mesg.id }));
}
} catch (err) {
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
}
}
async status(mesg): Promise<void> {
this.client.touch();
const search_many =
mesg.src_project_id != null || mesg.target_project_id != null;
if (!search_many && mesg.copy_path_id == null) {
this.client.error_to_client({
id: mesg.id,
error:
"'copy_path_id' (UUID) of a copy operation or 'src_project_id/target_project_id' must be defined",
});
return;
}
if (search_many) {
await this._status_query(mesg);
} else {
await this._status_single(mesg);
}
}
private async _status_query(mesg): Promise<void> {
const dbg = this.dbg("status_query");
const err = this.err("status_query");
try {
if (mesg.src_project_id == null && mesg.target_project_id == null) {
err(
`At least one of "src_project_id" or "target_project_id" must be given!`,
);
}
const where: WhereQueries = [];
if (mesg.src_project_id != null) {
await this._read_access(mesg.src_project_id);
where.push({ "source_project_id = $::UUID": mesg.src_project_id });
}
if (mesg.target_project_id != null) {
await this._write_access(mesg.target_project_id);
where.push({ "target_project_id = $::UUID": mesg.target_project_id });
}
if (mesg.src_path != null) {
where.push({ "source_path = $": mesg.src_path });
}
if (mesg.failed === true || mesg.failed === "true") {
where.push("error IS NOT NULL");
mesg.pending = false;
}
if (mesg.pending === true || mesg.pending === "true") {
where.push("finished IS NULL");
} else {
where.push("finished IS NOT NULL");
}
const offset = sanitize(mesg.offset, 0, 100 * 1000, "offset");
const limit = sanitize(mesg.limit, 1000, 1000, "limit");
dbg(`offset=${offset} limit=${limit}`);
const copy_ops: CopyOp[] = [];
const status_data = await callback2(this.client.database._query, {
query: "SELECT * FROM copy_paths",
where,
offset,
limit,
order_by: "time DESC",
});
if (status_data == null) {
this.throw(
"Can't find copy operations for given src_project_id/target_project_id",
);
}
for (const row of Array.from(status_data.rows)) {
copy_ops.push(row_to_copy_op(row));
}
this.client.push_to_client(
message.copy_path_status_response({
id: mesg.id,
data: copy_ops,
}),
);
} catch (err) {
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
}
}
private async _get_status(mesg): Promise<CopyOp | undefined> {
if (mesg.copy_path_id == null) {
this.throw("ERROR: copy_path_id missing");
}
const dbg = this.dbg("_get_status");
const where: WhereQueries = [{ "id = $::UUID": mesg.copy_path_id }];
if (mesg.not_yet_done) {
where.push("scheduled IS NOT NULL");
where.push("finished IS NULL");
}
const statuses = await callback2(this.client.database._query, {
query: "SELECT * FROM copy_paths",
where,
});
const copy_op: CopyOp = (() => {
let copy_op;
one_result((_, x) => {
if (x == null) {
if (mesg.not_yet_done) {
this.throw(
`Copy operation '${mesg.copy_path_id}' either does not exist or already finished`,
);
} else {
this.throw(
`Can't find copy operation with ID=${mesg.copy_path_id}`,
);
}
} else {
copy_op = x;
dbg(`copy_op=${to_json(copy_op)}`);
}
})(undefined, statuses);
return copy_op;
})();
if (copy_op == null) {
this.throw(`Can't find copy operation with ID=${mesg.copy_path_id}`);
return;
}
const write = this._write_access(copy_op.target_project_id);
const read = this._read_access(copy_op.source_project_id);
await Promise.all([write, read]);
return copy_op;
}
private async _status_single(mesg): Promise<void> {
try {
const copy_op = await this._get_status(mesg);
const data = row_to_copy_op(copy_op);
this.client.push_to_client(
message.copy_path_status_response({ id: mesg.id, data }),
);
} catch (err) {
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
}
}
async delete(mesg): Promise<void> {
this.client.touch();
const dbg = this.dbg("delete");
mesg.not_yet_done = true;
try {
const copy_op = await this._get_status(mesg);
if (copy_op == null) {
this.client.error_to_client({
id: mesg.id,
error: `copy op '${mesg.copy_path_id}' cannot be deleted.`,
});
} else {
await callback2(this.client.database._query, {
query: "DELETE FROM copy_paths",
where: { "id = $::UUID": mesg.copy_path_id },
});
this.client.push_to_client(
message.copy_path_status_response({
id: mesg.id,
data: `copy_path_id = '${mesg.copy_path_id}' deleted`,
}),
);
}
} catch (err) {
dbg(`status err=${err2str(err)}`);
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
}
}
private async _read_access(src_project_id): Promise<boolean> {
if (!is_valid_uuid_string(src_project_id)) {
this.throw(`invalid src_project_id=${src_project_id}`);
}
const read_ok = await callback2(access.user_has_read_access_to_project, {
project_id: src_project_id,
account_id: this.client.account_id,
account_groups: this.client.groups,
database: this.client.database,
});
if (!read_ok) {
this.throw(
`ACCESS BLOCKED -- No read access to source project -- ${src_project_id}`,
);
return false;
}
return true;
}
private async _write_access(target_project_id): Promise<boolean> {
if (!is_valid_uuid_string(target_project_id)) {
this.throw(`invalid target_project_id=${target_project_id}`);
}
const write_ok = await callback2(access.user_has_write_access_to_project, {
database: this.client.database,
project_id: target_project_id,
account_id: this.client.account_id,
account_groups: this.client.groups,
});
if (!write_ok) {
this.throw(
`ACCESS BLOCKED -- No write access to target project -- ${target_project_id}`,
);
return false;
}
return true;
}
}