Rework #6
					 47 changed files with 1236 additions and 747 deletions
				
			
		
							
								
								
									
										23
									
								
								dprint.json
									
										
									
									
									
								
							
							
						
						
									
										23
									
								
								dprint.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,23 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
  "incremental": true,
 | 
			
		||||
  "typescript": {
 | 
			
		||||
    "indentWidth": 2
 | 
			
		||||
  },
 | 
			
		||||
  "json": {
 | 
			
		||||
  },
 | 
			
		||||
  "markdown": {
 | 
			
		||||
  },
 | 
			
		||||
  "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json,md}"],
 | 
			
		||||
  "excludes": [
 | 
			
		||||
    "**/node_modules",
 | 
			
		||||
    "**/*-lock.json",
 | 
			
		||||
    "**/dist",
 | 
			
		||||
    "build/",
 | 
			
		||||
    "app/"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugins": [
 | 
			
		||||
    "https://plugins.dprint.dev/typescript-0.84.4.wasm",
 | 
			
		||||
    "https://plugins.dprint.dev/json-0.17.2.wasm",
 | 
			
		||||
    "https://plugins.dprint.dev/markdown-0.15.2.wasm"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ export interface SchemaMigration {
 | 
			
		|||
 | 
			
		||||
export interface Tags {
 | 
			
		||||
  description: string | null;
 | 
			
		||||
  name: string | null;
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Users {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,145 +1,7 @@
 | 
			
		|||
import { app, BrowserWindow, dialog, session } from "electron";
 | 
			
		||||
import { ipcMain } from "electron";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import { accessTokenName, getAdminAccessTokenValue, getAdminRefreshTokenValue, refreshTokenName } from "./src/login";
 | 
			
		||||
import { UserAccessor } from "./src/model/mod";
 | 
			
		||||
import { create_server } from "./src/server";
 | 
			
		||||
import { get_setting } from "./src/SettingConfig";
 | 
			
		||||
 | 
			
		||||
function registerChannel(cntr: UserAccessor) {
 | 
			
		||||
	ipcMain.handle("reset_password", async (event, username: string, password: string) => {
 | 
			
		||||
		const user = await cntr.findUser(username);
 | 
			
		||||
		if (user === undefined) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
		user.reset_password(password);
 | 
			
		||||
		return true;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
const setting = get_setting();
 | 
			
		||||
if (!setting.cli) {
 | 
			
		||||
	let wnd: BrowserWindow | null = null;
 | 
			
		||||
 | 
			
		||||
	const createWindow = async () => {
 | 
			
		||||
		wnd = new BrowserWindow({
 | 
			
		||||
			width: 800,
 | 
			
		||||
			height: 600,
 | 
			
		||||
			center: true,
 | 
			
		||||
			useContentSize: true,
 | 
			
		||||
			webPreferences: {
 | 
			
		||||
				preload: join(__dirname, "preload.js"),
 | 
			
		||||
				contextIsolation: true,
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
		await wnd.loadURL(`data:text/html;base64,` + Buffer.from(loading_html).toString("base64"));
 | 
			
		||||
		// await wnd.loadURL('../loading.html');
 | 
			
		||||
		// set admin cookies.
 | 
			
		||||
		await session.defaultSession.cookies.set({
 | 
			
		||||
			url: `http://localhost:${setting.port}`,
 | 
			
		||||
			name: accessTokenName,
 | 
			
		||||
			value: getAdminAccessTokenValue(),
 | 
			
		||||
			httpOnly: true,
 | 
			
		||||
			secure: false,
 | 
			
		||||
			sameSite: "strict",
 | 
			
		||||
		});
 | 
			
		||||
		await session.defaultSession.cookies.set({
 | 
			
		||||
			url: `http://localhost:${setting.port}`,
 | 
			
		||||
			name: refreshTokenName,
 | 
			
		||||
			value: getAdminRefreshTokenValue(),
 | 
			
		||||
			httpOnly: true,
 | 
			
		||||
			secure: false,
 | 
			
		||||
			sameSite: "strict",
 | 
			
		||||
		});
 | 
			
		||||
		try {
 | 
			
		||||
			const server = await create_server();
 | 
			
		||||
			const app = server.start_server();
 | 
			
		||||
			registerChannel(server.userController);
 | 
			
		||||
			await wnd.loadURL(`http://localhost:${setting.port}`);
 | 
			
		||||
		} catch (e) {
 | 
			
		||||
			if (e instanceof Error) {
 | 
			
		||||
				await dialog.showMessageBox({
 | 
			
		||||
					type: "error",
 | 
			
		||||
					title: "error!",
 | 
			
		||||
					message: e.message,
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				await dialog.showMessageBox({
 | 
			
		||||
					type: "error",
 | 
			
		||||
					title: "error!",
 | 
			
		||||
					message: String(e),
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		wnd.on("closed", () => {
 | 
			
		||||
			wnd = null;
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const isPrimary = app.requestSingleInstanceLock();
 | 
			
		||||
	if (!isPrimary) {
 | 
			
		||||
		app.quit(); // exit window
 | 
			
		||||
		app.exit();
 | 
			
		||||
	}
 | 
			
		||||
	app.on("second-instance", () => {
 | 
			
		||||
		if (wnd != null) {
 | 
			
		||||
			if (wnd.isMinimized()) {
 | 
			
		||||
				wnd.restore();
 | 
			
		||||
			}
 | 
			
		||||
			wnd.focus();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	app.on("ready", (event, info) => {
 | 
			
		||||
		createWindow();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	app.on("window-all-closed", () => {
 | 
			
		||||
		// quit when all windows are closed
 | 
			
		||||
		if (process.platform != "darwin") app.quit(); // (except leave MacOS app active until Cmd+Q)
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	app.on("activate", () => {
 | 
			
		||||
		// re-recreate window when dock icon is clicked and no other windows open
 | 
			
		||||
		if (wnd == null) createWindow();
 | 
			
		||||
	});
 | 
			
		||||
} else {
 | 
			
		||||
	(async () => {
 | 
			
		||||
		try {
 | 
			
		||||
			const server = await create_server();
 | 
			
		||||
			server.start_server();
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.log(error);
 | 
			
		||||
		}
 | 
			
		||||
	})();
 | 
			
		||||
}
 | 
			
		||||
const loading_html = `<!DOCTYPE html>
 | 
			
		||||
<html lang="ko"><head>
 | 
			
		||||
<meta charset="UTF-8">
 | 
			
		||||
<title>loading</title>
 | 
			
		||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
 | 
			
		||||
 fonts.googleapis.com; font-src 'self' fonts.gstatic.com">
 | 
			
		||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
</head>
 | 
			
		||||
<style>
 | 
			
		||||
body { margin-top: 100px; background-color: #3f51b5; color: #fff; text-align:center; }
 | 
			
		||||
h1 {
 | 
			
		||||
  font: 2em 'Roboto', sans-serif;
 | 
			
		||||
  margin-bottom: 40px;
 | 
			
		||||
}
 | 
			
		||||
#loading {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  width: 50px;
 | 
			
		||||
  height: 50px;
 | 
			
		||||
  border: 3px solid rgba(255,255,255,.3);
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  border-top-color: #fff;
 | 
			
		||||
  animation: spin 1s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
  to { transform: rotate(360deg);}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
    <body>
 | 
			
		||||
        <h1>Loading...</h1>
 | 
			
		||||
        <div id="loading"></div>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>`;
 | 
			
		||||
create_server().then((server) => {
 | 
			
		||||
	server.start_server();
 | 
			
		||||
}).catch((err) => {
 | 
			
		||||
	console.error(err);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										3
									
								
								packages/server/comic_config.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/server/comic_config.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
{
 | 
			
		||||
    "watch": []
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
require("ts-node").register();
 | 
			
		||||
const { Knex } = require("./src/config");
 | 
			
		||||
// Update with your config settings.
 | 
			
		||||
 | 
			
		||||
module.exports = Knex.config;
 | 
			
		||||
| 
						 | 
				
			
			@ -4,10 +4,8 @@
 | 
			
		|||
	"description": "",
 | 
			
		||||
	"main": "build/app.js",
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"compile": "tsc",
 | 
			
		||||
		"compile:watch": "tsc -w",
 | 
			
		||||
		"build": "cd src/client && pnpm run build:prod",
 | 
			
		||||
		"build:watch": "cd src/client && pnpm run build:watch",
 | 
			
		||||
		"compile": "swc src --out-dir dist",
 | 
			
		||||
		"dev": "nodemon -r @swc-node/register --exec node app.ts",
 | 
			
		||||
		"start": "node build/app.js"
 | 
			
		||||
	},
 | 
			
		||||
	"author": "",
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +14,7 @@
 | 
			
		|||
		"@zip.js/zip.js": "^2.7.40",
 | 
			
		||||
		"better-sqlite3": "^9.4.3",
 | 
			
		||||
		"chokidar": "^3.6.0",
 | 
			
		||||
		"dotenv": "^16.4.5",
 | 
			
		||||
		"jsonwebtoken": "^8.5.1",
 | 
			
		||||
		"koa": "^2.15.2",
 | 
			
		||||
		"koa-bodyparser": "^4.4.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -26,13 +25,18 @@
 | 
			
		|||
		"tiny-async-pool": "^1.3.0"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"dbtype": "*",
 | 
			
		||||
		"@swc-node/register": "^1.9.0",
 | 
			
		||||
		"@swc/cli": "^0.3.10",
 | 
			
		||||
		"@swc/core": "^1.4.11",
 | 
			
		||||
		"@types/better-sqlite3": "^7.6.9",
 | 
			
		||||
		"@types/jsonwebtoken": "^8.5.9",
 | 
			
		||||
		"@types/koa": "^2.15.0",
 | 
			
		||||
		"@types/koa-bodyparser": "^4.3.12",
 | 
			
		||||
		"@types/koa-compose": "^3.2.8",
 | 
			
		||||
		"@types/koa-router": "^7.4.8",
 | 
			
		||||
		"@types/node": "^14.18.63",
 | 
			
		||||
		"@types/tiny-async-pool": "^1.0.5"
 | 
			
		||||
		"@types/tiny-async-pool": "^1.0.5",
 | 
			
		||||
		"dbtype": "workspace:^",
 | 
			
		||||
		"nodemon": "^3.1.0"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { randomBytes } from "crypto";
 | 
			
		||||
import { existsSync, readFileSync, writeFileSync } from "fs";
 | 
			
		||||
import { Permission } from "./permission/permission";
 | 
			
		||||
import { randomBytes } from "node:crypto";
 | 
			
		||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
 | 
			
		||||
import type { Permission } from "./permission/permission";
 | 
			
		||||
 | 
			
		||||
export interface SettingConfig {
 | 
			
		||||
	/**
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +46,7 @@ const default_setting: SettingConfig = {
 | 
			
		|||
};
 | 
			
		||||
let setting: null | SettingConfig = null;
 | 
			
		||||
 | 
			
		||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
const setEmptyToDefault = (target: any, default_table: SettingConfig) => {
 | 
			
		||||
	let diff_occur = false;
 | 
			
		||||
	for (const key in default_table) {
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +60,7 @@ const setEmptyToDefault = (target: any, default_table: SettingConfig) => {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export const read_setting_from_file = () => {
 | 
			
		||||
	let ret = existsSync("settings.json") ? JSON.parse(readFileSync("settings.json", { encoding: "utf8" })) : {};
 | 
			
		||||
	const ret = existsSync("settings.json") ? JSON.parse(readFileSync("settings.json", { encoding: "utf8" })) : {};
 | 
			
		||||
	const partial_occur = setEmptyToDefault(ret, default_setting);
 | 
			
		||||
	if (partial_occur) {
 | 
			
		||||
		writeFileSync("settings.json", JSON.stringify(ret));
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +71,7 @@ export function get_setting(): SettingConfig {
 | 
			
		|||
	if (setting === null) {
 | 
			
		||||
		setting = read_setting_from_file();
 | 
			
		||||
		const env = process.env.NODE_ENV;
 | 
			
		||||
		if (env !== undefined && env != "production" && env != "development") {
 | 
			
		||||
		if (env !== undefined && env !== "production" && env !== "development") {
 | 
			
		||||
			throw new Error('process unknown value in NODE_ENV: must be either "development" or "production"');
 | 
			
		||||
		}
 | 
			
		||||
		setting.mode = env ?? setting.mode;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { Knex as k } from "knex";
 | 
			
		||||
import type { Knex as k } from "knex";
 | 
			
		||||
 | 
			
		||||
export namespace Knex {
 | 
			
		||||
	export const config: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { extname } from "path";
 | 
			
		||||
import { DocumentBody } from "../model/doc";
 | 
			
		||||
import { extname } from "node:path";
 | 
			
		||||
import type { DocumentBody } from "../model/doc";
 | 
			
		||||
import { readAllFromZip, readZip } from "../util/zipwrap";
 | 
			
		||||
import { ContentConstructOption, ContentFile, createDefaultClass, registerContentReferrer } from "./file";
 | 
			
		||||
import { type ContentConstructOption, createDefaultClass, registerContentReferrer } from "./file";
 | 
			
		||||
 | 
			
		||||
type ComicType = "doujinshi" | "artist cg" | "manga" | "western";
 | 
			
		||||
interface ComicDesc {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ export class ComicReferrer extends createDefaultClass("comic") {
 | 
			
		|||
		tags = tags.concat(this.desc.character?.map((x) => `character:${x}`) ?? []);
 | 
			
		||||
		tags = tags.concat(this.desc.group?.map((x) => `group:${x}`) ?? []);
 | 
			
		||||
		tags = tags.concat(this.desc.series?.map((x) => `series:${x}`) ?? []);
 | 
			
		||||
		const type = this.desc.type instanceof Array ? this.desc.type[0] : this.desc.type;
 | 
			
		||||
		const type = Array.isArray(this.desc.type) ? this.desc.type[0] : this.desc.type;
 | 
			
		||||
		tags.push(`type:${type}`);
 | 
			
		||||
		return {
 | 
			
		||||
			...basebody,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,9 @@
 | 
			
		|||
import { createHash } from "crypto";
 | 
			
		||||
import { promises, Stats } from "fs";
 | 
			
		||||
import { createHash } from "node:crypto";
 | 
			
		||||
import { promises, type Stats } from "node:fs";
 | 
			
		||||
import { Context, DefaultContext, DefaultState, Middleware, Next } from "koa";
 | 
			
		||||
import Router from "koa-router";
 | 
			
		||||
import { extname } from "path";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { DocumentBody } from "../model/mod";
 | 
			
		||||
import path, { extname } from "node:path";
 | 
			
		||||
import type { DocumentBody } from "../model/mod";
 | 
			
		||||
/**
 | 
			
		||||
 * content file or directory referrer
 | 
			
		||||
 */
 | 
			
		||||
| 
						 | 
				
			
			@ -24,13 +23,17 @@ type ContentFileConstructor = (new (
 | 
			
		|||
	content_type: string;
 | 
			
		||||
};
 | 
			
		||||
export const createDefaultClass = (type: string): ContentFileConstructor => {
 | 
			
		||||
	let cons = class implements ContentFile {
 | 
			
		||||
	const cons = class implements ContentFile {
 | 
			
		||||
		readonly path: string;
 | 
			
		||||
		// type = type;
 | 
			
		||||
		static content_type = type;
 | 
			
		||||
		protected hash: string | undefined;
 | 
			
		||||
		protected stat: Stats | undefined;
 | 
			
		||||
 | 
			
		||||
		protected getStat(){
 | 
			
		||||
			return this.stat;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		constructor(path: string, option?: ContentConstructOption) {
 | 
			
		||||
			this.path = path;
 | 
			
		||||
			this.hash = option?.hash;
 | 
			
		||||
| 
						 | 
				
			
			@ -67,14 +70,17 @@ export const createDefaultClass = (type: string): ContentFileConstructor => {
 | 
			
		|||
			return this.hash;
 | 
			
		||||
		}
 | 
			
		||||
		async getMtime(): Promise<number> {
 | 
			
		||||
			if (this.stat !== undefined) return this.stat.mtimeMs;
 | 
			
		||||
			const oldStat = this.getStat();
 | 
			
		||||
			if (oldStat !== undefined) return oldStat.mtimeMs;
 | 
			
		||||
			await this.getHash();
 | 
			
		||||
			return this.stat!.mtimeMs;
 | 
			
		||||
			const newStat = this.getStat();
 | 
			
		||||
			if (newStat === undefined) throw new Error("stat is undefined");
 | 
			
		||||
			return newStat.mtimeMs;
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
	return cons;
 | 
			
		||||
};
 | 
			
		||||
let ContstructorTable: { [k: string]: ContentFileConstructor } = {};
 | 
			
		||||
const ContstructorTable: { [k: string]: ContentFileConstructor } = {};
 | 
			
		||||
export function registerContentReferrer(s: ContentFileConstructor) {
 | 
			
		||||
	console.log(`registered content type: ${s.content_type}`);
 | 
			
		||||
	ContstructorTable[s.content_type] = s;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,6 @@
 | 
			
		|||
import { ContentConstructOption, ContentFile, registerContentReferrer } from "./file";
 | 
			
		||||
import { registerContentReferrer } from "./file";
 | 
			
		||||
import { createDefaultClass } from "./file";
 | 
			
		||||
 | 
			
		||||
export class VideoReferrer extends createDefaultClass("video") {
 | 
			
		||||
	constructor(path: string, desc?: ContentConstructOption) {
 | 
			
		||||
		super(path, desc);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
registerContentReferrer(VideoReferrer);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,47 +1,26 @@
 | 
			
		|||
import { existsSync } from "fs";
 | 
			
		||||
import Knex from "knex";
 | 
			
		||||
import { Knex as KnexConfig } from "./config";
 | 
			
		||||
import { existsSync } from "node:fs";
 | 
			
		||||
import { get_setting } from "./SettingConfig";
 | 
			
		||||
import { getKysely } from "./db/kysely";
 | 
			
		||||
 | 
			
		||||
export async function connectDB() {
 | 
			
		||||
	const env = get_setting().mode;
 | 
			
		||||
	const config = KnexConfig.config[env];
 | 
			
		||||
	if (!config.connection) {
 | 
			
		||||
		throw new Error("connection options required.");
 | 
			
		||||
	}
 | 
			
		||||
	const connection = config.connection;
 | 
			
		||||
	if (typeof connection === "string") {
 | 
			
		||||
		throw new Error("unknown connection options");
 | 
			
		||||
	}
 | 
			
		||||
	if (typeof connection === "function") {
 | 
			
		||||
		throw new Error("connection provider not supported...");
 | 
			
		||||
	}
 | 
			
		||||
	if (!("filename" in connection)) {
 | 
			
		||||
		throw new Error("sqlite3 config need");
 | 
			
		||||
	}
 | 
			
		||||
	const init_need = !existsSync(connection.filename);
 | 
			
		||||
	const knex = Knex(config);
 | 
			
		||||
	const kysely = getKysely();
 | 
			
		||||
 | 
			
		||||
	let tries = 0;
 | 
			
		||||
	for (;;) {
 | 
			
		||||
		try {
 | 
			
		||||
			console.log("try to connect db");
 | 
			
		||||
			await knex.raw("select 1 + 1;");
 | 
			
		||||
			await kysely.selectNoFrom(eb=> eb.val(1).as("dummy")).execute();
 | 
			
		||||
			console.log("connect success");
 | 
			
		||||
		} catch (err) {
 | 
			
		||||
			if (tries < 3) {
 | 
			
		||||
				tries++;
 | 
			
		||||
				console.error(`connection fail ${err} retry...`);
 | 
			
		||||
				await new Promise((resolve) => setTimeout(resolve, 1000));
 | 
			
		||||
				continue;
 | 
			
		||||
			} else {
 | 
			
		||||
				throw err;
 | 
			
		||||
			}
 | 
			
		||||
			throw err;
 | 
			
		||||
		}
 | 
			
		||||
		break;
 | 
			
		||||
	}
 | 
			
		||||
	if (init_need) {
 | 
			
		||||
		console.log("first execute: initialize database...");
 | 
			
		||||
		const migrate = await import("../migrations/initial");
 | 
			
		||||
		await migrate.up(knex);
 | 
			
		||||
	}
 | 
			
		||||
	return knex;
 | 
			
		||||
	return kysely;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,235 +1,222 @@
 | 
			
		|||
import { Knex } from "knex";
 | 
			
		||||
import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../model/doc";
 | 
			
		||||
import { TagAccessor } from "../model/tag";
 | 
			
		||||
import { createKnexTagController } from "./tag";
 | 
			
		||||
import { getKysely } from "./kysely";
 | 
			
		||||
import { jsonArrayFrom } from "kysely/helpers/sqlite";
 | 
			
		||||
import type { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../model/doc";
 | 
			
		||||
import { ParseJSONResultsPlugin, type NotNull } from "kysely";
 | 
			
		||||
import { MyParseJSONResultsPlugin } from "./plugin";
 | 
			
		||||
 | 
			
		||||
export type DBTagContentRelation = {
 | 
			
		||||
	doc_id: number;
 | 
			
		||||
	tag_name: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class KnexDocumentAccessor implements DocumentAccessor {
 | 
			
		||||
	knex: Knex;
 | 
			
		||||
	tagController: TagAccessor;
 | 
			
		||||
	constructor(knex: Knex) {
 | 
			
		||||
		this.knex = knex;
 | 
			
		||||
		this.tagController = createKnexTagController(knex);
 | 
			
		||||
class SqliteDocumentAccessor implements DocumentAccessor {
 | 
			
		||||
	constructor(private kysely = getKysely()) {
 | 
			
		||||
	}
 | 
			
		||||
	async search(search_word: string): Promise<Document[]> {
 | 
			
		||||
		throw new Error("Method not implemented.");
 | 
			
		||||
		const sw = `%${search_word}%`;
 | 
			
		||||
		const docs = await this.knex.select("*").from("document").where("title", "like", sw);
 | 
			
		||||
		return docs;
 | 
			
		||||
	}
 | 
			
		||||
	async addList(content_list: DocumentBody[]): Promise<number[]> {
 | 
			
		||||
		return await this.knex.transaction(async (trx) => {
 | 
			
		||||
		return await this.kysely.transaction().execute(async (trx) => {
 | 
			
		||||
			// add tags
 | 
			
		||||
			const tagCollected = new Set<string>();
 | 
			
		||||
			content_list
 | 
			
		||||
				.map((x) => x.tags)
 | 
			
		||||
				.forEach((x) => {
 | 
			
		||||
					x.forEach((x) => {
 | 
			
		||||
						tagCollected.add(x);
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			const tagCollectPromiseList = [];
 | 
			
		||||
			const tagController = createKnexTagController(trx);
 | 
			
		||||
			for (const it of tagCollected) {
 | 
			
		||||
				const p = tagController.addTag({ name: it });
 | 
			
		||||
				tagCollectPromiseList.push(p);
 | 
			
		||||
			}
 | 
			
		||||
			await Promise.all(tagCollectPromiseList);
 | 
			
		||||
			// add for each contents
 | 
			
		||||
			const ret = [];
 | 
			
		||||
			for (const content of content_list) {
 | 
			
		||||
				const { tags, additional, ...rest } = content;
 | 
			
		||||
				const id_lst = await trx
 | 
			
		||||
					.insert({
 | 
			
		||||
				for (const tag of content.tags) {
 | 
			
		||||
					tagCollected.add(tag);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			await trx.insertInto("tags")
 | 
			
		||||
				.values(Array.from(tagCollected).map((x) => ({ name: x })))
 | 
			
		||||
				.onConflict((oc) => oc.doNothing())
 | 
			
		||||
				.execute();
 | 
			
		||||
 | 
			
		||||
			const ids = await trx.insertInto("document")
 | 
			
		||||
				.values(content_list.map((content) => {
 | 
			
		||||
					const { tags, additional, ...rest } = content;
 | 
			
		||||
					return {
 | 
			
		||||
						additional: JSON.stringify(additional),
 | 
			
		||||
						created_at: Date.now(),
 | 
			
		||||
						...rest,
 | 
			
		||||
					})
 | 
			
		||||
					.into("document");
 | 
			
		||||
				const id = id_lst[0];
 | 
			
		||||
				if (tags.length > 0) {
 | 
			
		||||
					await trx
 | 
			
		||||
						.insert(
 | 
			
		||||
							tags.map((y) => ({
 | 
			
		||||
								doc_id: id,
 | 
			
		||||
								tag_name: y,
 | 
			
		||||
							})),
 | 
			
		||||
						)
 | 
			
		||||
						.into("doc_tag_relation");
 | 
			
		||||
				}
 | 
			
		||||
				ret.push(id);
 | 
			
		||||
			}
 | 
			
		||||
			return ret;
 | 
			
		||||
					};
 | 
			
		||||
				}))
 | 
			
		||||
				.returning("id")
 | 
			
		||||
				.execute();
 | 
			
		||||
			const id_lst = ids.map((x) => x.id);
 | 
			
		||||
 | 
			
		||||
			const doc_tags = content_list.flatMap((content, index) => {
 | 
			
		||||
				const { tags, ...rest } = content;
 | 
			
		||||
				return tags.map((tag) => ({ doc_id: id_lst[index], tag_name: tag }));
 | 
			
		||||
			});
 | 
			
		||||
			await trx.insertInto("doc_tag_relation")
 | 
			
		||||
				.values(doc_tags)
 | 
			
		||||
				.execute();
 | 
			
		||||
			return id_lst;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	async add(c: DocumentBody) {
 | 
			
		||||
		const { tags, additional, ...rest } = c;
 | 
			
		||||
		const id_lst = await this.knex
 | 
			
		||||
			.insert({
 | 
			
		||||
				additional: JSON.stringify(additional),
 | 
			
		||||
				created_at: Date.now(),
 | 
			
		||||
				...rest,
 | 
			
		||||
			})
 | 
			
		||||
			.into("document");
 | 
			
		||||
		const id = id_lst[0];
 | 
			
		||||
		for (const it of tags) {
 | 
			
		||||
			this.tagController.addTag({ name: it });
 | 
			
		||||
		}
 | 
			
		||||
		if (tags.length > 0) {
 | 
			
		||||
			await this.knex
 | 
			
		||||
				.insert<DBTagContentRelation>(tags.map((x) => ({ doc_id: id, tag_name: x })))
 | 
			
		||||
				.into("doc_tag_relation");
 | 
			
		||||
		}
 | 
			
		||||
		return id;
 | 
			
		||||
		return await this.kysely.transaction().execute(async (trx) => {
 | 
			
		||||
			const { tags, additional, ...rest } = c;
 | 
			
		||||
			const id_lst = await trx.insertInto("document").values({
 | 
			
		||||
					additional: JSON.stringify(additional),
 | 
			
		||||
					created_at: Date.now(),
 | 
			
		||||
					...rest,
 | 
			
		||||
				})
 | 
			
		||||
				.returning("id")
 | 
			
		||||
				.executeTakeFirst() as { id: number };
 | 
			
		||||
			const id = id_lst.id;
 | 
			
		||||
 | 
			
		||||
			// add tags
 | 
			
		||||
			await trx.insertInto("tags")
 | 
			
		||||
				.values(tags.map((x) => ({ name: x })))
 | 
			
		||||
				// on conflict is supported in sqlite and postgresql.
 | 
			
		||||
				.onConflict((oc) => oc.doNothing());
 | 
			
		||||
 | 
			
		||||
			if (tags.length > 0) {
 | 
			
		||||
				await trx.insertInto("doc_tag_relation")
 | 
			
		||||
					.values(tags.map((x) => ({ doc_id: id, tag_name: x })));
 | 
			
		||||
			}
 | 
			
		||||
			return id;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	async del(id: number) {
 | 
			
		||||
		if ((await this.findById(id)) !== undefined) {
 | 
			
		||||
			await this.knex.delete().from("doc_tag_relation").where({ doc_id: id });
 | 
			
		||||
			await this.knex.delete().from("document").where({ id: id });
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
		// delete tags
 | 
			
		||||
		await this.kysely
 | 
			
		||||
			.deleteFrom("doc_tag_relation")
 | 
			
		||||
			.where("doc_id", "=", id)
 | 
			
		||||
			.execute();
 | 
			
		||||
		// delete document
 | 
			
		||||
		const result = await this.kysely
 | 
			
		||||
			.deleteFrom("document")
 | 
			
		||||
			.where("id", "=", id)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return result.numDeletedRows > 0;
 | 
			
		||||
	}
 | 
			
		||||
	async findById(id: number, tagload?: boolean): Promise<Document | undefined> {
 | 
			
		||||
		const s = await this.knex.select("*").from("document").where({ id: id });
 | 
			
		||||
		if (s.length === 0) return undefined;
 | 
			
		||||
		const first = s[0];
 | 
			
		||||
		let ret_tags: string[] = [];
 | 
			
		||||
		if (tagload === true) {
 | 
			
		||||
			const tags: DBTagContentRelation[] = await this.knex
 | 
			
		||||
				.select("*")
 | 
			
		||||
				.from("doc_tag_relation")
 | 
			
		||||
				.where({ doc_id: first.id });
 | 
			
		||||
			ret_tags = tags.map((x) => x.tag_name);
 | 
			
		||||
		}
 | 
			
		||||
		const doc = await this.kysely.selectFrom("document")
 | 
			
		||||
			.selectAll()
 | 
			
		||||
			.where("id", "=", id)
 | 
			
		||||
			.$if(tagload ?? false, (qb) =>
 | 
			
		||||
				qb.select(eb => jsonArrayFrom(
 | 
			
		||||
					eb.selectFrom("doc_tag_relation")
 | 
			
		||||
						.select(["doc_tag_relation.tag_name"])
 | 
			
		||||
						.whereRef("document.id", "=", "doc_tag_relation.doc_id")
 | 
			
		||||
						.select("tag_name")
 | 
			
		||||
				).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags"))
 | 
			
		||||
			)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		if (!doc) return undefined;
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			...first,
 | 
			
		||||
			tags: ret_tags,
 | 
			
		||||
			additional: first.additional !== null ? JSON.parse(first.additional) : {},
 | 
			
		||||
			...doc,
 | 
			
		||||
			content_hash: doc.content_hash ?? "",
 | 
			
		||||
			additional: doc.additional !== null ? JSON.parse(doc.additional) : {},
 | 
			
		||||
			tags: doc.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [],
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
	async findDeleted(content_type: string) {
 | 
			
		||||
		const s = await this.knex
 | 
			
		||||
			.select("*")
 | 
			
		||||
			.where({ content_type: content_type })
 | 
			
		||||
			.whereNotNull("update_at")
 | 
			
		||||
			.from("document");
 | 
			
		||||
		return s.map((x) => ({
 | 
			
		||||
		const docs = await this.kysely
 | 
			
		||||
			.selectFrom("document")
 | 
			
		||||
			.selectAll()
 | 
			
		||||
			.where("content_type", "=", content_type)
 | 
			
		||||
			.where("deleted_at", "is not", null)
 | 
			
		||||
			.$narrowType<{ deleted_at: NotNull }>()
 | 
			
		||||
			.execute();
 | 
			
		||||
		return docs.map((x) => ({
 | 
			
		||||
			...x,
 | 
			
		||||
			tags: [],
 | 
			
		||||
			content_hash: x.content_hash ?? "",
 | 
			
		||||
			additional: {},
 | 
			
		||||
		}));
 | 
			
		||||
	}
 | 
			
		||||
	async findList(option?: QueryListOption) {
 | 
			
		||||
		option = option ?? {};
 | 
			
		||||
		const allow_tag = option.allow_tag ?? [];
 | 
			
		||||
		const eager_loading = option.eager_loading ?? true;
 | 
			
		||||
		const limit = option.limit ?? 20;
 | 
			
		||||
		const use_offset = option.use_offset ?? false;
 | 
			
		||||
		const offset = option.offset ?? 0;
 | 
			
		||||
		const word = option.word;
 | 
			
		||||
		const content_type = option.content_type;
 | 
			
		||||
		const cursor = option.cursor;
 | 
			
		||||
		const {
 | 
			
		||||
			allow_tag = [],
 | 
			
		||||
			eager_loading = true,
 | 
			
		||||
			limit = 20,
 | 
			
		||||
			use_offset = false,
 | 
			
		||||
			offset = 0,
 | 
			
		||||
			word,
 | 
			
		||||
			content_type,
 | 
			
		||||
			cursor,
 | 
			
		||||
		} = option ?? {};
 | 
			
		||||
 | 
			
		||||
		const buildquery = () => {
 | 
			
		||||
			let query = this.knex.select("document.*");
 | 
			
		||||
			if (allow_tag.length > 0) {
 | 
			
		||||
				query = query.from("doc_tag_relation as tags_0");
 | 
			
		||||
				query = query.where("tags_0.tag_name", "=", allow_tag[0]);
 | 
			
		||||
				for (let index = 1; index < allow_tag.length; index++) {
 | 
			
		||||
					const element = allow_tag[index];
 | 
			
		||||
					query = query.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.doc_id`, "tags_0.doc_id");
 | 
			
		||||
					query = query.where(`tags_${index}.tag_name`, "=", element);
 | 
			
		||||
				}
 | 
			
		||||
				query = query.innerJoin("document", "tags_0.doc_id", "document.id");
 | 
			
		||||
			} else {
 | 
			
		||||
				query = query.from("document");
 | 
			
		||||
			}
 | 
			
		||||
			if (word !== undefined) {
 | 
			
		||||
				// don't worry about sql injection.
 | 
			
		||||
				query = query.where("title", "like", `%${word}%`);
 | 
			
		||||
			}
 | 
			
		||||
			if (content_type !== undefined) {
 | 
			
		||||
				query = query.where("content_type", "=", content_type);
 | 
			
		||||
			}
 | 
			
		||||
			if (use_offset) {
 | 
			
		||||
				query = query.offset(offset);
 | 
			
		||||
			} else {
 | 
			
		||||
				if (cursor !== undefined) {
 | 
			
		||||
					query = query.where("id", "<", cursor);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			query = query.limit(limit);
 | 
			
		||||
			query = query.orderBy("id", "desc");
 | 
			
		||||
			return query;
 | 
			
		||||
		};
 | 
			
		||||
		let query = buildquery();
 | 
			
		||||
		// console.log(query.toSQL());
 | 
			
		||||
		let result: Document[] = await query;
 | 
			
		||||
		for (let i of result) {
 | 
			
		||||
			i.additional = JSON.parse(i.additional as unknown as string);
 | 
			
		||||
		}
 | 
			
		||||
		if (eager_loading) {
 | 
			
		||||
			let idmap: { [index: number]: Document } = {};
 | 
			
		||||
			for (const r of result) {
 | 
			
		||||
				idmap[r.id] = r;
 | 
			
		||||
				r.tags = [];
 | 
			
		||||
			}
 | 
			
		||||
			let subquery = buildquery();
 | 
			
		||||
			let tagquery = this.knex
 | 
			
		||||
				.select("id", "doc_tag_relation.tag_name")
 | 
			
		||||
				.from(subquery)
 | 
			
		||||
				.innerJoin("doc_tag_relation", "doc_tag_relation.doc_id", "id");
 | 
			
		||||
			// console.log(tagquery.toSQL());
 | 
			
		||||
			let tagresult: { id: number; tag_name: string }[] = await tagquery;
 | 
			
		||||
			for (const { id, tag_name } of tagresult) {
 | 
			
		||||
				idmap[id].tags.push(tag_name);
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			result.forEach((v) => {
 | 
			
		||||
				v.tags = [];
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
		return result;
 | 
			
		||||
		const result = await this.kysely
 | 
			
		||||
			.selectFrom("document")
 | 
			
		||||
			.selectAll()
 | 
			
		||||
			.$if(allow_tag.length > 0, (qb) => {
 | 
			
		||||
				return allow_tag.reduce((prevQb ,tag, index) => {
 | 
			
		||||
					return prevQb.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.tag_name`, "document.id")
 | 
			
		||||
						.where(`tags_${index}.tag_name`, "=", tag);
 | 
			
		||||
				}, qb) as unknown as typeof qb;
 | 
			
		||||
			})
 | 
			
		||||
			.$if(word !== undefined, (qb) => qb.where("title", "like", `%${word}%`))
 | 
			
		||||
			.$if(content_type !== undefined, (qb) => qb.where("content_type", "=", content_type as string))
 | 
			
		||||
			.$if(use_offset, (qb) => qb.offset(offset))
 | 
			
		||||
			.$if(!use_offset && cursor !== undefined, (qb) => qb.where("id", "<", cursor as number))
 | 
			
		||||
			.limit(limit)
 | 
			
		||||
			.$if(eager_loading, (qb) => {
 | 
			
		||||
				return qb.select(eb => jsonArrayFrom(
 | 
			
		||||
					eb.selectFrom("doc_tag_relation")
 | 
			
		||||
						.select(["doc_tag_relation.tag_name"])
 | 
			
		||||
						.whereRef("document.id", "=", "doc_tag_relation.doc_id")
 | 
			
		||||
				).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags"))
 | 
			
		||||
			})
 | 
			
		||||
			.orderBy("id", "desc")
 | 
			
		||||
			.execute();
 | 
			
		||||
		return result.map((x) => ({
 | 
			
		||||
			...x,
 | 
			
		||||
			content_hash: x.content_hash ?? "",
 | 
			
		||||
			additional: x.additional !== null ? (JSON.parse(x.additional)) : {},
 | 
			
		||||
			tags: x.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [],
 | 
			
		||||
		}));
 | 
			
		||||
	}
 | 
			
		||||
	async findByPath(path: string, filename?: string): Promise<Document[]> {
 | 
			
		||||
		const e = filename == undefined ? {} : { filename: filename };
 | 
			
		||||
		const results = await this.knex
 | 
			
		||||
			.select("*")
 | 
			
		||||
			.from("document")
 | 
			
		||||
			.where({ basepath: path, ...e });
 | 
			
		||||
		const results = await this.kysely
 | 
			
		||||
			.selectFrom("document")
 | 
			
		||||
			.selectAll()
 | 
			
		||||
			.where("basepath", "=", path)
 | 
			
		||||
			.$if(filename !== undefined, (qb) => qb.where("filename", "=", filename as string))
 | 
			
		||||
			.execute();
 | 
			
		||||
		return results.map((x) => ({
 | 
			
		||||
			...x,
 | 
			
		||||
			content_hash: x.content_hash ?? "",
 | 
			
		||||
			tags: [],
 | 
			
		||||
			additional: {},
 | 
			
		||||
		}));
 | 
			
		||||
	}
 | 
			
		||||
	async update(c: Partial<Document> & { id: number }) {
 | 
			
		||||
		const { id, tags, ...rest } = c;
 | 
			
		||||
		if ((await this.findById(id)) !== undefined) {
 | 
			
		||||
			await this.knex.update(rest).where({ id: id }).from("document");
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
		const { id, tags, additional, ...rest } = c;
 | 
			
		||||
		const r = await this.kysely.updateTable("document")
 | 
			
		||||
			.set({
 | 
			
		||||
				...rest,
 | 
			
		||||
				modified_at: Date.now(),
 | 
			
		||||
				additional: additional !== undefined ? JSON.stringify(additional) : undefined,
 | 
			
		||||
			})
 | 
			
		||||
			.where("id", "=", id)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return r.numUpdatedRows > 0;
 | 
			
		||||
	}
 | 
			
		||||
	async addTag(c: Document, tag_name: string) {
 | 
			
		||||
		if (c.tags.includes(tag_name)) return false;
 | 
			
		||||
		this.tagController.addTag({ name: tag_name });
 | 
			
		||||
		await this.knex.insert<DBTagContentRelation>({ tag_name: tag_name, doc_id: c.id }).into("doc_tag_relation");
 | 
			
		||||
		await this.kysely.insertInto("tags")
 | 
			
		||||
			.values({ name: tag_name })
 | 
			
		||||
			.onConflict((oc) => oc.doNothing())
 | 
			
		||||
			.execute();
 | 
			
		||||
		await this.kysely.insertInto("doc_tag_relation")
 | 
			
		||||
			.values({ tag_name: tag_name, doc_id: c.id })
 | 
			
		||||
			.execute();
 | 
			
		||||
		c.tags.push(tag_name);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	async delTag(c: Document, tag_name: string) {
 | 
			
		||||
		if (c.tags.includes(tag_name)) return false;
 | 
			
		||||
		await this.knex.delete().where({ tag_name: tag_name, doc_id: c.id }).from("doc_tag_relation");
 | 
			
		||||
		c.tags.push(tag_name);
 | 
			
		||||
		await this.kysely.deleteFrom("doc_tag_relation")
 | 
			
		||||
			.where("tag_name", "=", tag_name)
 | 
			
		||||
			.where("doc_id", "=", c.id)
 | 
			
		||||
			.execute();
 | 
			
		||||
		c.tags.splice(c.tags.indexOf(tag_name), 1);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
export const createKnexDocumentAccessor = (knex: Knex): DocumentAccessor => {
 | 
			
		||||
	return new KnexDocumentAccessor(knex);
 | 
			
		||||
export const createSqliteDocumentAccessor = (kysely = getKysely()): DocumentAccessor => {
 | 
			
		||||
	return new SqliteDocumentAccessor(kysely);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										26
									
								
								packages/server/src/db/kysely.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								packages/server/src/db/kysely.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
import { Kysely, ParseJSONResultsPlugin, SqliteDialect } from "kysely";
 | 
			
		||||
import SqliteDatabase from "better-sqlite3";
 | 
			
		||||
import type { DB } from "dbtype/types";
 | 
			
		||||
 | 
			
		||||
export function createSqliteDialect() {
 | 
			
		||||
    const url = process.env.DATABASE_URL;
 | 
			
		||||
    if (!url) {
 | 
			
		||||
        throw new Error("DATABASE_URL is not set");
 | 
			
		||||
    }
 | 
			
		||||
    const db = new SqliteDatabase(url);
 | 
			
		||||
    return new SqliteDialect({
 | 
			
		||||
        database: db,
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create a new Kysely instance with a new SqliteDatabase instance
 | 
			
		||||
let kysely: Kysely<DB> | null = null;
 | 
			
		||||
export function getKysely() {
 | 
			
		||||
    if (!kysely) {
 | 
			
		||||
        kysely = new Kysely<DB>({
 | 
			
		||||
            dialect: createSqliteDialect(),
 | 
			
		||||
            // plugins: [new ParseJSONResultsPlugin()],
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    return kysely;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								packages/server/src/db/plugin.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/server/src/db/plugin.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs, QueryResult, RootOperationNode, UnknownRow } from "kysely";
 | 
			
		||||
 | 
			
		||||
export class MyParseJSONResultsPlugin implements KyselyPlugin {
 | 
			
		||||
 | 
			
		||||
    constructor(private readonly itemPath: string) { }
 | 
			
		||||
 | 
			
		||||
    transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
 | 
			
		||||
        // do nothing
 | 
			
		||||
        return args.node;
 | 
			
		||||
    }
 | 
			
		||||
    async transformResult(args: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>> {
 | 
			
		||||
        return {
 | 
			
		||||
            ...args.result,
 | 
			
		||||
            rows: args.result.rows.map((row) => {
 | 
			
		||||
                const newRow = { ...row };
 | 
			
		||||
                const item = newRow[this.itemPath];
 | 
			
		||||
                if (typeof item === "string") {
 | 
			
		||||
                    newRow[this.itemPath] = JSON.parse(item);
 | 
			
		||||
                }
 | 
			
		||||
                return newRow;
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,61 +1,65 @@
 | 
			
		|||
import { Knex } from "knex";
 | 
			
		||||
import { Tag, TagAccessor, TagCount } from "../model/tag";
 | 
			
		||||
import { DBTagContentRelation } from "./doc";
 | 
			
		||||
import { getKysely } from "./kysely";
 | 
			
		||||
import { jsonArrayFrom } from "kysely/helpers/sqlite";
 | 
			
		||||
import type { Tag, TagAccessor, TagCount } from "../model/tag";
 | 
			
		||||
import type { DBTagContentRelation } from "./doc";
 | 
			
		||||
 | 
			
		||||
type DBTags = {
 | 
			
		||||
	name: string;
 | 
			
		||||
	description?: string;
 | 
			
		||||
};
 | 
			
		||||
class SqliteTagAccessor implements TagAccessor {
 | 
			
		||||
 | 
			
		||||
class KnexTagAccessor implements TagAccessor {
 | 
			
		||||
	knex: Knex<DBTags>;
 | 
			
		||||
	constructor(knex: Knex) {
 | 
			
		||||
		this.knex = knex;
 | 
			
		||||
	constructor(private kysely = getKysely()) {
 | 
			
		||||
	}
 | 
			
		||||
	async getAllTagCount(): Promise<TagCount[]> {
 | 
			
		||||
		const result = await this.knex<DBTagContentRelation>("doc_tag_relation")
 | 
			
		||||
		const result = await this.kysely
 | 
			
		||||
			.selectFrom("doc_tag_relation")
 | 
			
		||||
			.select("tag_name")
 | 
			
		||||
			.count("*", { as: "occurs" })
 | 
			
		||||
			.groupBy<TagCount[]>("tag_name");
 | 
			
		||||
			.select(qb => qb.fn.count<number>("doc_id").as("occurs"))
 | 
			
		||||
			.groupBy("tag_name")
 | 
			
		||||
			.execute();
 | 
			
		||||
		return result;
 | 
			
		||||
	}
 | 
			
		||||
	async getAllTagList(onlyname?: boolean) {
 | 
			
		||||
		onlyname = onlyname ?? false;
 | 
			
		||||
		const t: DBTags[] = await this.knex.select(onlyname ? "*" : "name").from("tags");
 | 
			
		||||
		return t;
 | 
			
		||||
	async getAllTagList(): Promise<Tag[]> {
 | 
			
		||||
		return (await this.kysely.selectFrom("tags")
 | 
			
		||||
			.selectAll()
 | 
			
		||||
			.execute()
 | 
			
		||||
			).map((x) => ({
 | 
			
		||||
				name: x.name,
 | 
			
		||||
				description: x.description ?? undefined,
 | 
			
		||||
			}));
 | 
			
		||||
	}
 | 
			
		||||
	async getTagByName(name: string) {
 | 
			
		||||
		const t: DBTags[] = await this.knex.select("*").from("tags").where({ name: name });
 | 
			
		||||
		if (t.length === 0) return undefined;
 | 
			
		||||
		return t[0];
 | 
			
		||||
		const result = await this.kysely
 | 
			
		||||
			.selectFrom("tags")
 | 
			
		||||
			.selectAll()
 | 
			
		||||
			.where("name", "=", name)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		if (result === undefined) {
 | 
			
		||||
			return undefined;
 | 
			
		||||
		}
 | 
			
		||||
		return {
 | 
			
		||||
			name: result.name,
 | 
			
		||||
			description: result.description ?? undefined,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
	async addTag(tag: Tag) {
 | 
			
		||||
		if ((await this.getTagByName(tag.name)) === undefined) {
 | 
			
		||||
			await this.knex
 | 
			
		||||
				.insert<DBTags>({
 | 
			
		||||
					name: tag.name,
 | 
			
		||||
					description: tag.description === undefined ? "" : tag.description,
 | 
			
		||||
				})
 | 
			
		||||
				.into("tags");
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
		const result = await this.kysely.insertInto("tags")
 | 
			
		||||
			.values([tag])
 | 
			
		||||
			.onConflict((oc) => oc.doNothing())
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return (result.numInsertedOrUpdatedRows ?? 0n) > 0;
 | 
			
		||||
	}
 | 
			
		||||
	async delTag(name: string) {
 | 
			
		||||
		if ((await this.getTagByName(name)) !== undefined) {
 | 
			
		||||
			await this.knex.delete().where({ name: name }).from("tags");
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
		const result = await this.kysely.deleteFrom("tags")
 | 
			
		||||
			.where("name", "=", name)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return (result.numDeletedRows ?? 0n) > 0;
 | 
			
		||||
	}
 | 
			
		||||
	async updateTag(name: string, desc: string) {
 | 
			
		||||
		if ((await this.getTagByName(name)) !== undefined) {
 | 
			
		||||
			await this.knex.update({ description: desc }).where({ name: name }).from("tags");
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
		const result = await this.kysely.updateTable("tags")
 | 
			
		||||
			.set({ description: desc })
 | 
			
		||||
			.where("name", "=", name)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return (result.numUpdatedRows ?? 0n) > 0;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
export const createKnexTagController = (knex: Knex): TagAccessor => {
 | 
			
		||||
	return new KnexTagAccessor(knex);
 | 
			
		||||
export const createSqliteTagController = (kysely = getKysely()): TagAccessor => {
 | 
			
		||||
	return new SqliteTagAccessor(kysely);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,88 +1,87 @@
 | 
			
		|||
import { Knex } from "knex";
 | 
			
		||||
import { IUser, Password, UserAccessor, UserCreateInput } from "../model/user";
 | 
			
		||||
import { getKysely } from "./kysely";
 | 
			
		||||
import { type IUser, Password, type UserAccessor, type UserCreateInput } from "../model/user";
 | 
			
		||||
 | 
			
		||||
type PermissionTable = {
 | 
			
		||||
	username: string;
 | 
			
		||||
	name: string;
 | 
			
		||||
};
 | 
			
		||||
type DBUser = {
 | 
			
		||||
	username: string;
 | 
			
		||||
	password_hash: string;
 | 
			
		||||
	password_salt: string;
 | 
			
		||||
};
 | 
			
		||||
class KnexUser implements IUser {
 | 
			
		||||
	private knex: Knex;
 | 
			
		||||
class SqliteUser implements IUser {
 | 
			
		||||
	readonly username: string;
 | 
			
		||||
	readonly password: Password;
 | 
			
		||||
 | 
			
		||||
	constructor(username: string, pw: Password, knex: Knex) {
 | 
			
		||||
	constructor(username: string, pw: Password, private kysely = getKysely()) {
 | 
			
		||||
		this.username = username;
 | 
			
		||||
		this.password = pw;
 | 
			
		||||
		this.knex = knex;
 | 
			
		||||
	}
 | 
			
		||||
	async reset_password(password: string) {
 | 
			
		||||
		this.password.set_password(password);
 | 
			
		||||
		await this.knex
 | 
			
		||||
			.from("users")
 | 
			
		||||
			.where({ username: this.username })
 | 
			
		||||
			.update({ password_hash: this.password.hash, password_salt: this.password.salt });
 | 
			
		||||
		await this.kysely
 | 
			
		||||
			.updateTable("users")
 | 
			
		||||
			.where("username", "=", this.username)
 | 
			
		||||
			.set({ password_hash: this.password.hash, password_salt: this.password.salt })
 | 
			
		||||
			.execute();
 | 
			
		||||
	}
 | 
			
		||||
	async get_permissions() {
 | 
			
		||||
		let b = (await this.knex.select("*").from("permissions").where({ username: this.username })) as PermissionTable[];
 | 
			
		||||
		return b.map((x) => x.name);
 | 
			
		||||
		const permissions = await this.kysely
 | 
			
		||||
			.selectFrom("permissions")
 | 
			
		||||
			.selectAll()
 | 
			
		||||
			.where("username", "=", this.username)
 | 
			
		||||
			.execute();
 | 
			
		||||
		return permissions.map((x) => x.name);
 | 
			
		||||
	}
 | 
			
		||||
	async add(name: string) {
 | 
			
		||||
		if (!(await this.get_permissions()).includes(name)) {
 | 
			
		||||
			const r = await this.knex
 | 
			
		||||
				.insert({
 | 
			
		||||
					username: this.username,
 | 
			
		||||
					name: name,
 | 
			
		||||
				})
 | 
			
		||||
				.into("permissions");
 | 
			
		||||
			return true;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
		const result = await this.kysely
 | 
			
		||||
			.insertInto("permissions")
 | 
			
		||||
			.values({ username: this.username, name })
 | 
			
		||||
			.onConflict((oc) => oc.doNothing())
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return (result.numInsertedOrUpdatedRows ?? 0n) > 0;
 | 
			
		||||
	}
 | 
			
		||||
	async remove(name: string) {
 | 
			
		||||
		const r = await this.knex
 | 
			
		||||
			.from("permissions")
 | 
			
		||||
			.where({
 | 
			
		||||
				username: this.username,
 | 
			
		||||
				name: name,
 | 
			
		||||
			})
 | 
			
		||||
			.delete();
 | 
			
		||||
		return r !== 0;
 | 
			
		||||
		const result = await this.kysely
 | 
			
		||||
			.deleteFrom("permissions")
 | 
			
		||||
			.where("username", "=", this.username)
 | 
			
		||||
			.where("name", "=", name)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return (result.numDeletedRows ?? 0n) > 0;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const createKnexUserController = (knex: Knex): UserAccessor => {
 | 
			
		||||
	const createUserKnex = async (input: UserCreateInput) => {
 | 
			
		||||
		if (undefined !== (await findUserKenx(input.username))) {
 | 
			
		||||
export const createSqliteUserController = (kysely = getKysely()): UserAccessor => {
 | 
			
		||||
	const createUser = async (input: UserCreateInput) => {
 | 
			
		||||
		if (undefined !== (await findUser(input.username))) {
 | 
			
		||||
			return undefined;
 | 
			
		||||
		}
 | 
			
		||||
		const user = new KnexUser(input.username, new Password(input.password), knex);
 | 
			
		||||
		await knex
 | 
			
		||||
			.insert<DBUser>({
 | 
			
		||||
				username: user.username,
 | 
			
		||||
				password_hash: user.password.hash,
 | 
			
		||||
				password_salt: user.password.salt,
 | 
			
		||||
			})
 | 
			
		||||
			.into("users");
 | 
			
		||||
		const user = new SqliteUser(input.username, new Password(input.password), kysely);
 | 
			
		||||
		await kysely
 | 
			
		||||
			.insertInto("users")
 | 
			
		||||
			.values({ username: user.username, password_hash: user.password.hash, password_salt: user.password.salt })
 | 
			
		||||
			.execute();
 | 
			
		||||
		return user;
 | 
			
		||||
	};
 | 
			
		||||
	const findUserKenx = async (id: string) => {
 | 
			
		||||
		let user: DBUser[] = await knex.select("*").from("users").where({ username: id });
 | 
			
		||||
		if (user.length == 0) return undefined;
 | 
			
		||||
		const first = user[0];
 | 
			
		||||
		return new KnexUser(first.username, new Password({ hash: first.password_hash, salt: first.password_salt }), knex);
 | 
			
		||||
	const findUser = async (id: string) => {
 | 
			
		||||
		const user = await kysely
 | 
			
		||||
			.selectFrom("users")
 | 
			
		||||
			.selectAll()
 | 
			
		||||
			.where("username", "=", id)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		if (!user) return undefined;
 | 
			
		||||
		if (!user.password_hash || !user.password_salt) {
 | 
			
		||||
			throw new Error("password hash or salt is missing");
 | 
			
		||||
		}
 | 
			
		||||
		if (user.username === null) {
 | 
			
		||||
			throw new Error("username is null");
 | 
			
		||||
		}
 | 
			
		||||
		return new SqliteUser(user.username, new Password({
 | 
			
		||||
			hash: user.password_hash,
 | 
			
		||||
			salt: user.password_salt
 | 
			
		||||
		}), kysely);
 | 
			
		||||
	};
 | 
			
		||||
	const delUserKnex = async (id: string) => {
 | 
			
		||||
		let r = await knex.delete().from("users").where({ username: id });
 | 
			
		||||
		return r === 0;
 | 
			
		||||
	const delUser = async (id: string) => {
 | 
			
		||||
		const result = await kysely.deleteFrom("users")
 | 
			
		||||
			.where("username", "=", id)
 | 
			
		||||
			.executeTakeFirst();
 | 
			
		||||
		return (result.numDeletedRows ?? 0n) > 0;
 | 
			
		||||
	};
 | 
			
		||||
	return {
 | 
			
		||||
		createUser: createUserKnex,
 | 
			
		||||
		findUser: findUserKenx,
 | 
			
		||||
		delUser: delUserKnex,
 | 
			
		||||
		createUser: createUser,
 | 
			
		||||
		findUser: findUser,
 | 
			
		||||
		delUser: delUser,
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
import { basename, dirname, join as pathjoin } from "path";
 | 
			
		||||
import { basename, dirname, join as pathjoin } from "node:path";
 | 
			
		||||
import { ContentFile, createContentFile } from "../content/mod";
 | 
			
		||||
import { Document, DocumentAccessor } from "../model/mod";
 | 
			
		||||
import type { Document, DocumentAccessor } from "../model/mod";
 | 
			
		||||
import { ContentList } from "./content_list";
 | 
			
		||||
import { IDiffWatcher } from "./watcher";
 | 
			
		||||
import type { IDiffWatcher } from "./watcher";
 | 
			
		||||
 | 
			
		||||
// refactoring needed.
 | 
			
		||||
export class ContentDiffHandler {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { ContentFile } from "../content/mod";
 | 
			
		||||
import type { ContentFile } from "../content/mod";
 | 
			
		||||
 | 
			
		||||
export class ContentList {
 | 
			
		||||
	/** path map */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import asyncPool from "tiny-async-pool";
 | 
			
		||||
import { DocumentAccessor } from "../model/doc";
 | 
			
		||||
import type { DocumentAccessor } from "../model/doc";
 | 
			
		||||
import { ContentDiffHandler } from "./content_handler";
 | 
			
		||||
import { IDiffWatcher } from "./watcher";
 | 
			
		||||
import type { IDiffWatcher } from "./watcher";
 | 
			
		||||
 | 
			
		||||
export class DiffManager {
 | 
			
		||||
	watching: { [content_type: string]: ContentDiffHandler };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
import Koa from "koa";
 | 
			
		||||
import type Koa from "koa";
 | 
			
		||||
import Router from "koa-router";
 | 
			
		||||
import { ContentFile } from "../content/mod";
 | 
			
		||||
import type { ContentFile } from "../content/mod";
 | 
			
		||||
import { AdminOnlyMiddleware } from "../permission/permission";
 | 
			
		||||
import { sendError } from "../route/error_handler";
 | 
			
		||||
import { DiffManager } from "./diff";
 | 
			
		||||
import type { DiffManager } from "./diff";
 | 
			
		||||
 | 
			
		||||
function content_file_to_return(x: ContentFile) {
 | 
			
		||||
	return { path: x.path, type: x.type };
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ type PostAddedBody = {
 | 
			
		|||
}[];
 | 
			
		||||
 | 
			
		||||
function checkPostAddedBody(body: any): body is PostAddedBody {
 | 
			
		||||
	if (body instanceof Array) {
 | 
			
		||||
	if (Array.isArray(body)) {
 | 
			
		||||
		return body.map((x) => "type" in x && "path" in x).every((x) => x);
 | 
			
		||||
	}
 | 
			
		||||
	return false;
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +54,7 @@ export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouter
 | 
			
		|||
		sendError(400, 'format exception: there is no "type"');
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	const t = reqbody["type"];
 | 
			
		||||
	const t = reqbody.type;
 | 
			
		||||
	if (typeof t !== "string") {
 | 
			
		||||
		sendError(400, 'format exception: invalid type of "type"');
 | 
			
		||||
		return;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
import event from "events";
 | 
			
		||||
import { FSWatcher, watch } from "fs";
 | 
			
		||||
import { promises } from "fs";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import { DocumentAccessor } from "../model/doc";
 | 
			
		||||
import type event from "node:events";
 | 
			
		||||
import { FSWatcher, watch } from "node:fs";
 | 
			
		||||
import { promises } from "node:fs";
 | 
			
		||||
import { join } from "node:path";
 | 
			
		||||
import type { DocumentAccessor } from "../model/doc";
 | 
			
		||||
 | 
			
		||||
const readdir = promises.readdir;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,3 @@
 | 
			
		|||
import { EventEmitter } from "events";
 | 
			
		||||
import { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import { DiffWatcherEvent, IDiffWatcher } from "../watcher";
 | 
			
		||||
import { ComicConfig } from "./ComicConfig";
 | 
			
		||||
import { WatcherCompositer } from "./compositer";
 | 
			
		||||
import { RecursiveWatcher } from "./recursive_watcher";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
import event from "events";
 | 
			
		||||
import { FSWatcher, promises, watch } from "fs";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import { DiffWatcherEvent, IDiffWatcher } from "../watcher";
 | 
			
		||||
import event from "node:events";
 | 
			
		||||
import { type FSWatcher, promises, watch } from "node:fs";
 | 
			
		||||
import { join } from "node:path";
 | 
			
		||||
import type { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import type { DiffWatcherEvent, IDiffWatcher } from "../watcher";
 | 
			
		||||
import { setupHelp } from "./util";
 | 
			
		||||
 | 
			
		||||
const { readdir } = promises;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { EventEmitter } from "events";
 | 
			
		||||
import { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher";
 | 
			
		||||
import { EventEmitter } from "node:events";
 | 
			
		||||
import type { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher";
 | 
			
		||||
 | 
			
		||||
export class WatcherCompositer extends EventEmitter implements IDiffWatcher {
 | 
			
		||||
	refWatchers: IDiffWatcher[];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,8 @@
 | 
			
		|||
import { FSWatcher, watch } from "chokidar";
 | 
			
		||||
import { EventEmitter } from "events";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import { DiffWatcherEvent, IDiffWatcher } from "../watcher";
 | 
			
		||||
import { setupHelp, setupRecursive } from "./util";
 | 
			
		||||
import { type FSWatcher, watch } from "chokidar";
 | 
			
		||||
import { EventEmitter } from "node:events";
 | 
			
		||||
import type { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import type { DiffWatcherEvent, IDiffWatcher } from "../watcher";
 | 
			
		||||
import { setupRecursive } from "./util";
 | 
			
		||||
 | 
			
		||||
type RecursiveWatcherOption = {
 | 
			
		||||
	/** @default true */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,13 @@
 | 
			
		|||
import { EventEmitter } from "events";
 | 
			
		||||
import { promises } from "fs";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import { promises } from "node:fs";
 | 
			
		||||
import { join } from "node:path";
 | 
			
		||||
const { readdir } = promises;
 | 
			
		||||
import { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import { IDiffWatcher } from "../watcher";
 | 
			
		||||
import type { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import type { IDiffWatcher } from "../watcher";
 | 
			
		||||
 | 
			
		||||
function setupCommon(watcher: IDiffWatcher, basepath: string, initial_filenames: string[], cur: string[]) {
 | 
			
		||||
	// Todo : reduce O(nm) to O(n+m) using hash map.
 | 
			
		||||
	let added = cur.filter((x) => !initial_filenames.includes(x));
 | 
			
		||||
	let deleted = initial_filenames.filter((x) => !cur.includes(x));
 | 
			
		||||
	const added = cur.filter((x) => !initial_filenames.includes(x));
 | 
			
		||||
	const deleted = initial_filenames.filter((x) => !cur.includes(x));
 | 
			
		||||
	for (const it of added) {
 | 
			
		||||
		const cpath = join(basepath, it);
 | 
			
		||||
		watcher.emit("create", cpath);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { EventEmitter } from "events";
 | 
			
		||||
import { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher";
 | 
			
		||||
import { EventEmitter } from "node:events";
 | 
			
		||||
import type { DocumentAccessor } from "../../model/doc";
 | 
			
		||||
import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher";
 | 
			
		||||
 | 
			
		||||
export class WatcherFilter extends EventEmitter implements IDiffWatcher {
 | 
			
		||||
	refWatcher: IDiffWatcher;
 | 
			
		||||
| 
						 | 
				
			
			@ -21,18 +21,16 @@ export class WatcherFilter extends EventEmitter implements IDiffWatcher {
 | 
			
		|||
			if (this.filter(prev)) {
 | 
			
		||||
				if (this.filter(cur)) {
 | 
			
		||||
					return super.emit("change", prev, cur);
 | 
			
		||||
				} else {
 | 
			
		||||
					return super.emit("delete", cur);
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
					return super.emit("delete", cur);
 | 
			
		||||
			}
 | 
			
		||||
				if (this.filter(cur)) {
 | 
			
		||||
					return super.emit("create", cur);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return false;
 | 
			
		||||
		} else if (!this.filter(arg[0])) {
 | 
			
		||||
		}if (!this.filter(arg[0])) {
 | 
			
		||||
			return false;
 | 
			
		||||
		} else return super.emit(event, ...arg);
 | 
			
		||||
		}return super.emit(event, ...arg);
 | 
			
		||||
	}
 | 
			
		||||
	constructor(refWatcher: IDiffWatcher, filter: (filename: string) => boolean) {
 | 
			
		||||
		super();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
import { request } from "http";
 | 
			
		||||
import { request } from "node:http";
 | 
			
		||||
import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken";
 | 
			
		||||
import Knex from "knex";
 | 
			
		||||
import Koa from "koa";
 | 
			
		||||
import type Koa from "koa";
 | 
			
		||||
import Router from "koa-router";
 | 
			
		||||
import { createKnexUserController } from "./db/mod";
 | 
			
		||||
import { IUser, UserAccessor } from "./model/mod";
 | 
			
		||||
import { createSqliteUserController } from "./db/mod";
 | 
			
		||||
import type { IUser, UserAccessor } from "./model/mod";
 | 
			
		||||
import { sendError } from "./route/error_handler";
 | 
			
		||||
import { get_setting } from "./SettingConfig";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ export type UserState = {
 | 
			
		|||
 | 
			
		||||
const isUserState = (obj: object | string): obj is PayloadInfo => {
 | 
			
		||||
	if (typeof obj === "string") return false;
 | 
			
		||||
	return "username" in obj && "permission" in obj && (obj as { permission: unknown }).permission instanceof Array;
 | 
			
		||||
	return "username" in obj && "permission" in obj && Array.isArray((obj as { permission: unknown }).permission);
 | 
			
		||||
};
 | 
			
		||||
type RefreshPayloadInfo = { username: string };
 | 
			
		||||
const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => {
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +57,7 @@ const publishRefreshToken = (secretKey: string, username: string, expiredtime: n
 | 
			
		|||
};
 | 
			
		||||
function setToken(ctx: Koa.Context, token_name: string, token_payload: string | null, expiredtime: number) {
 | 
			
		||||
	const setting = get_setting();
 | 
			
		||||
	if (token_payload === null && !!!ctx.cookies.get(token_name)) {
 | 
			
		||||
	if (token_payload === null && !ctx.cookies.get(token_name)) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	ctx.cookies.set(token_name, token_payload, {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,11 +72,11 @@ export const createLoginMiddleware = (userController: UserAccessor) => async (ct
 | 
			
		|||
	const secretKey = setting.jwt_secretkey;
 | 
			
		||||
	const body = ctx.request.body;
 | 
			
		||||
	// check format
 | 
			
		||||
	if (typeof body == "string" || !("username" in body) || !("password" in body)) {
 | 
			
		||||
	if (typeof body === "string" || !("username" in body) || !("password" in body)) {
 | 
			
		||||
		return sendError(400, "invalid form : username or password is not found in query.");
 | 
			
		||||
	}
 | 
			
		||||
	const username = body["username"];
 | 
			
		||||
	const password = body["password"];
 | 
			
		||||
	const username = body.username;
 | 
			
		||||
	const password = body.password;
 | 
			
		||||
	// check type
 | 
			
		||||
	if (typeof username !== "string" || typeof password !== "string") {
 | 
			
		||||
		return sendError(400, "invalid form : username or password is not string");
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +125,7 @@ export const createUserMiddleWare =
 | 
			
		|||
		const setGuest = async () => {
 | 
			
		||||
			setToken(ctx, accessTokenName, null, 0);
 | 
			
		||||
			setToken(ctx, refreshTokenName, null, 0);
 | 
			
		||||
			ctx.state["user"] = { username: "", permission: setting.guest };
 | 
			
		||||
			ctx.state.user = { username: "", permission: setting.guest };
 | 
			
		||||
			return await next();
 | 
			
		||||
		};
 | 
			
		||||
		return await refreshToken(ctx, setGuest, next);
 | 
			
		||||
| 
						 | 
				
			
			@ -134,7 +134,7 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai
 | 
			
		|||
	const accessPayload = ctx.cookies.get(accessTokenName);
 | 
			
		||||
	const setting = get_setting();
 | 
			
		||||
	const secretKey = setting.jwt_secretkey;
 | 
			
		||||
	if (accessPayload == undefined) {
 | 
			
		||||
	if (accessPayload === undefined) {
 | 
			
		||||
		return await checkRefreshAndUpdate();
 | 
			
		||||
	}
 | 
			
		||||
	try {
 | 
			
		||||
| 
						 | 
				
			
			@ -142,20 +142,19 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai
 | 
			
		|||
		if (isUserState(o)) {
 | 
			
		||||
			ctx.state.user = o;
 | 
			
		||||
			return await next();
 | 
			
		||||
		} else {
 | 
			
		||||
		}
 | 
			
		||||
			console.error("invalid token detected");
 | 
			
		||||
			throw new Error("token form invalid");
 | 
			
		||||
		}
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		if (e instanceof TokenExpiredError) {
 | 
			
		||||
			return await checkRefreshAndUpdate();
 | 
			
		||||
		} else throw e;
 | 
			
		||||
		}throw e;
 | 
			
		||||
	}
 | 
			
		||||
	async function checkRefreshAndUpdate() {
 | 
			
		||||
		const refreshPayload = ctx.cookies.get(refreshTokenName);
 | 
			
		||||
		if (refreshPayload === undefined) {
 | 
			
		||||
			return await fail(); // refresh token doesn't exist
 | 
			
		||||
		} else {
 | 
			
		||||
		}
 | 
			
		||||
			try {
 | 
			
		||||
				const o = verify(refreshPayload, secretKey);
 | 
			
		||||
				if (isRefreshToken(o)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -173,9 +172,8 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai
 | 
			
		|||
				if (e instanceof TokenExpiredError) {
 | 
			
		||||
					// refresh token is expired.
 | 
			
		||||
					return await fail();
 | 
			
		||||
				} else throw e;
 | 
			
		||||
				}throw e;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return await next();
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -205,9 +203,9 @@ export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.C
 | 
			
		|||
	if (typeof body !== "object" || !("username" in body) || !("oldpassword" in body) || !("newpassword" in body)) {
 | 
			
		||||
		return sendError(400, "request body is invalid format");
 | 
			
		||||
	}
 | 
			
		||||
	const username = body["username"];
 | 
			
		||||
	const oldpw = body["oldpassword"];
 | 
			
		||||
	const newpw = body["newpassword"];
 | 
			
		||||
	const username = body.username;
 | 
			
		||||
	const oldpw = body.oldpassword;
 | 
			
		||||
	const newpw = body.newpassword;
 | 
			
		||||
	if (typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string") {
 | 
			
		||||
		return sendError(400, "request body is invalid format");
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { JSONMap } from "../types/json";
 | 
			
		||||
import type { JSONMap } from "../types/json";
 | 
			
		||||
import { check_type } from "../util/type_check";
 | 
			
		||||
import { TagAccessor } from "./tag";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,7 @@ export interface DocumentBody {
 | 
			
		|||
	basepath: string;
 | 
			
		||||
	filename: string;
 | 
			
		||||
	modified_at: number;
 | 
			
		||||
	content_hash: string;
 | 
			
		||||
	content_hash: string | null;
 | 
			
		||||
	additional: JSONMap;
 | 
			
		||||
	tags: string[]; // eager loading
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -23,7 +23,7 @@ export const MetaContentBody = {
 | 
			
		|||
	tags: "string[]",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isDocBody = (c: any): c is DocumentBody => {
 | 
			
		||||
export const isDocBody = (c: unknown): c is DocumentBody => {
 | 
			
		||||
	return check_type<DocumentBody>(c, MetaContentBody);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,8 +33,9 @@ export interface Document extends DocumentBody {
 | 
			
		|||
	readonly deleted_at: number | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const isDoc = (c: any): c is Document => {
 | 
			
		||||
	if ("id" in c && typeof c["id"] === "number") {
 | 
			
		||||
export const isDoc = (c: unknown): c is Document => {
 | 
			
		||||
	if (typeof c !== "object" || c === null) return false;
 | 
			
		||||
	if ("id" in c && typeof c.id === "number") {
 | 
			
		||||
		const { id, ...rest } = c;
 | 
			
		||||
		return isDocBody(rest);
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ export interface TagCount {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export interface TagAccessor {
 | 
			
		||||
	getAllTagList: (onlyname?: boolean) => Promise<Tag[]>;
 | 
			
		||||
	getAllTagList: () => Promise<Tag[]>;
 | 
			
		||||
	getAllTagCount(): Promise<TagCount[]>;
 | 
			
		||||
	getTagByName: (name: string) => Promise<Tag | undefined>;
 | 
			
		||||
	addTag: (tag: Tag) => Promise<boolean>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { createHmac, randomBytes } from "crypto";
 | 
			
		||||
import { createHmac, randomBytes } from "node:crypto";
 | 
			
		||||
 | 
			
		||||
function hashForPassword(salt: string, password: string) {
 | 
			
		||||
	return createHmac("sha256", salt).update(password).digest("hex");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import Koa from "koa";
 | 
			
		||||
import { UserState } from "../login";
 | 
			
		||||
import type Koa from "koa";
 | 
			
		||||
import type { UserState } from "../login";
 | 
			
		||||
import { sendError } from "../route/error_handler";
 | 
			
		||||
 | 
			
		||||
export enum Permission {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +37,7 @@ export enum Permission {
 | 
			
		|||
export const createPermissionCheckMiddleware =
 | 
			
		||||
	(...permissions: string[]) =>
 | 
			
		||||
	async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
 | 
			
		||||
		const user = ctx.state["user"];
 | 
			
		||||
		const user = ctx.state.user;
 | 
			
		||||
		if (user.username === "admin") {
 | 
			
		||||
			return await next();
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -46,12 +46,12 @@ export const createPermissionCheckMiddleware =
 | 
			
		|||
		if (!permissions.map((p) => user_permission.includes(p)).every((x) => x)) {
 | 
			
		||||
			if (user.username === "") {
 | 
			
		||||
				return sendError(401, "you are guest. login needed.");
 | 
			
		||||
			} else return sendError(403, "do not have permission");
 | 
			
		||||
			}return sendError(403, "do not have permission");
 | 
			
		||||
		}
 | 
			
		||||
		await next();
 | 
			
		||||
	};
 | 
			
		||||
export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
 | 
			
		||||
	const user = ctx.state["user"];
 | 
			
		||||
	const user = ctx.state.user;
 | 
			
		||||
	if (user.username !== "admin") {
 | 
			
		||||
		return sendError(403, "admin only");
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
import { DefaultContext, Middleware, Next, ParameterizedContext } from "koa";
 | 
			
		||||
import type { DefaultContext, Middleware, Next, ParameterizedContext } from "koa";
 | 
			
		||||
import compose from "koa-compose";
 | 
			
		||||
import Router, { IParamMiddleware } from "koa-router";
 | 
			
		||||
import Router from "koa-router";
 | 
			
		||||
import ComicRouter from "./comic";
 | 
			
		||||
import { ContentContext } from "./context";
 | 
			
		||||
import type { ContentContext } from "./context";
 | 
			
		||||
import VideoRouter from "./video";
 | 
			
		||||
 | 
			
		||||
const table: { [s: string]: Router | undefined } = {
 | 
			
		||||
| 
						 | 
				
			
			@ -12,25 +12,26 @@ const table: { [s: string]: Router | undefined } = {
 | 
			
		|||
const all_middleware =
 | 
			
		||||
	(cont: string | undefined, restarg: string | undefined) =>
 | 
			
		||||
	async (ctx: ParameterizedContext<ContentContext, DefaultContext>, next: Next) => {
 | 
			
		||||
		if (cont == undefined) {
 | 
			
		||||
		if (cont === undefined) {
 | 
			
		||||
			ctx.status = 404;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		if (ctx.state.location.type != cont) {
 | 
			
		||||
		if (ctx.state.location.type !== cont) {
 | 
			
		||||
			console.error("not matched");
 | 
			
		||||
			ctx.status = 404;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		const router = table[cont];
 | 
			
		||||
		if (router == undefined) {
 | 
			
		||||
		if (router === undefined) {
 | 
			
		||||
			ctx.status = 404;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		const rest = "/" + (restarg ?? "");
 | 
			
		||||
		const rest = `/${restarg ?? ""}`;
 | 
			
		||||
		const result = router.match(rest, "GET");
 | 
			
		||||
		if (!result.route) {
 | 
			
		||||
			return await next();
 | 
			
		||||
		}
 | 
			
		||||
		// biome-ignore lint/suspicious/noExplicitAny: <explanation>
 | 
			
		||||
		const chain = result.pathAndMethod.reduce((combination: Middleware<any & DefaultContext, any>[], cur) => {
 | 
			
		||||
			combination.push(async (ctx, next) => {
 | 
			
		||||
				const captures = cur.captures(rest);
 | 
			
		||||
| 
						 | 
				
			
			@ -47,11 +48,11 @@ export class AllContentRouter extends Router<ContentContext> {
 | 
			
		|||
	constructor() {
 | 
			
		||||
		super();
 | 
			
		||||
		this.get("/:content_type", async (ctx, next) => {
 | 
			
		||||
			return await all_middleware(ctx.params["content_type"], undefined)(ctx, next);
 | 
			
		||||
			return await all_middleware(ctx.params.content_type, undefined)(ctx, next);
 | 
			
		||||
		});
 | 
			
		||||
		this.get("/:content_type/:rest(.*)", async (ctx, next) => {
 | 
			
		||||
			const cont = ctx.params["content_type"] as string;
 | 
			
		||||
			return await all_middleware(cont, ctx.params["rest"])(ctx, next);
 | 
			
		||||
			const cont = ctx.params.content_type as string;
 | 
			
		||||
			return await all_middleware(cont, ctx.params.rest)(ctx, next);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,14 @@
 | 
			
		|||
import { Context, DefaultContext, DefaultState, Next } from "koa";
 | 
			
		||||
import { type Context, DefaultContext, DefaultState, Next } from "koa";
 | 
			
		||||
import Router from "koa-router";
 | 
			
		||||
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip, ZipAsync } from "../util/zipwrap";
 | 
			
		||||
import { ContentContext } from "./context";
 | 
			
		||||
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip, type ZipAsync } from "../util/zipwrap";
 | 
			
		||||
import type { ContentContext } from "./context";
 | 
			
		||||
import { since_last_modified } from "./util";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * zip stream cache.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
let ZipStreamCache: { [path: string]: [ZipAsync, number] } = {};
 | 
			
		||||
const ZipStreamCache: { [path: string]: [ZipAsync, number] } = {};
 | 
			
		||||
 | 
			
		||||
async function acquireZip(path: string) {
 | 
			
		||||
	if (!(path in ZipStreamCache)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -16,12 +16,11 @@ async function acquireZip(path: string) {
 | 
			
		|||
		ZipStreamCache[path] = [ret, 1];
 | 
			
		||||
		// console.log(`acquire ${path} 1`);
 | 
			
		||||
		return ret;
 | 
			
		||||
	} else {
 | 
			
		||||
	}
 | 
			
		||||
		const [ret, refCount] = ZipStreamCache[path];
 | 
			
		||||
		ZipStreamCache[path] = [ret, refCount + 1];
 | 
			
		||||
		// console.log(`acquire ${path} ${refCount + 1}`);
 | 
			
		||||
		return ret;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function releaseZip(path: string) {
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +39,7 @@ function releaseZip(path: string) {
 | 
			
		|||
async function renderZipImage(ctx: Context, path: string, page: number) {
 | 
			
		||||
	const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"];
 | 
			
		||||
	// console.log(`opened ${page}`);
 | 
			
		||||
	let zip = await acquireZip(path);
 | 
			
		||||
	const zip = await acquireZip(path);
 | 
			
		||||
	const entries = (await entriesByNaturalOrder(zip)).filter((x) => {
 | 
			
		||||
		const ext = x.name.split(".").pop();
 | 
			
		||||
		return ext !== undefined && image_ext.includes(ext);
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +83,7 @@ export class ComicRouter extends Router<ContentContext> {
 | 
			
		|||
			await renderZipImage(ctx, ctx.state.location.path, 0);
 | 
			
		||||
		});
 | 
			
		||||
		this.get("/:page(\\d+)", async (ctx, next) => {
 | 
			
		||||
			const page = Number.parseInt(ctx.params["page"]);
 | 
			
		||||
			const page = Number.parseInt(ctx.params.page);
 | 
			
		||||
			await renderZipImage(ctx, ctx.state.location.path, page);
 | 
			
		||||
		});
 | 
			
		||||
		this.get("/thumbnail", async (ctx, next) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,22 +1,22 @@
 | 
			
		|||
import { Context, Next } from "koa";
 | 
			
		||||
import type { Context, Next } from "koa";
 | 
			
		||||
import Router from "koa-router";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import { Document, DocumentAccessor, isDocBody } from "../model/doc";
 | 
			
		||||
import { QueryListOption } from "../model/doc";
 | 
			
		||||
import { join } from "node:path";
 | 
			
		||||
import { type Document, type DocumentAccessor, isDocBody } from "../model/doc";
 | 
			
		||||
import type { QueryListOption } from "../model/doc";
 | 
			
		||||
import {
 | 
			
		||||
	AdminOnlyMiddleware as AdminOnly,
 | 
			
		||||
	createPermissionCheckMiddleware as PerCheck,
 | 
			
		||||
	Permission as Per,
 | 
			
		||||
} from "../permission/permission";
 | 
			
		||||
import { AllContentRouter } from "./all";
 | 
			
		||||
import { ContentLocation } from "./context";
 | 
			
		||||
import type { ContentLocation } from "./context";
 | 
			
		||||
import { sendError } from "./error_handler";
 | 
			
		||||
import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util";
 | 
			
		||||
 | 
			
		||||
const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
 | 
			
		||||
	const num = Number.parseInt(ctx.params["num"]);
 | 
			
		||||
	let document = await controller.findById(num, true);
 | 
			
		||||
	if (document == undefined) {
 | 
			
		||||
	const num = Number.parseInt(ctx.params.num);
 | 
			
		||||
	const document = await controller.findById(num, true);
 | 
			
		||||
	if (document === undefined) {
 | 
			
		||||
		return sendError(404, "document does not exist.");
 | 
			
		||||
	}
 | 
			
		||||
	ctx.body = document;
 | 
			
		||||
| 
						 | 
				
			
			@ -24,28 +24,22 @@ const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context,
 | 
			
		|||
	console.log(document.additional);
 | 
			
		||||
};
 | 
			
		||||
const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
 | 
			
		||||
	const num = Number.parseInt(ctx.params["num"]);
 | 
			
		||||
	let document = await controller.findById(num, true);
 | 
			
		||||
	if (document == undefined) {
 | 
			
		||||
	const num = Number.parseInt(ctx.params.num);
 | 
			
		||||
	const document = await controller.findById(num, true);
 | 
			
		||||
	if (document === undefined) {
 | 
			
		||||
		return sendError(404, "document does not exist.");
 | 
			
		||||
	}
 | 
			
		||||
	ctx.body = document.tags;
 | 
			
		||||
	ctx.type = "json";
 | 
			
		||||
};
 | 
			
		||||
const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
 | 
			
		||||
	let query_limit = ctx.query["limit"];
 | 
			
		||||
	let query_cursor = ctx.query["cursor"];
 | 
			
		||||
	let query_word = ctx.query["word"];
 | 
			
		||||
	let query_content_type = ctx.query["content_type"];
 | 
			
		||||
	let query_offset = ctx.query["offset"];
 | 
			
		||||
	let query_use_offset = ctx.query["use_offset"];
 | 
			
		||||
	if (
 | 
			
		||||
		query_limit instanceof Array ||
 | 
			
		||||
		query_cursor instanceof Array ||
 | 
			
		||||
		query_word instanceof Array ||
 | 
			
		||||
		query_content_type instanceof Array ||
 | 
			
		||||
		query_offset instanceof Array ||
 | 
			
		||||
		query_use_offset instanceof Array
 | 
			
		||||
	const query_limit = ctx.query.limit;
 | 
			
		||||
	const query_cursor = ctx.query.cursor;
 | 
			
		||||
	const query_word = ctx.query.word;
 | 
			
		||||
	const query_content_type = ctx.query.content_type;
 | 
			
		||||
	const query_offset = ctx.query.offset;
 | 
			
		||||
	const query_use_offset = ctx.query.use_offset;
 | 
			
		||||
	if (Array.isArray(query_limit) ||Array.isArray(query_cursor) ||Array.isArray(query_word) ||Array.isArray(query_content_type) ||Array.isArray(query_offset) ||Array.isArray(query_use_offset)
 | 
			
		||||
	) {
 | 
			
		||||
		return sendError(400, "paramter can not be array");
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -54,10 +48,10 @@ const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Contex
 | 
			
		|||
	const word = ParseQueryArgString(query_word);
 | 
			
		||||
	const content_type = ParseQueryArgString(query_content_type);
 | 
			
		||||
	const offset = ParseQueryNumber(query_offset);
 | 
			
		||||
	if (limit === NaN || cursor === NaN || offset === NaN) {
 | 
			
		||||
	if (Number.isNaN(limit) || Number.isNaN(cursor) || Number.isNaN(offset)) {
 | 
			
		||||
		return sendError(400, "parameter limit, cursor or offset is not a number");
 | 
			
		||||
	}
 | 
			
		||||
	const allow_tag = ParseQueryArray(ctx.query["allow_tag"]);
 | 
			
		||||
	const allow_tag = ParseQueryArray(ctx.query.allow_tag);
 | 
			
		||||
	const [ok, use_offset] = ParseQueryBoolean(query_use_offset);
 | 
			
		||||
	if (!ok) {
 | 
			
		||||
		return sendError(400, "use_offset must be true or false.");
 | 
			
		||||
| 
						 | 
				
			
			@ -72,12 +66,12 @@ const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Contex
 | 
			
		|||
		use_offset: use_offset,
 | 
			
		||||
		content_type: content_type,
 | 
			
		||||
	};
 | 
			
		||||
	let document = await controller.findList(option);
 | 
			
		||||
	const document = await controller.findList(option);
 | 
			
		||||
	ctx.body = document;
 | 
			
		||||
	ctx.type = "json";
 | 
			
		||||
};
 | 
			
		||||
const UpdateContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
 | 
			
		||||
	const num = Number.parseInt(ctx.params["num"]);
 | 
			
		||||
	const num = Number.parseInt(ctx.params.num);
 | 
			
		||||
 | 
			
		||||
	if (ctx.request.type !== "json") {
 | 
			
		||||
		return sendError(400, "update fail. invalid document type: it is not json.");
 | 
			
		||||
| 
						 | 
				
			
			@ -95,9 +89,9 @@ const UpdateContentHandler = (controller: DocumentAccessor) => async (ctx: Conte
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
 | 
			
		||||
	let tag_name = ctx.params["tag"];
 | 
			
		||||
	const num = Number.parseInt(ctx.params["num"]);
 | 
			
		||||
	if (typeof tag_name === undefined) {
 | 
			
		||||
	let tag_name = ctx.params.tag;
 | 
			
		||||
	const num = Number.parseInt(ctx.params.num);
 | 
			
		||||
	if (typeof tag_name === "undefined") {
 | 
			
		||||
		return sendError(400, "??? Unreachable");
 | 
			
		||||
	}
 | 
			
		||||
	tag_name = String(tag_name);
 | 
			
		||||
| 
						 | 
				
			
			@ -110,9 +104,9 @@ const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, nex
 | 
			
		|||
	ctx.type = "json";
 | 
			
		||||
};
 | 
			
		||||
const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
 | 
			
		||||
	let tag_name = ctx.params["tag"];
 | 
			
		||||
	const num = Number.parseInt(ctx.params["num"]);
 | 
			
		||||
	if (typeof tag_name === undefined) {
 | 
			
		||||
	let tag_name = ctx.params.tag;
 | 
			
		||||
	const num = Number.parseInt(ctx.params.num);
 | 
			
		||||
	if (typeof tag_name === "undefined") {
 | 
			
		||||
		return sendError(400, "?? Unreachable");
 | 
			
		||||
	}
 | 
			
		||||
	tag_name = String(tag_name);
 | 
			
		||||
| 
						 | 
				
			
			@ -125,22 +119,22 @@ const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, nex
 | 
			
		|||
	ctx.type = "json";
 | 
			
		||||
};
 | 
			
		||||
const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
 | 
			
		||||
	const num = Number.parseInt(ctx.params["num"]);
 | 
			
		||||
	const num = Number.parseInt(ctx.params.num);
 | 
			
		||||
	const r = await controller.del(num);
 | 
			
		||||
	ctx.body = JSON.stringify(r);
 | 
			
		||||
	ctx.type = "json";
 | 
			
		||||
};
 | 
			
		||||
const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
 | 
			
		||||
	const num = Number.parseInt(ctx.params["num"]);
 | 
			
		||||
	let document = await controller.findById(num, true);
 | 
			
		||||
	if (document == undefined) {
 | 
			
		||||
	const num = Number.parseInt(ctx.params.num);
 | 
			
		||||
	const document = await controller.findById(num, true);
 | 
			
		||||
	if (document === undefined) {
 | 
			
		||||
		return sendError(404, "document does not exist.");
 | 
			
		||||
	}
 | 
			
		||||
	if (document.deleted_at !== null) {
 | 
			
		||||
		return sendError(404, "document has been removed.");
 | 
			
		||||
	}
 | 
			
		||||
	const path = join(document.basepath, document.filename);
 | 
			
		||||
	ctx.state["location"] = {
 | 
			
		||||
	ctx.state.location = {
 | 
			
		||||
		path: path,
 | 
			
		||||
		type: document.content_type,
 | 
			
		||||
		additional: document.additional,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { Context, Next } from "koa";
 | 
			
		||||
import type { Context, Next } from "koa";
 | 
			
		||||
 | 
			
		||||
export interface ErrorFormat {
 | 
			
		||||
	code: number;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,13 @@
 | 
			
		|||
import { Context, Next } from "koa";
 | 
			
		||||
import Router, { RouterContext } from "koa-router";
 | 
			
		||||
import { TagAccessor } from "../model/tag";
 | 
			
		||||
import { type Context, Next } from "koa";
 | 
			
		||||
import Router, { type RouterContext } from "koa-router";
 | 
			
		||||
import type { TagAccessor } from "../model/tag";
 | 
			
		||||
import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission";
 | 
			
		||||
import { sendError } from "./error_handler";
 | 
			
		||||
 | 
			
		||||
export function getTagRounter(tagController: TagAccessor) {
 | 
			
		||||
	let router = new Router();
 | 
			
		||||
	const router = new Router();
 | 
			
		||||
	router.get("/", PerCheck(Permission.QueryContent), async (ctx: Context) => {
 | 
			
		||||
		if (ctx.query["withCount"]) {
 | 
			
		||||
		if (ctx.query.withCount) {
 | 
			
		||||
			const c = await tagController.getAllTagCount();
 | 
			
		||||
			ctx.body = c;
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ export function getTagRounter(tagController: TagAccessor) {
 | 
			
		|||
		ctx.type = "json";
 | 
			
		||||
	});
 | 
			
		||||
	router.get("/:tag_name", PerCheck(Permission.QueryContent), async (ctx: RouterContext) => {
 | 
			
		||||
		const tag_name = ctx.params["tag_name"];
 | 
			
		||||
		const tag_name = ctx.params.tag_name;
 | 
			
		||||
		const c = await tagController.getTagByName(tag_name);
 | 
			
		||||
		if (!c) {
 | 
			
		||||
			sendError(404, "tags not found");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,13 @@
 | 
			
		|||
import { Context } from "koa";
 | 
			
		||||
import type { Context } from "koa";
 | 
			
		||||
 | 
			
		||||
export function ParseQueryNumber(s: string[] | string | undefined): number | undefined {
 | 
			
		||||
	if (s === undefined) return undefined;
 | 
			
		||||
	else if (typeof s === "object") return undefined;
 | 
			
		||||
	else return Number.parseInt(s);
 | 
			
		||||
	if (typeof s === "object") return undefined;
 | 
			
		||||
	return Number.parseInt(s);
 | 
			
		||||
}
 | 
			
		||||
export function ParseQueryArray(s: string[] | string | undefined) {
 | 
			
		||||
	s = s ?? [];
 | 
			
		||||
	const r = s instanceof Array ? s : [s];
 | 
			
		||||
	const input = s ?? [];
 | 
			
		||||
	const r = Array.isArray(input) ? input : [input];
 | 
			
		||||
	return r.map((x) => decodeURIComponent(x));
 | 
			
		||||
}
 | 
			
		||||
export function ParseQueryArgString(s: string[] | string | undefined) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { createReadStream, promises } from "fs";
 | 
			
		||||
import { Context } from "koa";
 | 
			
		||||
import { createReadStream, promises } from "node:fs";
 | 
			
		||||
import type { Context } from "koa";
 | 
			
		||||
import Router from "koa-router";
 | 
			
		||||
import { ContentContext } from "./context";
 | 
			
		||||
import type { ContentContext } from "./context";
 | 
			
		||||
 | 
			
		||||
export async function renderVideo(ctx: Context, path: string) {
 | 
			
		||||
	const ext = path.trim().split(".").pop();
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +27,7 @@ export async function renderVideo(ctx: Context, path: string) {
 | 
			
		|||
		}
 | 
			
		||||
		ctx.status = 200;
 | 
			
		||||
		ctx.length = stat.size;
 | 
			
		||||
		let stream = createReadStream(path);
 | 
			
		||||
		const stream = createReadStream(path);
 | 
			
		||||
		ctx.body = stream;
 | 
			
		||||
	} else {
 | 
			
		||||
		const m = range_text.match(/^bytes=(\d+)-(\d*)/);
 | 
			
		||||
| 
						 | 
				
			
			@ -35,8 +35,8 @@ export async function renderVideo(ctx: Context, path: string) {
 | 
			
		|||
			ctx.status = 416;
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		start = parseInt(m[1]);
 | 
			
		||||
		end = m[2].length > 0 ? parseInt(m[2]) : start + 1024 * 1024;
 | 
			
		||||
		start = Number.parseInt(m[1]);
 | 
			
		||||
		end = m[2].length > 0 ? Number.parseInt(m[2]) : start + 1024 * 1024;
 | 
			
		||||
		end = Math.min(end, stat.size - 1);
 | 
			
		||||
		if (start > end) {
 | 
			
		||||
			ctx.status = 416;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,18 +5,21 @@ import { connectDB } from "./database";
 | 
			
		|||
import { createDiffRouter, DiffManager } from "./diff/mod";
 | 
			
		||||
import { get_setting, SettingConfig } from "./SettingConfig";
 | 
			
		||||
 | 
			
		||||
import { createReadStream, readFileSync } from "fs";
 | 
			
		||||
import { createReadStream, readFileSync } from "node:fs";
 | 
			
		||||
import bodyparser from "koa-bodyparser";
 | 
			
		||||
import { createKnexDocumentAccessor, createKnexTagController, createKnexUserController } from "./db/mod";
 | 
			
		||||
import { createSqliteDocumentAccessor, createSqliteTagController, createSqliteUserController } from "./db/mod";
 | 
			
		||||
import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login";
 | 
			
		||||
import getContentRouter from "./route/contents";
 | 
			
		||||
import { error_handler } from "./route/error_handler";
 | 
			
		||||
 | 
			
		||||
import { createInterface as createReadlineInterface } from "readline";
 | 
			
		||||
import { createInterface as createReadlineInterface } from "node:readline";
 | 
			
		||||
import { createComicWatcher } from "./diff/watcher/comic_watcher";
 | 
			
		||||
import { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod";
 | 
			
		||||
import type { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod";
 | 
			
		||||
import { getTagRounter } from "./route/tags";
 | 
			
		||||
 | 
			
		||||
import { config } from "dotenv";
 | 
			
		||||
config();
 | 
			
		||||
 | 
			
		||||
class ServerApplication {
 | 
			
		||||
	readonly userController: UserAccessor;
 | 
			
		||||
	readonly documentController: DocumentAccessor;
 | 
			
		||||
| 
						 | 
				
			
			@ -61,12 +64,12 @@ class ServerApplication {
 | 
			
		|||
		app.use(error_handler);
 | 
			
		||||
		app.use(createUserMiddleWare(this.userController));
 | 
			
		||||
 | 
			
		||||
		let diff_router = createDiffRouter(this.diffManger);
 | 
			
		||||
		const diff_router = createDiffRouter(this.diffManger);
 | 
			
		||||
		this.diffManger.register("comic", createComicWatcher());
 | 
			
		||||
 | 
			
		||||
		console.log("setup router");
 | 
			
		||||
 | 
			
		||||
		let router = new Router();
 | 
			
		||||
		const router = new Router();
 | 
			
		||||
		router.use("/api/(.*)", async (ctx, next) => {
 | 
			
		||||
			// For CORS
 | 
			
		||||
			ctx.res.setHeader("access-control-allow-origin", "*");
 | 
			
		||||
| 
						 | 
				
			
			@ -92,12 +95,12 @@ class ServerApplication {
 | 
			
		|||
		router.use("/user", login_router.routes());
 | 
			
		||||
		router.use("/user", login_router.allowedMethods());
 | 
			
		||||
 | 
			
		||||
		if (setting.mode == "development") {
 | 
			
		||||
		if (setting.mode === "development") {
 | 
			
		||||
			let mm_count = 0;
 | 
			
		||||
			app.use(async (ctx, next) => {
 | 
			
		||||
				console.log(`==========================${mm_count++}`);
 | 
			
		||||
				const ip = ctx.get("X-Real-IP") ?? ctx.ip;
 | 
			
		||||
				const fromClient = ctx.state["user"].username === "" ? ip : ctx.state["user"].username;
 | 
			
		||||
				const fromClient = ctx.state.user.username === "" ? ip : ctx.state.user.username;
 | 
			
		||||
				console.log(`${fromClient} : ${ctx.method} ${ctx.url}`);
 | 
			
		||||
				await next();
 | 
			
		||||
				// console.log(`404`);
 | 
			
		||||
| 
						 | 
				
			
			@ -132,8 +135,9 @@ class ServerApplication {
 | 
			
		|||
	}
 | 
			
		||||
	private serve_with_meta_index(router: Router) {
 | 
			
		||||
		const DocMiddleware = async (ctx: Koa.ParameterizedContext) => {
 | 
			
		||||
			const docId = Number.parseInt(ctx.params["id"]);
 | 
			
		||||
			const docId = Number.parseInt(ctx.params.id);
 | 
			
		||||
			const doc = await this.documentController.findById(docId, true);
 | 
			
		||||
			// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
 | 
			
		||||
			let meta;
 | 
			
		||||
			if (doc === undefined) {
 | 
			
		||||
				ctx.status = 404;
 | 
			
		||||
| 
						 | 
				
			
			@ -190,7 +194,7 @@ class ServerApplication {
 | 
			
		|||
	}
 | 
			
		||||
	private serve_static_file(router: Router) {
 | 
			
		||||
		const static_file_server = (path: string, type: string) => {
 | 
			
		||||
			router.get("/" + path, async (ctx, next) => {
 | 
			
		||||
			router.get(`/${path}`, async (ctx, next) => {
 | 
			
		||||
				const setting = get_setting();
 | 
			
		||||
				ctx.type = type;
 | 
			
		||||
				ctx.body = createReadStream(path);
 | 
			
		||||
| 
						 | 
				
			
			@ -211,19 +215,19 @@ class ServerApplication {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
	start_server() {
 | 
			
		||||
		let setting = get_setting();
 | 
			
		||||
		const setting = get_setting();
 | 
			
		||||
		// todo : support https
 | 
			
		||||
		console.log(`listen on http://${setting.localmode ? "localhost" : "0.0.0.0"}:${setting.port}`);
 | 
			
		||||
		return this.app.listen(setting.port, setting.localmode ? "127.0.0.1" : "0.0.0.0");
 | 
			
		||||
	}
 | 
			
		||||
	static async createServer() {
 | 
			
		||||
		const setting = get_setting();
 | 
			
		||||
		let db = await connectDB();
 | 
			
		||||
		const db = await connectDB();
 | 
			
		||||
 | 
			
		||||
		const app = new ServerApplication({
 | 
			
		||||
			userController: createKnexUserController(db),
 | 
			
		||||
			documentController: createKnexDocumentAccessor(db),
 | 
			
		||||
			tagController: createKnexTagController(db),
 | 
			
		||||
			userController: createSqliteUserController(db),
 | 
			
		||||
			documentController: createSqliteDocumentAccessor(db),
 | 
			
		||||
			tagController: createSqliteTagController(db),
 | 
			
		||||
		});
 | 
			
		||||
		await app.setup();
 | 
			
		||||
		return app;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										34
									
								
								packages/server/src/types/db.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								packages/server/src/types/db.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,34 +0,0 @@
 | 
			
		|||
import { Knex } from "knex";
 | 
			
		||||
 | 
			
		||||
declare module "knex" {
 | 
			
		||||
	interface Tables {
 | 
			
		||||
		tags: {
 | 
			
		||||
			name: string;
 | 
			
		||||
			description?: string;
 | 
			
		||||
		};
 | 
			
		||||
		users: {
 | 
			
		||||
			username: string;
 | 
			
		||||
			password_hash: string;
 | 
			
		||||
			password_salt: string;
 | 
			
		||||
		};
 | 
			
		||||
		document: {
 | 
			
		||||
			id: number;
 | 
			
		||||
			title: string;
 | 
			
		||||
			content_type: string;
 | 
			
		||||
			basepath: string;
 | 
			
		||||
			filename: string;
 | 
			
		||||
			created_at: number;
 | 
			
		||||
			deleted_at: number | null;
 | 
			
		||||
			content_hash: string;
 | 
			
		||||
			additional: string | null;
 | 
			
		||||
		};
 | 
			
		||||
		doc_tag_relation: {
 | 
			
		||||
			doc_id: number;
 | 
			
		||||
			tag_name: string;
 | 
			
		||||
		};
 | 
			
		||||
		permissions: {
 | 
			
		||||
			username: string;
 | 
			
		||||
			name: string;
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { existsSync, promises as fs, readFileSync, writeFileSync } from "fs";
 | 
			
		||||
import { existsSync, promises as fs, readFileSync, writeFileSync } from "node:fs";
 | 
			
		||||
import { validate } from "jsonschema";
 | 
			
		||||
 | 
			
		||||
export class ConfigManager<T> {
 | 
			
		||||
export class ConfigManager<T extends object> {
 | 
			
		||||
	path: string;
 | 
			
		||||
	default_config: T;
 | 
			
		||||
	config: T | null;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,15 @@
 | 
			
		|||
export function check_type<T>(obj: any, check_proto: Record<string, string | undefined>): obj is T {
 | 
			
		||||
export function check_type<T>(obj: unknown, check_proto: Record<string, string | undefined>): obj is T {
 | 
			
		||||
	if (typeof obj !== "object" || obj === null) return false;
 | 
			
		||||
	for (const it in check_proto) {
 | 
			
		||||
		let defined = check_proto[it];
 | 
			
		||||
		if (defined === undefined) return false;
 | 
			
		||||
		defined = defined.trim();
 | 
			
		||||
		if (defined.endsWith("[]")) {
 | 
			
		||||
			if (!(obj[it] instanceof Array)) {
 | 
			
		||||
			if (!Array.isArray((obj as Record<string, unknown>)[it])) {
 | 
			
		||||
				return false;
 | 
			
		||||
			}
 | 
			
		||||
		} else if (defined !== typeof obj[it]) {
 | 
			
		||||
		// biome-ignore lint/suspicious/useValidTypeof: <explanation>
 | 
			
		||||
		} else if (defined !== typeof (obj as Record<string, unknown>)[it]) {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { ZipEntry } from "node-stream-zip";
 | 
			
		||||
import type { ZipEntry } from "node-stream-zip";
 | 
			
		||||
 | 
			
		||||
import { ReadStream } from "fs";
 | 
			
		||||
import { ReadStream } from "node:fs";
 | 
			
		||||
import { orderBy } from "natural-orderby";
 | 
			
		||||
import StreamZip from "node-stream-zip";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
    // "incremental": true,                   /* Enable incremental compilation */
 | 
			
		||||
    "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
 | 
			
		||||
    "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
 | 
			
		||||
    "lib": ["DOM", "ES6"], /* Specify library files to be included in the compilation. */
 | 
			
		||||
    "lib": ["DOM", "ESNext"],
 | 
			
		||||
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
 | 
			
		||||
    // "checkJs": true,                       /* Report errors in .js files. */
 | 
			
		||||
    "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										709
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										709
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Loading…
	
	Add table
		
		Reference in a new issue