/**
*
* @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;