diff --git a/.gitignore b/.gitignore index 0e72f66..78745ad 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ pdfgen/* !pdfgen/template*.tex documents/* +user-data/* +tmp-user-data/* diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 1417e59..8201e69 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -1,10 +1,10 @@ -import { mkdir } from "node:fs/promises"; +import fs from "node:fs/promises"; import { Database, SQLiteError } from "bun:sqlite"; import { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types"; import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util"; -const DATABASES_PATH: string = "./databases/"; +const DATABASES_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/databases/"; const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite"; const CHECK_QUERY: string = @@ -291,7 +291,7 @@ function is_db_initialized(db: Database): boolean { throw exception; } - console.log(e); + console.log(exception); return false; } @@ -305,8 +305,10 @@ function setup_db(db: Database, setup_queries: string[]) { setup_queries.forEach((q) => { /*console.log(q);*/ db.query(q).run(); }); } -export function init_db() { +export async function init_db() { + const stdout = await fs.mkdir(DATABASES_PATH, { recursive: true }); + console.log(stdout) user_database = new Database(USER_DATABASE_PATH, { strict: true, create: true }); if (!is_db_initialized(user_database)) { @@ -366,6 +368,8 @@ export function get_user(): User | null { const db_name = get_user_db_name(user); try { + + fs.mkdir(DATABASES_PATH, { recursive: true }); let userdb = new Database(db_name, { create: true, strict: true }); diff --git a/src/lib/server/docstore.ts b/src/lib/server/docstore.ts index 68864d5..e8c8644 100644 --- a/src/lib/server/docstore.ts +++ b/src/lib/server/docstore.ts @@ -1,21 +1,25 @@ import b from "bun"; import fs from "node:fs/promises" -import type { RecordEntry } from "$lib/db_types"; import { User } from "$lib/server/database"; import { MONTHS, toInt, padInt, parseDate, calculateDuration, isoToLocalDate, month_of, weekday_of } from "$lib/util" export class UserBusyException extends Error { - constructor(message, userid) { - super(`user (id: ${userid}) is busy: ${message}`); - this.name = this.constructor.name; - this.userid = userid; - } + userid: number + + constructor(message: string, userid: number) { + super(`user (id: ${userid}) is busy: ${message}`); + this.name = this.constructor.name; + this.userid = userid; + } } export class LatexException extends Error { - constructor(message, userid, errno, stdout) { + errno: number + stdout: string + + constructor(message: string, userid: number, errno: number, stdout: string) { super(message + ` (userid: ${userid}, errno: ${errno})\n${stdout}`); this.name = this.constructor.name; this.errno = errno; @@ -24,107 +28,129 @@ export class LatexException extends Error { } export class FileExistsException extends Error { - constructor(path) { + path: string + + constructor(path: string) { super("File already exists: " + path); this.name = this.constructor.name; this.path = path; } } +export interface FileProperties { + identifier: string + path: string + name: string + cdate: Date +} -const DOCUMENTS_PATH: string = "./documents" + +const DOCUMENTS_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/documents" const ESTIMATES_FOLD: string = "estimates" const RECORDS_FOLD: string = "records" -const GENERATION_PATH: string = "./pdfgen"; +const GENERATION_PATH: string = (process.env.APP_TMP_USER_DATA_PATH ?? ".") + "/pdfgen"; const TEMPLATE_REC = "template-rec.tex"; -const TEMPLATE_REC_PATH = `${GENERATION_PATH}/${TEMPLATE_REC}`; +const TEMPLATE_REC_PATH = `./templates/${TEMPLATE_REC}`; const TEMPLATE_EST = "template-est.tex"; -const TEMPLATE_EST_PATH = `${GENERATION_PATH}/${TEMPLATE_EST}`; +const TEMPLATE_EST_PATH = `./templates/${TEMPLATE_EST}`; -const SALUTATION = { +const SALUTATION: Record = { m: "Herrn", w: "Frau" } -const GENDER_END = { +const GENDER_END: Record = { m: "", w: "in", } +/* + +const SALUTATION: Map = new Map(Object.entries({ + m: "Herrn", + w: "Frau" +})) + +const GENDER_END: Map = new Map(Object.entries({ + "m": "", + "w": "in", +})) +*/ // this may be a race condition??? // is a mutex needed in JS? -let locked_users: number[] = new Set(); +let locked_users: Set = new Set(); export async function getAllFiles(user: User) { const path = `${DOCUMENTS_PATH}/user-${user.id}`; - let file_names = (await fs.readdir(path, { recursive: true }).catch((err) => { + let file_names = (await fs.readdir(path, { recursive: true }).catch((_) => { return []; })).filter((v) => { return !(v == ESTIMATES_FOLD || v == RECORDS_FOLD); }); - let files = (await Promise.all(file_names.map(async (v) => { + let files: FileProperties[] = (await Promise.all(file_names.map(async (v) => { return { + identifier: v.split("/").pop(), path: v, name: v.split("/").pop(), - timestamp: (await fs.stat(`${path}/${v}`)).mtime - } + cdate: (await fs.stat(`${path}/${v}`)).ctime + } as FileProperties; }))).sort((a, b) => { - return a.timestamp - b.timestamp + return a.cdate.getTime() - b.cdate.getTime() }); return files; } -export async function getRecordFiles(user: User) { +export async function getRecordFiles(user: User): Promise { const path = `${DOCUMENTS_PATH}/user-${user.id}/${RECORDS_FOLD}`; let file_names = await fs.readdir(path).catch((err) => { console.log(err); return [] }); - let files = await Promise.all(file_names.map(async (v) => { + let files: FileProperties[] = await Promise.all(file_names.map(async (v) => { return { identifier: v.replace(/^Stundenliste-/, "").replace(/\.pdf$/, ""), - filename: v, + name: v, path: `${RECORDS_FOLD}/${v}`, cdate: (await fs.stat(`${path}/${v}`)).ctime, - } + } as FileProperties; })) return files; } -export async function getEstimateFiles(user: User) { +export async function getEstimateFiles(user: User): Promise { const path = `${DOCUMENTS_PATH}/user-${user.id}/${ESTIMATES_FOLD}`; let file_names = await fs.readdir(path).catch((err) => { console.log(err); return [] }); - let files = await Promise.all(file_names.map(async (v) => { + let files: FileProperties[] = await Promise.all(file_names.map(async (v) => { return { - identifier: v.match(/\d.Quartal_\d\d\d\d/)[0].replace(/.Quartal_/, ".").split(".").reverse().join("-"), - filename: v, + identifier: v.match(/\d.Quartal_\d\d\d\d/)?.[0].replace(/.Quartal_/, ".").split(".").reverse().join("-") ?? "", + name: v, path: `${ESTIMATES_FOLD}/${v}`, cdate: (await fs.stat(`${path}/${v}`)).ctime, - } + } as FileProperties; })) return files; } -export async function getFile(user: User, filename: string) { +export async function getFile(user: User, filename: string): Promise { const path = `${DOCUMENTS_PATH}/user-${user.id}`; let file = Bun.file(`${path}/${filename}`) if (!await file.exists()) { - return null; + return new Promise(_ => null); } - return await file.bytes(); + return file.bytes(); } export async function generateEstimatePDF(user: User, year: number, quarter: number) { @@ -134,10 +160,10 @@ export async function generateRecordPDF(user: User, year: number, quarter: numbe return await _runProtocted(_generateRecordPDF, user, year, quarter); } -async function _runProtocted(f, user: User, ...args) { +async function _runProtocted(f: Function, user: User, ...args: any[]) { if (locked_users.has(user.id)) { - throw UserBusyException(`Cannot generate pdf (type: ${type})` ,user.id); + throw new UserBusyException(`Cannot generate pdf for user: ` ,user.id); } locked_users.add(user.id); @@ -208,7 +234,7 @@ async function _genLatexRec(user: User, file_pref: string, year: number, month: if (estimate == null || isNaN(estimate)) { throw new Error("No estimate found") - } + } // TODO: escape semicolon in comment @@ -266,7 +292,7 @@ async function _genLatexEst(user: User, file_pref: string, year: number, quarter csvfilewriter.write(`${SALUTATION[user.gender]} ${user.name};${user.address};Arbeitnehmer${GENDER_END[user.gender]};Teilzeitmitarbeiter${GENDER_END[user.gender]};${isoToLocalDate(new Date().toISOString())}\n`) for (let i = 0; i < 3; ++i) { - csvfilewriter.write(`${MONTHS[(quarter - 1) * 3 + i]} ${year};${estimates[`estimate_${i}`]}\n`); + csvfilewriter.write(`${MONTHS[(quarter - 1) * 3 + i]} ${year};${(estimates as any)[`estimate_${i}`]}\n`); } csvfilewriter.end(); @@ -274,7 +300,7 @@ async function _genLatexEst(user: User, file_pref: string, year: number, quarter const { stdout, exitCode } = await b.$`pdflatex -interaction=nonstopmode -halt-on-error -jobname=${file_pref} -output-format=pdf ${TEMPLATE_EST}`.cwd(dir).quiet().nothrow(); if (exitCode != 0) { - throw new LatexException("Failed to create estimate PDF", user.id, exitCode, stdout); + throw new LatexException("Failed to create estimate PDF", user.id, exitCode, stdout.toString()); } return `${dir}/${file_pref}.pdf`; diff --git a/pdfgen/template-est.tex b/templates/template-est.tex similarity index 100% rename from pdfgen/template-est.tex rename to templates/template-est.tex diff --git a/pdfgen/template-rec.tex b/templates/template-rec.tex similarity index 100% rename from pdfgen/template-rec.tex rename to templates/template-rec.tex