Path: blob/master/src/packages/file-server/zfs/db.ts
1447 views
/*1Database2*/34import Database from "better-sqlite3";5import { context } from "./config";6import {7primaryKey,8type PrimaryKey,9type Filesystem,10type RawFilesystem,11type SetFilesystem,12OWNER_ID_FIELDS,13} from "./types";14import { is_array, is_date } from "@cocalc/util/misc";1516let db: { [file: string]: Database.Database } = {};1718const tableName = "filesystems";19const schema = {20// this uniquely defines the filesystem (it's the compound primary key)21owner_type: "TEXT",22owner_id: "TEXT",23namespace: "TEXT",24name: "TEXT",2526// data about the filesystem27pool: "TEXT",28archived: "INTEGER",29affinity: "TEXT",30nfs: "TEXT",31snapshots: "TEXT",32last_edited: "TEXT",33last_send_snapshot: "TEXT",34last_bup_backup: "TEXT",35error: "TEXT",36last_error: "TEXT",37used_by_dataset: "INTEGER",38used_by_snapshots: "INTEGER",39quota: "INTEGER",40};4142const WHERE_PRIMARY_KEY =43"WHERE namespace=? AND owner_type=? AND owner_id=? AND name=?";44function primaryKeyArgs(fs: PrimaryKey) {45const { namespace, owner_type, owner_id, name } = primaryKey(fs);46return [namespace, owner_type, owner_id, name];47}4849export function getDb(databaseFile?): Database.Database {50const file = databaseFile ?? context.SQLITE3_DATABASE_FILE;51if (db[file] == null) {52db[file] = new Database(file);53initDb(db[file]);54}55return db[file]!;56}5758function initDb(db) {59const columnDefinitions = Object.entries(schema)60.map(([name, type]) => `${name} ${type}`)61.join(", ");6263// Create table if it doesn't exist64db.prepare(65`CREATE TABLE IF NOT EXISTS ${tableName} (66${columnDefinitions},67PRIMARY KEY (namespace, owner_type, owner_id, name)68)`,69).run();7071// Check for missing columns and add them72const existingColumnsStmt = db.prepare(`PRAGMA table_info(${tableName})`);73const existingColumns = existingColumnsStmt.all().map((row) => row.name);7475for (const [name, type] of Object.entries(schema)) {76if (!existingColumns.includes(name)) {77db.prepare(`ALTER TABLE ${tableName} ADD COLUMN ${name} ${type}`).run();78}79}80}8182// This is extremely dangerous and mainly used for unit testing:83export function resetDb() {84const db = new Database(context.SQLITE3_DATABASE_FILE);85db.prepare("DROP TABLE IF EXISTS filesystems").run();86initDb(db);87}8889function convertToSqliteType({ value, getFilesystem }) {90if (is_array(value)) {91return value.join(",");92} else if (is_date(value)) {93return value.toISOString();94} else if (typeof value == "boolean") {95return value ? 1 : 0;96} else if (typeof value == "function") {97const x = value(getFilesystem());98if (typeof x == "function") {99throw Error("function must not return a function");100}101// returned value needs to be converted102return convertToSqliteType({ value: x, getFilesystem });103}104return value;105}106107export function set(obj: SetFilesystem) {108const pk = primaryKey(obj);109const fields: string[] = [];110const values: any[] = [];111let filesystem: null | Filesystem = null;112const getFilesystem = () => {113if (filesystem == null) {114filesystem = get(pk);115}116return filesystem;117};118for (const field in obj) {119if (pk[field] !== undefined || OWNER_ID_FIELDS.includes(field)) {120continue;121}122fields.push(field);123values.push(convertToSqliteType({ value: obj[field], getFilesystem }));124}125let query = `UPDATE filesystems SET126${fields.map((field) => `${field}=?`).join(", ")}127${WHERE_PRIMARY_KEY}128`;129for (const x of primaryKeyArgs(pk)) {130values.push(x);131}132const db = getDb();133db.prepare(query).run(...values);134}135136// Call this if something that should never happen, does in fact, happen.137// It will set the error state of the filesystem and throw the exception.138// Admins will be regularly notified of all filesystems in an error state.139export function fatalError(140obj: PrimaryKey & {141err: Error;142desc?: string;143},144) {145set({146...primaryKey(obj),147error: `${obj.err}${obj.desc ? " - " + obj.desc : ""}`,148last_error: new Date(),149});150throw obj.err;151}152153export function clearError(fs: PrimaryKey) {154set({ ...fs, error: null });155}156157export function clearAllErrors() {158const db = getDb();159db.prepare("UPDATE filesystems SET error=null").run();160}161162export function getErrors() {163const db = getDb();164return db165.prepare("SELECT * FROM filesystems WHERE error!=''")166.all() as RawFilesystem[];167}168169export function touch(fs: PrimaryKey) {170set({ ...fs, last_edited: new Date() });171}172173export function filesystemExists(174fs: PrimaryKey,175databaseFile?: string,176): boolean {177const db = getDb(databaseFile);178const x = db179.prepare("SELECT COUNT(*) AS count FROM filesystems " + WHERE_PRIMARY_KEY)180.get(...primaryKeyArgs(fs));181return (x as any).count > 0;182}183184export function get(fs: PrimaryKey, databaseFile?: string): Filesystem {185const db = getDb(databaseFile);186const filesystem = db187.prepare("SELECT * FROM filesystems " + WHERE_PRIMARY_KEY)188.get(...primaryKeyArgs(fs)) as any;189if (filesystem == null) {190throw Error(`no filesystem ${JSON.stringify(fs)}`);191}192for (const key of ["nfs", "snapshots"]) {193filesystem[key] = sqliteStringToArray(filesystem[key]);194}195filesystem["archived"] = !!filesystem["archived"];196if (filesystem.last_edited) {197filesystem.last_edited = new Date(filesystem.last_edited);198}199if (filesystem.last_error) {200filesystem.last_error = new Date(filesystem.last_error);201}202return filesystem as Filesystem;203}204205export function create(206obj: PrimaryKey & {207pool: string;208affinity?: string;209},210) {211if (!obj.pool.startsWith(context.PREFIX)) {212throw Error(`pool must start with ${context.PREFIX} - ${obj.pool}`);213}214getDb()215.prepare(216"INSERT INTO filesystems(namespace, owner_type, owner_id, name, pool, affinity, last_edited) VALUES(?,?,?,?,?,?,?)",217)218.run(219...primaryKeyArgs(obj),220obj.pool,221obj.affinity,222new Date().toISOString(),223);224}225226export function deleteFromDb(fs: PrimaryKey) {227getDb()228.prepare("DELETE FROM filesystems " + WHERE_PRIMARY_KEY)229.run(...primaryKeyArgs(fs));230}231232export function getAll({233namespace = context.namespace,234}: { namespace?: string } = {}): RawFilesystem[] {235const db = getDb();236return db237.prepare("SELECT * FROM filesystems WHERE namespace=?")238.all(namespace) as RawFilesystem[];239}240241export function getNamespacesAndPools(): { namespace: string; pool: string }[] {242const db = getDb();243return db244.prepare("SELECT DISTINCT namespace, pool FROM filesystems")245.all() as any;246}247248export function getRecent({249namespace,250cutoff,251databaseFile,252}: {253namespace?: string;254cutoff?: Date;255databaseFile?: string;256} = {}): RawFilesystem[] {257const db = getDb(databaseFile);258if (cutoff == null) {259cutoff = new Date(Date.now() - 1000 * 60 * 60 * 24 * 7);260}261const query = "SELECT * FROM filesystems WHERE last_edited>=?";262if (namespace == null) {263return db.prepare(query).all(cutoff.toISOString()) as RawFilesystem[];264} else {265return db266.prepare(`${query} AND namespace=?`)267.all(cutoff.toISOString(), namespace) as RawFilesystem[];268}269}270271function sqliteStringToArray(s?: string): string[] {272if (!s) {273return [];274}275return s.split(",");276}277278279