Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/hub/copy-path.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// Copy Operations Provider6// Used in the "Client"78const access = require("./access");9import { callback2 } from "@cocalc/util/async-utils";10import * as message from "@cocalc/util/message";11import { one_result } from "@cocalc/database";12import { is_valid_uuid_string, to_json } from "@cocalc/util/misc";13import { ProjectControlFunction } from "@cocalc/server/projects/control";14import getLogger from "@cocalc/backend/logger";15import { delay } from "awaiting";1617const log = getLogger("hub:copy-path");1819type WhereQueries = ({ [query: string]: string } | string)[];2021interface CopyOp {22copy_path_id: any;23time: any;24source_project_id: any;25source_path: any;26target_project_id: any;27target_path: any;28overwrite_newer: any;29delete_missing: any;30backup: any;31started: any;32finished: any;33scheduled: any;34error: any;35exclude: any;36}3738// this is specific to queries built here39function sanitize(40val: number | string,41deflt: number,42max: number,43name,44): number {45if (val != null) {46const o = typeof val == "string" ? parseInt(val) : val;47if (isNaN(o) || o < 0 || o > max) {48throw new Error(49`ILLEGAL VALUE ${name}='${val}' (must be in [0, ${max}])`,50);51}52return o;53} else {54return deflt;55}56}5758// thrown errors are an object, but the response needs a string59function err2str(err: string | { message?: string }) {60if (typeof err === "string") {61return err;62} else if (err.message != null) {63return err.message;64} else {65return `ERROR: ${to_json(err)}`;66}67}68export const test_err2str = err2str;6970// transforms copy_op data from the database to the specific object we want to return71function row_to_copy_op(copy_op): CopyOp {72return {73copy_path_id: copy_op.id,74time: copy_op.time,75source_project_id: copy_op.source_project_id,76source_path: copy_op.source_path,77target_project_id: copy_op.target_project_id,78target_path: copy_op.target_path,79overwrite_newer: copy_op.overwrite_newer,80delete_missing: copy_op.delete_missing,81backup: copy_op.backup,82started: copy_op.started,83finished: copy_op.finished,84scheduled: copy_op.scheduled,85error: copy_op.error,86exclude: copy_op.exclude,87};88}8990export class CopyPath {91private client: any;92private dbg: (method: string) => (msg: string) => void;93private err: (method: string) => (msg: string) => void;94private throw: (msg: string) => void;9596constructor(client) {97this.client = client;98this._init_errors();99this.copy = this.copy.bind(this);100this.status = this.status.bind(this);101this.delete = this.delete.bind(this);102this._status_query = this._status_query.bind(this);103this._status_single = this._status_single.bind(this);104this._get_status = this._get_status.bind(this);105this._read_access = this._read_access.bind(this);106this._write_access = this._write_access.bind(this);107}108109private _init_errors(): void {110// client.dbg returns a function111this.dbg = function (method: string): (msg: string) => void {112return this.client.dbg(`CopyPath::${method}`);113};114this.err = function (method: string): (msg: string) => void {115return (msg) => {116throw new Error(`CopyPath::${method}: ${msg}`);117};118};119this.throw = (msg: string) => {120throw new Error(msg);121};122}123124async copy(mesg): Promise<void> {125log.debug(mesg);126this.client.touch();127128try {129// prereq checks130if (!is_valid_uuid_string(mesg.src_project_id)) {131this.throw(`src_project_id='${mesg.src_project_id}' not valid`);132}133if (!is_valid_uuid_string(mesg.target_project_id)) {134this.throw(`target_project_id='${mesg.target_project_id}' not valid`);135}136if (mesg.src_path == null) {137this.throw("src_path must be defined");138}139140// check read/write access141const write = this._write_access(mesg.target_project_id);142const read = this._read_access(mesg.src_project_id);143await Promise.all([write, read]);144145// get the "project" for issuing commands146const projectControl: ProjectControlFunction = this.client.projectControl;147const project = projectControl(mesg.src_project_id);148149// do the copy150const copy_id = await project.copyPath({151path: mesg.src_path,152target_project_id: mesg.target_project_id,153target_path: mesg.target_path,154overwrite_newer: mesg.overwrite_newer,155delete_missing: mesg.delete_missing,156backup: mesg.backup,157timeout: mesg.timeout,158wait_until_done: mesg.wait_until_done ?? true, // default to true or we do not see the error159scheduled: mesg.scheduled,160exclude: mesg.exclude,161});162163// for debugging164if (mesg.debug_delay_s) {165log.debug(mesg.debug_delay_s, "second delay for debugging...");166await delay(mesg.debug_delay_s * 1000);167log.debug("done with delay for debugging...");168}169170// if we're still here, the copy was ok!171if (copy_id != null) {172// we only expect a copy_id in kucalc mode173const resp = message.copy_path_between_projects_response({174id: mesg.id,175copy_path_id: copy_id,176});177this.client.push_to_client(resp);178} else {179this.client.push_to_client(message.success({ id: mesg.id }));180}181} catch (err) {182this.client.error_to_client({ id: mesg.id, error: err2str(err) });183}184}185186async status(mesg): Promise<void> {187this.client.touch();188//const dbg = this.dbg("status");189// src_project_id, target_project_id and optionally src_path + offset (limit is 1000)190const search_many =191mesg.src_project_id != null || mesg.target_project_id != null;192if (!search_many && mesg.copy_path_id == null) {193this.client.error_to_client({194id: mesg.id,195error:196"'copy_path_id' (UUID) of a copy operation or 'src_project_id/target_project_id' must be defined",197});198return;199}200if (search_many) {201await this._status_query(mesg);202} else {203await this._status_single(mesg);204}205}206207private async _status_query(mesg): Promise<void> {208const dbg = this.dbg("status_query");209const err = this.err("status_query");210211try {212// prereq checks -- at least src or target must be set213if (mesg.src_project_id == null && mesg.target_project_id == null) {214// serious error: this should never happen, actually215err(216`At least one of "src_project_id" or "target_project_id" must be given!`,217);218}219220// constructing the query221const where: WhereQueries = [];222223if (mesg.src_project_id != null) {224await this._read_access(mesg.src_project_id);225where.push({ "source_project_id = $::UUID": mesg.src_project_id });226}227if (mesg.target_project_id != null) {228await this._write_access(mesg.target_project_id);229where.push({ "target_project_id = $::UUID": mesg.target_project_id });230}231232if (mesg.src_path != null) {233where.push({ "source_path = $": mesg.src_path });234}235236// all failed ones are implicitly also finished237if (mesg.failed === true || mesg.failed === "true") {238where.push("error IS NOT NULL");239mesg.pending = false;240}241242if (mesg.pending === true || mesg.pending === "true") {243where.push("finished IS NULL");244} else {245where.push("finished IS NOT NULL");246}247248// … and also sanitizing input!249const offset = sanitize(mesg.offset, 0, 100 * 1000, "offset");250const limit = sanitize(mesg.limit, 1000, 1000, "limit");251dbg(`offset=${offset} limit=${limit}`);252253// essentially, we want to fill up and return this array254const copy_ops: CopyOp[] = [];255256const status_data = await callback2(this.client.database._query, {257query: "SELECT * FROM copy_paths",258where,259offset,260limit,261order_by: "time DESC", // most recent first262});263264if (status_data == null) {265this.throw(266"Can't find copy operations for given src_project_id/target_project_id",267);268}269for (const row of Array.from(status_data.rows)) {270// be explicit about what we return271copy_ops.push(row_to_copy_op(row));272}273274// we're good275this.client.push_to_client(276message.copy_path_status_response({277id: mesg.id,278data: copy_ops,279}),280);281} catch (err) {282this.client.error_to_client({ id: mesg.id, error: err2str(err) });283}284}285286private async _get_status(mesg): Promise<CopyOp | undefined> {287if (mesg.copy_path_id == null) {288this.throw("ERROR: copy_path_id missing");289}290291const dbg = this.dbg("_get_status");292293const where: WhereQueries = [{ "id = $::UUID": mesg.copy_path_id }];294// not_yet_done is set internally for deleting a scheduled copy op295if (mesg.not_yet_done) {296where.push("scheduled IS NOT NULL");297where.push("finished IS NULL");298}299300// get the status info301const statuses = await callback2(this.client.database._query, {302query: "SELECT * FROM copy_paths",303where,304});305306const copy_op: CopyOp = (() => {307let copy_op;308one_result((_, x) => {309if (x == null) {310if (mesg.not_yet_done) {311this.throw(312`Copy operation '${mesg.copy_path_id}' either does not exist or already finished`,313);314} else {315this.throw(316`Can't find copy operation with ID=${mesg.copy_path_id}`,317);318}319} else {320copy_op = x;321dbg(`copy_op=${to_json(copy_op)}`);322}323})(undefined, statuses);324return copy_op;325})();326327if (copy_op == null) {328this.throw(`Can't find copy operation with ID=${mesg.copy_path_id}`);329return;330}331332// check read/write access333const write = this._write_access(copy_op.target_project_id);334const read = this._read_access(copy_op.source_project_id);335await Promise.all([write, read]);336337return copy_op;338}339340private async _status_single(mesg): Promise<void> {341try {342const copy_op = await this._get_status(mesg);343// be explicit about what we return344const data = row_to_copy_op(copy_op);345this.client.push_to_client(346message.copy_path_status_response({ id: mesg.id, data }),347);348} catch (err) {349this.client.error_to_client({ id: mesg.id, error: err2str(err) });350}351}352353async delete(mesg): Promise<void> {354this.client.touch();355const dbg = this.dbg("delete");356// this filters possible results357mesg.not_yet_done = true;358try {359const copy_op = await this._get_status(mesg);360361if (copy_op == null) {362this.client.error_to_client({363id: mesg.id,364error: `copy op '${mesg.copy_path_id}' cannot be deleted.`,365});366} else {367await callback2(this.client.database._query, {368query: "DELETE FROM copy_paths",369where: { "id = $::UUID": mesg.copy_path_id },370});371// no error372this.client.push_to_client(373message.copy_path_status_response({374id: mesg.id,375data: `copy_path_id = '${mesg.copy_path_id}' deleted`,376}),377);378}379} catch (err) {380dbg(`status err=${err2str(err)}`);381this.client.error_to_client({ id: mesg.id, error: err2str(err) });382}383}384385private async _read_access(src_project_id): Promise<boolean> {386if (!is_valid_uuid_string(src_project_id)) {387this.throw(`invalid src_project_id=${src_project_id}`);388}389390const read_ok = await callback2(access.user_has_read_access_to_project, {391project_id: src_project_id,392account_id: this.client.account_id,393account_groups: this.client.groups,394database: this.client.database,395});396// this.dbg("_read_access")(read_ok);397if (!read_ok) {398this.throw(399`ACCESS BLOCKED -- No read access to source project -- ${src_project_id}`,400);401return false;402}403return true;404}405406private async _write_access(target_project_id): Promise<boolean> {407if (!is_valid_uuid_string(target_project_id)) {408this.throw(`invalid target_project_id=${target_project_id}`);409}410411const write_ok = await callback2(access.user_has_write_access_to_project, {412database: this.client.database,413project_id: target_project_id,414account_id: this.client.account_id,415account_groups: this.client.groups,416});417// this.dbg("_write_access")(write_ok);418if (!write_ok) {419this.throw(420`ACCESS BLOCKED -- No write access to target project -- ${target_project_id}`,421);422return false;423}424return true;425}426}427428429