Source: saltcorn-data/models/file.js

/**
 *
 * @type {{changeConnection?: ((function(*): Promise<void>)|(function(*=): Promise<void>)), select?: ((function(*=, *=, *=): Promise<*>)|(function(*=, *=, *=): Promise<*>)), runWithTenant: ((function(*=, *=): (*))|(function(*, *): *)), set_sql_logging?: (function(*=): void), insert?: ((function(*=, *=, *=): Promise<undefined|*>)|(function(*=, *=, *=): Promise<*>)), update?: ((function(*=, *=, *=): Promise<void>)|(function(*=, *=, *, *=): Promise<void>)), sql_log?: (function(*=, *=): void), deleteWhere?: ((function(*=, *=): Promise<void>)|(function(*=, *=): Promise<*>)), isSQLite: boolean, selectMaybeOne?: ((function(*=, *=): Promise<null|*>)|(function(*=, *=): Promise<null|*>)), close?: ((function(): Promise<void>)|(function(): Promise<void>)), drop_unique_constraint?: ((function(*=, *): Promise<void>)|(function(*=, *): Promise<void>)), enable_multi_tenant: (function()), getVersion?: ((function(): Promise<*>)|(function(*=): Promise<*>)), add_unique_constraint?: ((function(*=, *): Promise<void>)|(function(*=, *): Promise<void>)), getTenantSchema: ((function(): *)|(function(): *)), is_it_multi_tenant: ((function(): boolean)|(function(): boolean)), sqliteDatabase?: *, drop_reset_schema?: ((function(): Promise<void>)|(function(*): Promise<void>)), query?: ((function(*=, *=): Promise<unknown>)|(function(*=, *=): *)), count?: ((function(*=, *=): Promise<number>)|(function(*=, *=): Promise<number>)), pool?: *, connectObj: {sc_version: string, connectionString: *, git_commit: *, version_tag: (*|string)}|{sqlite_path}|boolean, sqlsanitize: *|(function(...[*]=): *), getClient?: (function(): Promise<*>), reset_sequence?: (function(*=): Promise<void>), copyFrom?: (function(*=, *=, *, *): Promise<void>), mkWhere: function(*=): {values: *, where: string|string}, selectOne?: ((function(*=, *=): Promise<*|undefined>)|(function(*=, *=): Promise<*>)), getTenantSchemaPrefix: function(): string|string}|{sqlsanitize?: *|(function(...[*]=): *), connectObj?: {sc_version: string, connectionString: *, git_commit: *, version_tag: (*|string)}|{sqlite_path}|boolean, isSQLite?: boolean, mkWhere?: function(*=): {values: *, where: string|string}, getTenantSchemaPrefix?: function(): string|string}}
 */


const db = require("../db");
const { contract, is } = require("contractis");
const { v4: uuidv4 } = require("uuid");
const path = require("path");
const { asyncMap } = require("../utils");
const fs = require("fs").promises;

/**
 * File Descriptor class
 *
 * Architecture tips:
 * 1. Physically Saltcorn stores files on local filesystem of the server, where Saltcorn runs.
 * 2. The path to file store is defined in db.connectObj.file_store.
 * 3. List of files stored in _sc_files table in Saltcorn database.
 * 4. Each tenant has own file list and file storage.
 * 5. This class provides file descriptor and basic functions to manipulate with files.
 */
class File {
  constructor(o) {
    this.filename = o.filename;
    this.location = o.location;
    this.uploaded_at = ["string", "number"].includes(typeof o.uploaded_at)
      ? new Date(o.uploaded_at)
      : o.uploaded_at;
    this.size_kb = o.size_kb;
    this.id = o.id;
    this.user_id = o.user_id;
    this.mime_super = o.mime_super;
    this.mime_sub = o.mime_sub;
    this.min_role_read = o.min_role_read;
    // TBD add checksum this.checksum = o.checksum;
    contract.class(this);
  }

  /**
   * Select list of file descriptors
   * @param where
   * @param selectopts
   * @returns {Promise<*>}
   */
  static async find(where, selectopts) {
    const db_flds = await db.select("_sc_files", where, selectopts);
    return db_flds.map((dbf) => new File(dbf));
  }

