UNPKG

@cocalc/hub

Version:
363 lines 14.5 kB
"use strict"; /* * 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