@cocalc/hub
Version:
CoCalc: Backend webserver component
363 lines • 14.5 kB
JavaScript
;
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CopyPath = exports.test_err2str = void 0;
// Copy Operations Provider
// Used in the "Client"
const access = require("./access");
const async_utils_1 = require("@cocalc/util/async-utils");
const message = __importStar(require("@cocalc/util/message"));
const database_1 = require("@cocalc/database");
const misc_1 = require("@cocalc/util/misc");
// this is specific to queries built here
function sanitize(val, deflt, max, name) {
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;
}
}
// thrown errors are an object, but the response needs a string
function err2str(err) {
if (typeof err === "string") {
return err;
}
else if (err.message != null) {
return err.message;
}
else {
return `ERROR: ${(0, misc_1.to_json)(err)}`;
}
}
exports.test_err2str = err2str;
// transforms copy_op data from the database to the specific object we want to return
function row_to_copy_op(copy_op) {
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,
};
}
class CopyPath {
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);
}
_init_errors() {
// client.dbg returns a function
this.dbg = function (method) {
return this.client.dbg(`CopyPath::${method}`);
};
this.err = function (method) {
return (msg) => {
throw new Error(`CopyPath::${method}: ${msg}`);
};
};
this.throw = (msg) => {
throw new Error(msg);
};
}
async copy(mesg) {
this.client.touch();
try {
// prereq checks
if (!(0, misc_1.is_valid_uuid_string)(mesg.src_project_id)) {
this.throw(`src_project_id='${mesg.src_project_id}' not valid`);
}
if (!(0, misc_1.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");
}
// check read/write access
const write = this._write_access(mesg.target_project_id);
const read = this._read_access(mesg.src_project_id);
await Promise.all([write, read]);
// get the "project" for issuing commands
const projectControl = this.client.compute_server;
const project = projectControl(mesg.src_project_id);
// do the copy
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 we're still here, the copy was ok!
if (copy_id != null) {
// we only expect a copy_id in kucalc mode
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) {
this.client.touch();
//const dbg = this.dbg("status");
// src_project_id, target_project_id and optionally src_path + offset (limit is 1000)
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);
}
}
async _status_query(mesg) {
const dbg = this.dbg("status_query");
const err = this.err("status_query");
try {
// prereq checks -- at least src or target must be set
if (mesg.src_project_id == null && mesg.target_project_id == null) {
// serious error: this should never happen, actually
err(`At least one of "src_project_id" or "target_project_id" must be given!`);
}
// constructing the query
const where = [];
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 });
}
// all failed ones are implicitly also finished
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");
}
// … and also sanitizing input!
const offset = sanitize(mesg.offset, 0, 100 * 1000, "offset");
const limit = sanitize(mesg.limit, 1000, 1000, "limit");
dbg(`offset=${offset} limit=${limit}`);
// essentially, we want to fill up and return this array
const copy_ops = [];
const status_data = await (0, async_utils_1.callback2)(this.client.database._query, {
query: "SELECT * FROM copy_paths",
where,
offset,
limit,
order_by: "time DESC", // most recent first
});
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)) {
// be explicit about what we return
copy_ops.push(row_to_copy_op(row));
}
// we're good
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) });
}
}
async _get_status(mesg) {
if (mesg.copy_path_id == null) {
this.throw("ERROR: copy_path_id missing");
}
const dbg = this.dbg("_get_status");
const where = [{ "id = $::UUID": mesg.copy_path_id }];
// not_yet_done is set internally for deleting a scheduled copy op
if (mesg.not_yet_done) {
where.push("scheduled IS NOT NULL");
where.push("finished IS NULL");
}
// get the status info
const statuses = await (0, async_utils_1.callback2)(this.client.database._query, {
query: "SELECT * FROM copy_paths",
where,
});
const copy_op = (() => {
let copy_op;
(0, database_1.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=${(0, misc_1.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;
}
// check read/write access
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;
}
async _status_single(mesg) {
try {
const copy_op = await this._get_status(mesg);
// be explicit about what we return
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) {
this.client.touch();
const dbg = this.dbg("delete");
// this filters possible results
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 (0, async_utils_1.callback2)(this.client.database._query, {
query: "DELETE FROM copy_paths",
where: { "id = $::UUID": mesg.copy_path_id },
});
// no error
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(`stauts err=${err2str(err)}`);
this.client.error_to_client({ id: mesg.id, error: err2str(err) });
}
}
async _read_access(src_project_id) {
if (!(0, misc_1.is_valid_uuid_string)(src_project_id)) {
this.throw(`invalid src_project_id=${src_project_id}`);
}
const read_ok = await (0, async_utils_1.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,
});
// this.dbg("_read_access")(read_ok);
if (!read_ok) {
this.throw(`ACCESS BLOCKED -- No read access to source project -- ${src_project_id}`);
return false;
}
return true;
}
async _write_access(target_project_id) {
if (!(0, misc_1.is_valid_uuid_string)(target_project_id)) {
this.throw(`invalid target_project_id=${target_project_id}`);
}
const write_ok = await (0, async_utils_1.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,
});
// this.dbg("_write_access")(write_ok);
if (!write_ok) {
this.throw(`ACCESS BLOCKED -- No write access to target project -- ${target_project_id}`);
return false;
}
return true;
}
}
exports.CopyPath = CopyPath;
//# sourceMappingURL=copy-path.js.map