  /**
   * Select one file descriptor
   *
   * @param where
   * @returns {Promise<File|null>}
   */
  static async findOne(where) {
    if (where.id) {
      const { getState } = require("../db/state");
      const cf = getState().files[+where.id];
      if (cf) return new File(cf);
    }
    const f = await db.selectMaybeOne("_sc_files", where);
    return f ? new File(f) : null;
  }

  /**
   * Update File descriptor
   *
   * @param id - primary key
   * @param row - row data
   * @returns {Promise<void>} no returns
   */
  static async update(id, row) {
    await db.update("_sc_files", row, id);
    await require("../db/state").getState().refresh_files();
  }

  /**
   * Get absolute path to new file in db.connectObj.file_store.
   *
   * @param suggest - path to file inside file store. If undefined that autogenerated uudv4 is used.
   * @returns {string} - path to file
   */
  static get_new_path(suggest) {
    const file_store = db.connectObj.file_store;

    const newFnm = suggest || uuidv4();
    return path.join(file_store, newFnm);
  }

  /**
   * Ensure that file_store path is physically exists in file system.
   * In reality just recursively creates full absolute path to db.connectObj.file_store.
   *
   * @returns {Promise<void>}
   */
  // TBD fs errors handling
  static async ensure_file_store() {
    const file_store = db.connectObj.file_store;
    await fs.mkdir(file_store, { recursive: true });
  }

  /**
   * Create new file
   * @param file
   * @param user_id
   * @param min_role_read
   * @returns {Promise<File|[]>}
   */
  static async from_req_files(file, user_id, min_role_read = 1) {
    if (Array.isArray(file)) {
      return await asyncMap(file, (f) =>
        File.from_req_files(f, user_id, min_role_read)
      );
    } else {
      // get path to file
      const newPath = File.get_new_path();
      // set mime type
      const [mime_super, mime_sub] = file.mimetype.split("/");
      // move file in file system to newPath
      await file.mv(newPath);
      // create file
      return await File.create({
        filename: file.name,
        location: newPath,
        uploaded_at: new Date(),
        size_kb: Math.round(file.size / 1024),
        user_id,
        mime_super,
        mime_sub,
        min_role_read,
      });
    }
  }

  /**
   * Delete file
   * @returns {Promise<{error}>}
   */
  async delete() {
    try {
      // delete file from database
      await db.deleteWhere("_sc_files", { id: this.id });
      // delete name and possible file from file system
      await fs.unlink(this.location);
      // reload file list cache
      await require("../db/state").getState().refresh_files();
    } catch (e) {
      return { error: e.message };
    }
  }
  get mimetype() {
    return `${this.mime_super}/${this.mime_sub}`;
  }

  /**
   * Create file
   * @param f
   * @returns {Promise<File>}
   */
  static async create(f) {
    const file = new File(f);
    const { id, ...rest } = file;
    // insert file descriptor row to database
    file.id = await db.insert("_sc_files", rest);
    // refresh file list cache
    await require("../db/state").getState().refresh_files();

    return file;
  }
}

File.contract = {
  variables: {
    filename: is.str,
    location: is.str,
    mime_super: is.str,
    mime_sub: is.str,
    uploaded_at: is.class("Date"),
    size_kb: is.posint,
    id: is.maybe(is.posint),
    user_id: is.maybe(is.posint),
    min_role_read: is.posint,
    // tdb add checksum
  },
  methods: {
    delete: is.fun(
      [],
      is.promise(is.or(is.obj({ error: is.str }), is.undefined))
    ),
    mimetype: is.getter(is.str),
  },
  static_methods: {
    find: is.fun(
      [is.maybe(is.obj()), is.maybe(is.obj())],
      is.promise(is.array(is.class("File")))
    ),
    findOne: is.fun(is.obj(), is.promise(is.maybe(is.class("File")))),
    create: is.fun(is.obj(), is.promise(is.class("File"))),
    from_req_files: is.fun(
      [
        is.or(is.obj(), is.array(is.obj())),
        is.maybe(is.posint),
        is.maybe(is.posint),
      ],
      is.promise(is.or(is.class("File"), is.array(is.class("File"))))
    ),
    update: is.fun([is.posint, is.obj()], is.promise(is.undefined)),
    ensure_file_store: is.fun([], is.promise(is.undefined)),
    get_new_path: is.fun(is.maybe(is.str), is.str),
  },
};

module.exports = File;