Rework #6

Merged
monoid merged 38 commits from dev into main 2024-04-17 01:45:37 +09:00
47 changed files with 1236 additions and 747 deletions
Showing only changes of commit 04ce1306b7 - Show all commits

View File

@ -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"
]
}

View File

@ -34,7 +34,7 @@ export interface SchemaMigration {
export interface Tags { export interface Tags {
description: string | null; description: string | null;
name: string | null; name: string;
} }
export interface Users { export interface Users {

View File

@ -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 { create_server } from "./src/server";
import { get_setting } from "./src/SettingConfig";
function registerChannel(cntr: UserAccessor) { create_server().then((server) => {
ipcMain.handle("reset_password", async (event, username: string, password: string) => { server.start_server();
const user = await cntr.findUser(username); }).catch((err) => {
if (user === undefined) { console.error(err);
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>`;

View File

@ -0,0 +1,3 @@
{
"watch": []
}

View File

@ -1,5 +0,0 @@
require("ts-node").register();
const { Knex } = require("./src/config");
// Update with your config settings.
module.exports = Knex.config;

View File

@ -4,10 +4,8 @@
"description": "", "description": "",
"main": "build/app.js", "main": "build/app.js",
"scripts": { "scripts": {
"compile": "tsc", "compile": "swc src --out-dir dist",
"compile:watch": "tsc -w", "dev": "nodemon -r @swc-node/register --exec node app.ts",
"build": "cd src/client && pnpm run build:prod",
"build:watch": "cd src/client && pnpm run build:watch",
"start": "node build/app.js" "start": "node build/app.js"
}, },
"author": "", "author": "",
@ -16,6 +14,7 @@
"@zip.js/zip.js": "^2.7.40", "@zip.js/zip.js": "^2.7.40",
"better-sqlite3": "^9.4.3", "better-sqlite3": "^9.4.3",
"chokidar": "^3.6.0", "chokidar": "^3.6.0",
"dotenv": "^16.4.5",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.15.2", "koa": "^2.15.2",
"koa-bodyparser": "^4.4.1", "koa-bodyparser": "^4.4.1",
@ -26,13 +25,18 @@
"tiny-async-pool": "^1.3.0" "tiny-async-pool": "^1.3.0"
}, },
"devDependencies": { "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/jsonwebtoken": "^8.5.9",
"@types/koa": "^2.15.0", "@types/koa": "^2.15.0",
"@types/koa-bodyparser": "^4.3.12", "@types/koa-bodyparser": "^4.3.12",
"@types/koa-compose": "^3.2.8", "@types/koa-compose": "^3.2.8",
"@types/koa-router": "^7.4.8", "@types/koa-router": "^7.4.8",
"@types/node": "^14.18.63", "@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"
} }
} }

View File

@ -1,6 +1,6 @@
import { randomBytes } from "crypto"; import { randomBytes } from "node:crypto";
import { existsSync, readFileSync, writeFileSync } from "fs"; import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { Permission } from "./permission/permission"; import type { Permission } from "./permission/permission";
export interface SettingConfig { export interface SettingConfig {
/** /**
@ -46,6 +46,7 @@ const default_setting: SettingConfig = {
}; };
let setting: null | SettingConfig = null; let setting: null | SettingConfig = null;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const setEmptyToDefault = (target: any, default_table: SettingConfig) => { const setEmptyToDefault = (target: any, default_table: SettingConfig) => {
let diff_occur = false; let diff_occur = false;
for (const key in default_table) { for (const key in default_table) {
@ -59,7 +60,7 @@ const setEmptyToDefault = (target: any, default_table: SettingConfig) => {
}; };
export const read_setting_from_file = () => { 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); const partial_occur = setEmptyToDefault(ret, default_setting);
if (partial_occur) { if (partial_occur) {
writeFileSync("settings.json", JSON.stringify(ret)); writeFileSync("settings.json", JSON.stringify(ret));
@ -70,7 +71,7 @@ export function get_setting(): SettingConfig {
if (setting === null) { if (setting === null) {
setting = read_setting_from_file(); setting = read_setting_from_file();
const env = process.env.NODE_ENV; 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"'); throw new Error('process unknown value in NODE_ENV: must be either "development" or "production"');
} }
setting.mode = env ?? setting.mode; setting.mode = env ?? setting.mode;

View File

@ -1,4 +1,4 @@
import { Knex as k } from "knex"; import type { Knex as k } from "knex";
export namespace Knex { export namespace Knex {
export const config: { export const config: {

View File

@ -1,7 +1,7 @@
import { extname } from "path"; import { extname } from "node:path";
import { DocumentBody } from "../model/doc"; import type { DocumentBody } from "../model/doc";
import { readAllFromZip, readZip } from "../util/zipwrap"; 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"; type ComicType = "doujinshi" | "artist cg" | "manga" | "western";
interface ComicDesc { 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.character?.map((x) => `character:${x}`) ?? []);
tags = tags.concat(this.desc.group?.map((x) => `group:${x}`) ?? []); tags = tags.concat(this.desc.group?.map((x) => `group:${x}`) ?? []);
tags = tags.concat(this.desc.series?.map((x) => `series:${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}`); tags.push(`type:${type}`);
return { return {
...basebody, ...basebody,

View File

@ -1,10 +1,9 @@
import { createHash } from "crypto"; import { createHash } from "node:crypto";
import { promises, Stats } from "fs"; import { promises, type Stats } from "node:fs";
import { Context, DefaultContext, DefaultState, Middleware, Next } from "koa"; import { Context, DefaultContext, DefaultState, Middleware, Next } from "koa";
import Router from "koa-router"; import Router from "koa-router";
import { extname } from "path"; import path, { extname } from "node:path";
import path from "path"; import type { DocumentBody } from "../model/mod";
import { DocumentBody } from "../model/mod";
/** /**
* content file or directory referrer * content file or directory referrer
*/ */
@ -24,13 +23,17 @@ type ContentFileConstructor = (new (
content_type: string; content_type: string;
}; };
export const createDefaultClass = (type: string): ContentFileConstructor => { export const createDefaultClass = (type: string): ContentFileConstructor => {
let cons = class implements ContentFile { const cons = class implements ContentFile {
readonly path: string; readonly path: string;
// type = type; // type = type;
static content_type = type; static content_type = type;
protected hash: string | undefined; protected hash: string | undefined;
protected stat: Stats | undefined; protected stat: Stats | undefined;
protected getStat(){
return this.stat;
}
constructor(path: string, option?: ContentConstructOption) { constructor(path: string, option?: ContentConstructOption) {
this.path = path; this.path = path;
this.hash = option?.hash; this.hash = option?.hash;
@ -67,14 +70,17 @@ export const createDefaultClass = (type: string): ContentFileConstructor => {
return this.hash; return this.hash;
} }
async getMtime(): Promise<number> { 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(); 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; return cons;
}; };
let ContstructorTable: { [k: string]: ContentFileConstructor } = {}; const ContstructorTable: { [k: string]: ContentFileConstructor } = {};
export function registerContentReferrer(s: ContentFileConstructor) { export function registerContentReferrer(s: ContentFileConstructor) {
console.log(`registered content type: ${s.content_type}`); console.log(`registered content type: ${s.content_type}`);
ContstructorTable[s.content_type] = s; ContstructorTable[s.content_type] = s;

View File

@ -1,9 +1,6 @@
import { ContentConstructOption, ContentFile, registerContentReferrer } from "./file"; import { registerContentReferrer } from "./file";
import { createDefaultClass } from "./file"; import { createDefaultClass } from "./file";
export class VideoReferrer extends createDefaultClass("video") { export class VideoReferrer extends createDefaultClass("video") {
constructor(path: string, desc?: ContentConstructOption) {
super(path, desc);
}
} }
registerContentReferrer(VideoReferrer); registerContentReferrer(VideoReferrer);

View File

@ -1,47 +1,26 @@
import { existsSync } from "fs"; import { existsSync } from "node:fs";
import Knex from "knex";
import { Knex as KnexConfig } from "./config";
import { get_setting } from "./SettingConfig"; import { get_setting } from "./SettingConfig";
import { getKysely } from "./db/kysely";
export async function connectDB() { export async function connectDB() {
const env = get_setting().mode; const kysely = getKysely();
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);
let tries = 0; let tries = 0;
for (;;) { for (;;) {
try { try {
console.log("try to connect db"); 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"); console.log("connect success");
} catch (err) { } catch (err) {
if (tries < 3) { if (tries < 3) {
tries++; tries++;
console.error(`connection fail ${err} retry...`); console.error(`connection fail ${err} retry...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
continue; continue;
} else {
throw err;
} }
throw err;
} }
break; break;
} }
if (init_need) { return kysely;
console.log("first execute: initialize database...");
const migrate = await import("../migrations/initial");
await migrate.up(knex);
}
return knex;
} }

View File

@ -1,235 +1,222 @@
import { Knex } from "knex"; import { getKysely } from "./kysely";
import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../model/doc"; import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { TagAccessor } from "../model/tag"; import type { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../model/doc";
import { createKnexTagController } from "./tag"; import { ParseJSONResultsPlugin, type NotNull } from "kysely";
import { MyParseJSONResultsPlugin } from "./plugin";
export type DBTagContentRelation = { export type DBTagContentRelation = {
doc_id: number; doc_id: number;
tag_name: string; tag_name: string;
}; };
class KnexDocumentAccessor implements DocumentAccessor { class SqliteDocumentAccessor implements DocumentAccessor {
knex: Knex; constructor(private kysely = getKysely()) {
tagController: TagAccessor;
constructor(knex: Knex) {
this.knex = knex;
this.tagController = createKnexTagController(knex);
} }
async search(search_word: string): Promise<Document[]> { async search(search_word: string): Promise<Document[]> {
throw new Error("Method not implemented."); 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[]> { async addList(content_list: DocumentBody[]): Promise<number[]> {
return await this.knex.transaction(async (trx) => { return await this.kysely.transaction().execute(async (trx) => {
// add tags // add tags
const tagCollected = new Set<string>(); 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) { for (const content of content_list) {
const { tags, additional, ...rest } = content; for (const tag of content.tags) {
const id_lst = await trx tagCollected.add(tag);
.insert({ }
}
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), additional: JSON.stringify(additional),
created_at: Date.now(), created_at: Date.now(),
...rest, ...rest,
}) };
.into("document"); }))
const id = id_lst[0]; .returning("id")
if (tags.length > 0) { .execute();
await trx const id_lst = ids.map((x) => x.id);
.insert(
tags.map((y) => ({ const doc_tags = content_list.flatMap((content, index) => {
doc_id: id, const { tags, ...rest } = content;
tag_name: y, return tags.map((tag) => ({ doc_id: id_lst[index], tag_name: tag }));
})), });
) await trx.insertInto("doc_tag_relation")
.into("doc_tag_relation"); .values(doc_tags)
} .execute();
ret.push(id); return id_lst;
}
return ret;
}); });
} }
async add(c: DocumentBody) { async add(c: DocumentBody) {
const { tags, additional, ...rest } = c; return await this.kysely.transaction().execute(async (trx) => {
const id_lst = await this.knex const { tags, additional, ...rest } = c;
.insert({ const id_lst = await trx.insertInto("document").values({
additional: JSON.stringify(additional), additional: JSON.stringify(additional),
created_at: Date.now(), created_at: Date.now(),
...rest, ...rest,
}) })
.into("document"); .returning("id")
const id = id_lst[0]; .executeTakeFirst() as { id: number };
for (const it of tags) { const id = id_lst.id;
this.tagController.addTag({ name: it });
} // add tags
if (tags.length > 0) { await trx.insertInto("tags")
await this.knex .values(tags.map((x) => ({ name: x })))
.insert<DBTagContentRelation>(tags.map((x) => ({ doc_id: id, tag_name: x }))) // on conflict is supported in sqlite and postgresql.
.into("doc_tag_relation"); .onConflict((oc) => oc.doNothing());
}
return id; 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) { async del(id: number) {
if ((await this.findById(id)) !== undefined) { // delete tags
await this.knex.delete().from("doc_tag_relation").where({ doc_id: id }); await this.kysely
await this.knex.delete().from("document").where({ id: id }); .deleteFrom("doc_tag_relation")
return true; .where("doc_id", "=", id)
} .execute();
return false; // 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> { async findById(id: number, tagload?: boolean): Promise<Document | undefined> {
const s = await this.knex.select("*").from("document").where({ id: id }); const doc = await this.kysely.selectFrom("document")
if (s.length === 0) return undefined; .selectAll()
const first = s[0]; .where("id", "=", id)
let ret_tags: string[] = []; .$if(tagload ?? false, (qb) =>
if (tagload === true) { qb.select(eb => jsonArrayFrom(
const tags: DBTagContentRelation[] = await this.knex eb.selectFrom("doc_tag_relation")
.select("*") .select(["doc_tag_relation.tag_name"])
.from("doc_tag_relation") .whereRef("document.id", "=", "doc_tag_relation.doc_id")
.where({ doc_id: first.id }); .select("tag_name")
ret_tags = tags.map((x) => x.tag_name); ).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags"))
} )
.executeTakeFirst();
if (!doc) return undefined;
return { return {
...first, ...doc,
tags: ret_tags, content_hash: doc.content_hash ?? "",
additional: first.additional !== null ? JSON.parse(first.additional) : {}, additional: doc.additional !== null ? JSON.parse(doc.additional) : {},
tags: doc.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [],
}; };
} }
async findDeleted(content_type: string) { async findDeleted(content_type: string) {
const s = await this.knex const docs = await this.kysely
.select("*") .selectFrom("document")
.where({ content_type: content_type }) .selectAll()
.whereNotNull("update_at") .where("content_type", "=", content_type)
.from("document"); .where("deleted_at", "is not", null)
return s.map((x) => ({ .$narrowType<{ deleted_at: NotNull }>()
.execute();
return docs.map((x) => ({
...x, ...x,
tags: [], tags: [],
content_hash: x.content_hash ?? "",
additional: {}, additional: {},
})); }));
} }
async findList(option?: QueryListOption) { async findList(option?: QueryListOption) {
option = option ?? {}; const {
const allow_tag = option.allow_tag ?? []; allow_tag = [],
const eager_loading = option.eager_loading ?? true; eager_loading = true,
const limit = option.limit ?? 20; limit = 20,
const use_offset = option.use_offset ?? false; use_offset = false,
const offset = option.offset ?? 0; offset = 0,
const word = option.word; word,
const content_type = option.content_type; content_type,
const cursor = option.cursor; cursor,
} = option ?? {};
const buildquery = () => { const result = await this.kysely
let query = this.knex.select("document.*"); .selectFrom("document")
if (allow_tag.length > 0) { .selectAll()
query = query.from("doc_tag_relation as tags_0"); .$if(allow_tag.length > 0, (qb) => {
query = query.where("tags_0.tag_name", "=", allow_tag[0]); return allow_tag.reduce((prevQb ,tag, index) => {
for (let index = 1; index < allow_tag.length; index++) { return prevQb.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.tag_name`, "document.id")
const element = allow_tag[index]; .where(`tags_${index}.tag_name`, "=", tag);
query = query.innerJoin(`doc_tag_relation as tags_${index}`, `tags_${index}.doc_id`, "tags_0.doc_id"); }, qb) as unknown as typeof qb;
query = query.where(`tags_${index}.tag_name`, "=", element); })
} .$if(word !== undefined, (qb) => qb.where("title", "like", `%${word}%`))
query = query.innerJoin("document", "tags_0.doc_id", "document.id"); .$if(content_type !== undefined, (qb) => qb.where("content_type", "=", content_type as string))
} else { .$if(use_offset, (qb) => qb.offset(offset))
query = query.from("document"); .$if(!use_offset && cursor !== undefined, (qb) => qb.where("id", "<", cursor as number))
} .limit(limit)
if (word !== undefined) { .$if(eager_loading, (qb) => {
// don't worry about sql injection. return qb.select(eb => jsonArrayFrom(
query = query.where("title", "like", `%${word}%`); eb.selectFrom("doc_tag_relation")
} .select(["doc_tag_relation.tag_name"])
if (content_type !== undefined) { .whereRef("document.id", "=", "doc_tag_relation.doc_id")
query = query.where("content_type", "=", content_type); ).as("tags")).withPlugin(new MyParseJSONResultsPlugin("tags"))
} })
if (use_offset) { .orderBy("id", "desc")
query = query.offset(offset); .execute();
} else { return result.map((x) => ({
if (cursor !== undefined) { ...x,
query = query.where("id", "<", cursor); content_hash: x.content_hash ?? "",
} additional: x.additional !== null ? (JSON.parse(x.additional)) : {},
} tags: x.tags?.map((x: { tag_name: string }) => x.tag_name) ?? [],
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;
} }
async findByPath(path: string, filename?: string): Promise<Document[]> { async findByPath(path: string, filename?: string): Promise<Document[]> {
const e = filename == undefined ? {} : { filename: filename }; const results = await this.kysely
const results = await this.knex .selectFrom("document")
.select("*") .selectAll()
.from("document") .where("basepath", "=", path)
.where({ basepath: path, ...e }); .$if(filename !== undefined, (qb) => qb.where("filename", "=", filename as string))
.execute();
return results.map((x) => ({ return results.map((x) => ({
...x, ...x,
content_hash: x.content_hash ?? "",
tags: [], tags: [],
additional: {}, additional: {},
})); }));
} }
async update(c: Partial<Document> & { id: number }) { async update(c: Partial<Document> & { id: number }) {
const { id, tags, ...rest } = c; const { id, tags, additional, ...rest } = c;
if ((await this.findById(id)) !== undefined) { const r = await this.kysely.updateTable("document")
await this.knex.update(rest).where({ id: id }).from("document"); .set({
return true; ...rest,
} modified_at: Date.now(),
return false; additional: additional !== undefined ? JSON.stringify(additional) : undefined,
})
.where("id", "=", id)
.executeTakeFirst();
return r.numUpdatedRows > 0;
} }
async addTag(c: Document, tag_name: string) { async addTag(c: Document, tag_name: string) {
if (c.tags.includes(tag_name)) return false; if (c.tags.includes(tag_name)) return false;
this.tagController.addTag({ name: tag_name }); await this.kysely.insertInto("tags")
await this.knex.insert<DBTagContentRelation>({ tag_name: tag_name, doc_id: c.id }).into("doc_tag_relation"); .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); c.tags.push(tag_name);
return true; return true;
} }
async delTag(c: Document, tag_name: string) { async delTag(c: Document, tag_name: string) {
if (c.tags.includes(tag_name)) return false; 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"); await this.kysely.deleteFrom("doc_tag_relation")
c.tags.push(tag_name); .where("tag_name", "=", tag_name)
.where("doc_id", "=", c.id)
.execute();
c.tags.splice(c.tags.indexOf(tag_name), 1);
return true; return true;
} }
} }
export const createKnexDocumentAccessor = (knex: Knex): DocumentAccessor => { export const createSqliteDocumentAccessor = (kysely = getKysely()): DocumentAccessor => {
return new KnexDocumentAccessor(knex); return new SqliteDocumentAccessor(kysely);
}; };

View 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;
}

View 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;
})
}
}
}

View File

@ -1,61 +1,65 @@
import { Knex } from "knex"; import { getKysely } from "./kysely";
import { Tag, TagAccessor, TagCount } from "../model/tag"; import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { DBTagContentRelation } from "./doc"; import type { Tag, TagAccessor, TagCount } from "../model/tag";
import type { DBTagContentRelation } from "./doc";
type DBTags = { class SqliteTagAccessor implements TagAccessor {
name: string;
description?: string;
};
class KnexTagAccessor implements TagAccessor { constructor(private kysely = getKysely()) {
knex: Knex<DBTags>;
constructor(knex: Knex) {
this.knex = knex;
} }
async getAllTagCount(): Promise<TagCount[]> { 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") .select("tag_name")
.count("*", { as: "occurs" }) .select(qb => qb.fn.count<number>("doc_id").as("occurs"))
.groupBy<TagCount[]>("tag_name"); .groupBy("tag_name")
.execute();
return result; return result;
} }
async getAllTagList(onlyname?: boolean) { async getAllTagList(): Promise<Tag[]> {
onlyname = onlyname ?? false; return (await this.kysely.selectFrom("tags")
const t: DBTags[] = await this.knex.select(onlyname ? "*" : "name").from("tags"); .selectAll()
return t; .execute()
).map((x) => ({
name: x.name,
description: x.description ?? undefined,
}));
} }
async getTagByName(name: string) { async getTagByName(name: string) {
const t: DBTags[] = await this.knex.select("*").from("tags").where({ name: name }); const result = await this.kysely
if (t.length === 0) return undefined; .selectFrom("tags")
return t[0]; .selectAll()
.where("name", "=", name)
.executeTakeFirst();
if (result === undefined) {
return undefined;
}
return {
name: result.name,
description: result.description ?? undefined,
};
} }
async addTag(tag: Tag) { async addTag(tag: Tag) {
if ((await this.getTagByName(tag.name)) === undefined) { const result = await this.kysely.insertInto("tags")
await this.knex .values([tag])
.insert<DBTags>({ .onConflict((oc) => oc.doNothing())
name: tag.name, .executeTakeFirst();
description: tag.description === undefined ? "" : tag.description, return (result.numInsertedOrUpdatedRows ?? 0n) > 0;
})
.into("tags");
return true;
}
return false;
} }
async delTag(name: string) { async delTag(name: string) {
if ((await this.getTagByName(name)) !== undefined) { const result = await this.kysely.deleteFrom("tags")
await this.knex.delete().where({ name: name }).from("tags"); .where("name", "=", name)
return true; .executeTakeFirst();
} return (result.numDeletedRows ?? 0n) > 0;
return false;
} }
async updateTag(name: string, desc: string) { async updateTag(name: string, desc: string) {
if ((await this.getTagByName(name)) !== undefined) { const result = await this.kysely.updateTable("tags")
await this.knex.update({ description: desc }).where({ name: name }).from("tags"); .set({ description: desc })
return true; .where("name", "=", name)
} .executeTakeFirst();
return false; return (result.numUpdatedRows ?? 0n) > 0;
} }
} }
export const createKnexTagController = (knex: Knex): TagAccessor => { export const createSqliteTagController = (kysely = getKysely()): TagAccessor => {
return new KnexTagAccessor(knex); return new SqliteTagAccessor(kysely);
}; };

View File

@ -1,88 +1,87 @@
import { Knex } from "knex"; import { getKysely } from "./kysely";
import { IUser, Password, UserAccessor, UserCreateInput } from "../model/user"; import { type IUser, Password, type UserAccessor, type UserCreateInput } from "../model/user";
type PermissionTable = { class SqliteUser implements IUser {
username: string;
name: string;
};
type DBUser = {
username: string;
password_hash: string;
password_salt: string;
};
class KnexUser implements IUser {
private knex: Knex;
readonly username: string; readonly username: string;
readonly password: Password; readonly password: Password;
constructor(username: string, pw: Password, knex: Knex) { constructor(username: string, pw: Password, private kysely = getKysely()) {
this.username = username; this.username = username;
this.password = pw; this.password = pw;
this.knex = knex;
} }
async reset_password(password: string) { async reset_password(password: string) {
this.password.set_password(password); this.password.set_password(password);
await this.knex await this.kysely
.from("users") .updateTable("users")
.where({ username: this.username }) .where("username", "=", this.username)
.update({ password_hash: this.password.hash, password_salt: this.password.salt }); .set({ password_hash: this.password.hash, password_salt: this.password.salt })
.execute();
} }
async get_permissions() { async get_permissions() {
let b = (await this.knex.select("*").from("permissions").where({ username: this.username })) as PermissionTable[]; const permissions = await this.kysely
return b.map((x) => x.name); .selectFrom("permissions")
.selectAll()
.where("username", "=", this.username)
.execute();
return permissions.map((x) => x.name);
} }
async add(name: string) { async add(name: string) {
if (!(await this.get_permissions()).includes(name)) { const result = await this.kysely
const r = await this.knex .insertInto("permissions")
.insert({ .values({ username: this.username, name })
username: this.username, .onConflict((oc) => oc.doNothing())
name: name, .executeTakeFirst();
}) return (result.numInsertedOrUpdatedRows ?? 0n) > 0;
.into("permissions");
return true;
}
return false;
} }
async remove(name: string) { async remove(name: string) {
const r = await this.knex const result = await this.kysely
.from("permissions") .deleteFrom("permissions")
.where({ .where("username", "=", this.username)
username: this.username, .where("name", "=", name)
name: name, .executeTakeFirst();
}) return (result.numDeletedRows ?? 0n) > 0;
.delete();
return r !== 0;
} }
} }
export const createKnexUserController = (knex: Knex): UserAccessor => { export const createSqliteUserController = (kysely = getKysely()): UserAccessor => {
const createUserKnex = async (input: UserCreateInput) => { const createUser = async (input: UserCreateInput) => {
if (undefined !== (await findUserKenx(input.username))) { if (undefined !== (await findUser(input.username))) {
return undefined; return undefined;
} }
const user = new KnexUser(input.username, new Password(input.password), knex); const user = new SqliteUser(input.username, new Password(input.password), kysely);
await knex await kysely
.insert<DBUser>({ .insertInto("users")
username: user.username, .values({ username: user.username, password_hash: user.password.hash, password_salt: user.password.salt })
password_hash: user.password.hash, .execute();
password_salt: user.password.salt,
})
.into("users");
return user; return user;
}; };
const findUserKenx = async (id: string) => { const findUser = async (id: string) => {
let user: DBUser[] = await knex.select("*").from("users").where({ username: id }); const user = await kysely
if (user.length == 0) return undefined; .selectFrom("users")
const first = user[0]; .selectAll()
return new KnexUser(first.username, new Password({ hash: first.password_hash, salt: first.password_salt }), knex); .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) => { const delUser = async (id: string) => {
let r = await knex.delete().from("users").where({ username: id }); const result = await kysely.deleteFrom("users")
return r === 0; .where("username", "=", id)
.executeTakeFirst();
return (result.numDeletedRows ?? 0n) > 0;
}; };
return { return {
createUser: createUserKnex, createUser: createUser,
findUser: findUserKenx, findUser: findUser,
delUser: delUserKnex, delUser: delUser,
}; };
}; };

View File

@ -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 { ContentFile, createContentFile } from "../content/mod";
import { Document, DocumentAccessor } from "../model/mod"; import type { Document, DocumentAccessor } from "../model/mod";
import { ContentList } from "./content_list"; import { ContentList } from "./content_list";
import { IDiffWatcher } from "./watcher"; import type { IDiffWatcher } from "./watcher";
// refactoring needed. // refactoring needed.
export class ContentDiffHandler { export class ContentDiffHandler {

View File

@ -1,4 +1,4 @@
import { ContentFile } from "../content/mod"; import type { ContentFile } from "../content/mod";
export class ContentList { export class ContentList {
/** path map */ /** path map */

View File

@ -1,7 +1,7 @@
import asyncPool from "tiny-async-pool"; import asyncPool from "tiny-async-pool";
import { DocumentAccessor } from "../model/doc"; import type { DocumentAccessor } from "../model/doc";
import { ContentDiffHandler } from "./content_handler"; import { ContentDiffHandler } from "./content_handler";
import { IDiffWatcher } from "./watcher"; import type { IDiffWatcher } from "./watcher";
export class DiffManager { export class DiffManager {
watching: { [content_type: string]: ContentDiffHandler }; watching: { [content_type: string]: ContentDiffHandler };

View File

@ -1,9 +1,9 @@
import Koa from "koa"; import type Koa from "koa";
import Router from "koa-router"; import Router from "koa-router";
import { ContentFile } from "../content/mod"; import type { ContentFile } from "../content/mod";
import { AdminOnlyMiddleware } from "../permission/permission"; import { AdminOnlyMiddleware } from "../permission/permission";
import { sendError } from "../route/error_handler"; import { sendError } from "../route/error_handler";
import { DiffManager } from "./diff"; import type { DiffManager } from "./diff";
function content_file_to_return(x: ContentFile) { function content_file_to_return(x: ContentFile) {
return { path: x.path, type: x.type }; return { path: x.path, type: x.type };
@ -24,7 +24,7 @@ type PostAddedBody = {
}[]; }[];
function checkPostAddedBody(body: any): body is 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 body.map((x) => "type" in x && "path" in x).every((x) => x);
} }
return false; return false;
@ -54,7 +54,7 @@ export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouter
sendError(400, 'format exception: there is no "type"'); sendError(400, 'format exception: there is no "type"');
return; return;
} }
const t = reqbody["type"]; const t = reqbody.type;
if (typeof t !== "string") { if (typeof t !== "string") {
sendError(400, 'format exception: invalid type of "type"'); sendError(400, 'format exception: invalid type of "type"');
return; return;

View File

@ -1,8 +1,8 @@
import event from "events"; import type event from "node:events";
import { FSWatcher, watch } from "fs"; import { FSWatcher, watch } from "node:fs";
import { promises } from "fs"; import { promises } from "node:fs";
import { join } from "path"; import { join } from "node:path";
import { DocumentAccessor } from "../model/doc"; import type { DocumentAccessor } from "../model/doc";
const readdir = promises.readdir; const readdir = promises.readdir;

View File

@ -1,6 +1,3 @@
import { EventEmitter } from "events";
import { DocumentAccessor } from "../../model/doc";
import { DiffWatcherEvent, IDiffWatcher } from "../watcher";
import { ComicConfig } from "./ComicConfig"; import { ComicConfig } from "./ComicConfig";
import { WatcherCompositer } from "./compositer"; import { WatcherCompositer } from "./compositer";
import { RecursiveWatcher } from "./recursive_watcher"; import { RecursiveWatcher } from "./recursive_watcher";

View File

@ -1,8 +1,8 @@
import event from "events"; import event from "node:events";
import { FSWatcher, promises, watch } from "fs"; import { type FSWatcher, promises, watch } from "node:fs";
import { join } from "path"; import { join } from "node:path";
import { DocumentAccessor } from "../../model/doc"; import type { DocumentAccessor } from "../../model/doc";
import { DiffWatcherEvent, IDiffWatcher } from "../watcher"; import type { DiffWatcherEvent, IDiffWatcher } from "../watcher";
import { setupHelp } from "./util"; import { setupHelp } from "./util";
const { readdir } = promises; const { readdir } = promises;

View File

@ -1,6 +1,6 @@
import { EventEmitter } from "events"; import { EventEmitter } from "node:events";
import { DocumentAccessor } from "../../model/doc"; import type { DocumentAccessor } from "../../model/doc";
import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher"; import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher";
export class WatcherCompositer extends EventEmitter implements IDiffWatcher { export class WatcherCompositer extends EventEmitter implements IDiffWatcher {
refWatchers: IDiffWatcher[]; refWatchers: IDiffWatcher[];

View File

@ -1,9 +1,8 @@
import { FSWatcher, watch } from "chokidar"; import { type FSWatcher, watch } from "chokidar";
import { EventEmitter } from "events"; import { EventEmitter } from "node:events";
import { join } from "path"; import type { DocumentAccessor } from "../../model/doc";
import { DocumentAccessor } from "../../model/doc"; import type { DiffWatcherEvent, IDiffWatcher } from "../watcher";
import { DiffWatcherEvent, IDiffWatcher } from "../watcher"; import { setupRecursive } from "./util";
import { setupHelp, setupRecursive } from "./util";
type RecursiveWatcherOption = { type RecursiveWatcherOption = {
/** @default true */ /** @default true */

View File

@ -1,14 +1,13 @@
import { EventEmitter } from "events"; import { promises } from "node:fs";
import { promises } from "fs"; import { join } from "node:path";
import { join } from "path";
const { readdir } = promises; const { readdir } = promises;
import { DocumentAccessor } from "../../model/doc"; import type { DocumentAccessor } from "../../model/doc";
import { IDiffWatcher } from "../watcher"; import type { IDiffWatcher } from "../watcher";
function setupCommon(watcher: IDiffWatcher, basepath: string, initial_filenames: string[], cur: string[]) { function setupCommon(watcher: IDiffWatcher, basepath: string, initial_filenames: string[], cur: string[]) {
// Todo : reduce O(nm) to O(n+m) using hash map. // Todo : reduce O(nm) to O(n+m) using hash map.
let added = cur.filter((x) => !initial_filenames.includes(x)); const added = cur.filter((x) => !initial_filenames.includes(x));
let deleted = initial_filenames.filter((x) => !cur.includes(x)); const deleted = initial_filenames.filter((x) => !cur.includes(x));
for (const it of added) { for (const it of added) {
const cpath = join(basepath, it); const cpath = join(basepath, it);
watcher.emit("create", cpath); watcher.emit("create", cpath);

View File

@ -1,6 +1,6 @@
import { EventEmitter } from "events"; import { EventEmitter } from "node:events";
import { DocumentAccessor } from "../../model/doc"; import type { DocumentAccessor } from "../../model/doc";
import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher"; import { type DiffWatcherEvent, type IDiffWatcher, linkWatcher } from "../watcher";
export class WatcherFilter extends EventEmitter implements IDiffWatcher { export class WatcherFilter extends EventEmitter implements IDiffWatcher {
refWatcher: IDiffWatcher; refWatcher: IDiffWatcher;
@ -21,18 +21,16 @@ export class WatcherFilter extends EventEmitter implements IDiffWatcher {
if (this.filter(prev)) { if (this.filter(prev)) {
if (this.filter(cur)) { if (this.filter(cur)) {
return super.emit("change", prev, cur); return super.emit("change", prev, cur);
} else {
return super.emit("delete", cur);
} }
} else { return super.emit("delete", cur);
}
if (this.filter(cur)) { if (this.filter(cur)) {
return super.emit("create", cur); return super.emit("create", cur);
} }
}
return false; return false;
} else if (!this.filter(arg[0])) { }if (!this.filter(arg[0])) {
return false; return false;
} else return super.emit(event, ...arg); }return super.emit(event, ...arg);
} }
constructor(refWatcher: IDiffWatcher, filter: (filename: string) => boolean) { constructor(refWatcher: IDiffWatcher, filter: (filename: string) => boolean) {
super(); super();

View File

@ -1,10 +1,10 @@
import { request } from "http"; import { request } from "node:http";
import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken"; import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken";
import Knex from "knex"; import Knex from "knex";
import Koa from "koa"; import type Koa from "koa";
import Router from "koa-router"; import Router from "koa-router";
import { createKnexUserController } from "./db/mod"; import { createSqliteUserController } from "./db/mod";
import { IUser, UserAccessor } from "./model/mod"; import type { IUser, UserAccessor } from "./model/mod";
import { sendError } from "./route/error_handler"; import { sendError } from "./route/error_handler";
import { get_setting } from "./SettingConfig"; import { get_setting } from "./SettingConfig";
@ -19,7 +19,7 @@ export type UserState = {
const isUserState = (obj: object | string): obj is PayloadInfo => { const isUserState = (obj: object | string): obj is PayloadInfo => {
if (typeof obj === "string") return false; 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 }; type RefreshPayloadInfo = { username: string };
const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => { 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) { function setToken(ctx: Koa.Context, token_name: string, token_payload: string | null, expiredtime: number) {
const setting = get_setting(); const setting = get_setting();
if (token_payload === null && !!!ctx.cookies.get(token_name)) { if (token_payload === null && !ctx.cookies.get(token_name)) {
return; return;
} }
ctx.cookies.set(token_name, token_payload, { ctx.cookies.set(token_name, token_payload, {
@ -72,11 +72,11 @@ export const createLoginMiddleware = (userController: UserAccessor) => async (ct
const secretKey = setting.jwt_secretkey; const secretKey = setting.jwt_secretkey;
const body = ctx.request.body; const body = ctx.request.body;
// check format // 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."); return sendError(400, "invalid form : username or password is not found in query.");
} }
const username = body["username"]; const username = body.username;
const password = body["password"]; const password = body.password;
// check type // check type
if (typeof username !== "string" || typeof password !== "string") { if (typeof username !== "string" || typeof password !== "string") {
return sendError(400, "invalid form : username or password is not string"); return sendError(400, "invalid form : username or password is not string");
@ -125,7 +125,7 @@ export const createUserMiddleWare =
const setGuest = async () => { const setGuest = async () => {
setToken(ctx, accessTokenName, null, 0); setToken(ctx, accessTokenName, null, 0);
setToken(ctx, refreshTokenName, 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 next();
}; };
return await refreshToken(ctx, setGuest, 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 accessPayload = ctx.cookies.get(accessTokenName);
const setting = get_setting(); const setting = get_setting();
const secretKey = setting.jwt_secretkey; const secretKey = setting.jwt_secretkey;
if (accessPayload == undefined) { if (accessPayload === undefined) {
return await checkRefreshAndUpdate(); return await checkRefreshAndUpdate();
} }
try { try {
@ -142,20 +142,19 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai
if (isUserState(o)) { if (isUserState(o)) {
ctx.state.user = o; ctx.state.user = o;
return await next(); return await next();
} else { }
console.error("invalid token detected"); console.error("invalid token detected");
throw new Error("token form invalid"); throw new Error("token form invalid");
}
} catch (e) { } catch (e) {
if (e instanceof TokenExpiredError) { if (e instanceof TokenExpiredError) {
return await checkRefreshAndUpdate(); return await checkRefreshAndUpdate();
} else throw e; }throw e;
} }
async function checkRefreshAndUpdate() { async function checkRefreshAndUpdate() {
const refreshPayload = ctx.cookies.get(refreshTokenName); const refreshPayload = ctx.cookies.get(refreshTokenName);
if (refreshPayload === undefined) { if (refreshPayload === undefined) {
return await fail(); // refresh token doesn't exist return await fail(); // refresh token doesn't exist
} else { }
try { try {
const o = verify(refreshPayload, secretKey); const o = verify(refreshPayload, secretKey);
if (isRefreshToken(o)) { if (isRefreshToken(o)) {
@ -173,9 +172,8 @@ const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fai
if (e instanceof TokenExpiredError) { if (e instanceof TokenExpiredError) {
// refresh token is expired. // refresh token is expired.
return await fail(); return await fail();
} else throw e; }throw e;
} }
}
return await next(); 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)) { if (typeof body !== "object" || !("username" in body) || !("oldpassword" in body) || !("newpassword" in body)) {
return sendError(400, "request body is invalid format"); return sendError(400, "request body is invalid format");
} }
const username = body["username"]; const username = body.username;
const oldpw = body["oldpassword"]; const oldpw = body.oldpassword;
const newpw = body["newpassword"]; const newpw = body.newpassword;
if (typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string") { if (typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string") {
return sendError(400, "request body is invalid format"); return sendError(400, "request body is invalid format");
} }

View File

@ -1,4 +1,4 @@
import { JSONMap } from "../types/json"; import type { JSONMap } from "../types/json";
import { check_type } from "../util/type_check"; import { check_type } from "../util/type_check";
import { TagAccessor } from "./tag"; import { TagAccessor } from "./tag";
@ -8,7 +8,7 @@ export interface DocumentBody {
basepath: string; basepath: string;
filename: string; filename: string;
modified_at: number; modified_at: number;
content_hash: string; content_hash: string | null;
additional: JSONMap; additional: JSONMap;
tags: string[]; // eager loading tags: string[]; // eager loading
} }
@ -23,7 +23,7 @@ export const MetaContentBody = {
tags: "string[]", tags: "string[]",
}; };
export const isDocBody = (c: any): c is DocumentBody => { export const isDocBody = (c: unknown): c is DocumentBody => {
return check_type<DocumentBody>(c, MetaContentBody); return check_type<DocumentBody>(c, MetaContentBody);
}; };
@ -33,8 +33,9 @@ export interface Document extends DocumentBody {
readonly deleted_at: number | null; readonly deleted_at: number | null;
} }
export const isDoc = (c: any): c is Document => { export const isDoc = (c: unknown): c is Document => {
if ("id" in c && typeof c["id"] === "number") { if (typeof c !== "object" || c === null) return false;
if ("id" in c && typeof c.id === "number") {
const { id, ...rest } = c; const { id, ...rest } = c;
return isDocBody(rest); return isDocBody(rest);
} }

View File

@ -9,7 +9,7 @@ export interface TagCount {
} }
export interface TagAccessor { export interface TagAccessor {
getAllTagList: (onlyname?: boolean) => Promise<Tag[]>; getAllTagList: () => Promise<Tag[]>;
getAllTagCount(): Promise<TagCount[]>; getAllTagCount(): Promise<TagCount[]>;
getTagByName: (name: string) => Promise<Tag | undefined>; getTagByName: (name: string) => Promise<Tag | undefined>;
addTag: (tag: Tag) => Promise<boolean>; addTag: (tag: Tag) => Promise<boolean>;

View File

@ -1,4 +1,4 @@
import { createHmac, randomBytes } from "crypto"; import { createHmac, randomBytes } from "node:crypto";
function hashForPassword(salt: string, password: string) { function hashForPassword(salt: string, password: string) {
return createHmac("sha256", salt).update(password).digest("hex"); return createHmac("sha256", salt).update(password).digest("hex");

View File

@ -1,5 +1,5 @@
import Koa from "koa"; import type Koa from "koa";
import { UserState } from "../login"; import type { UserState } from "../login";
import { sendError } from "../route/error_handler"; import { sendError } from "../route/error_handler";
export enum Permission { export enum Permission {
@ -37,7 +37,7 @@ export enum Permission {
export const createPermissionCheckMiddleware = export const createPermissionCheckMiddleware =
(...permissions: string[]) => (...permissions: string[]) =>
async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => { async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
const user = ctx.state["user"]; const user = ctx.state.user;
if (user.username === "admin") { if (user.username === "admin") {
return await next(); return await next();
} }
@ -46,12 +46,12 @@ export const createPermissionCheckMiddleware =
if (!permissions.map((p) => user_permission.includes(p)).every((x) => x)) { if (!permissions.map((p) => user_permission.includes(p)).every((x) => x)) {
if (user.username === "") { if (user.username === "") {
return sendError(401, "you are guest. login needed."); 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(); await next();
}; };
export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.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") { if (user.username !== "admin") {
return sendError(403, "admin only"); return sendError(403, "admin only");
} }

View File

@ -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 compose from "koa-compose";
import Router, { IParamMiddleware } from "koa-router"; import Router from "koa-router";
import ComicRouter from "./comic"; import ComicRouter from "./comic";
import { ContentContext } from "./context"; import type { ContentContext } from "./context";
import VideoRouter from "./video"; import VideoRouter from "./video";
const table: { [s: string]: Router | undefined } = { const table: { [s: string]: Router | undefined } = {
@ -12,25 +12,26 @@ const table: { [s: string]: Router | undefined } = {
const all_middleware = const all_middleware =
(cont: string | undefined, restarg: string | undefined) => (cont: string | undefined, restarg: string | undefined) =>
async (ctx: ParameterizedContext<ContentContext, DefaultContext>, next: Next) => { async (ctx: ParameterizedContext<ContentContext, DefaultContext>, next: Next) => {
if (cont == undefined) { if (cont === undefined) {
ctx.status = 404; ctx.status = 404;
return; return;
} }
if (ctx.state.location.type != cont) { if (ctx.state.location.type !== cont) {
console.error("not matched"); console.error("not matched");
ctx.status = 404; ctx.status = 404;
return; return;
} }
const router = table[cont]; const router = table[cont];
if (router == undefined) { if (router === undefined) {
ctx.status = 404; ctx.status = 404;
return; return;
} }
const rest = "/" + (restarg ?? ""); const rest = `/${restarg ?? ""}`;
const result = router.match(rest, "GET"); const result = router.match(rest, "GET");
if (!result.route) { if (!result.route) {
return await next(); return await next();
} }
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
const chain = result.pathAndMethod.reduce((combination: Middleware<any & DefaultContext, any>[], cur) => { const chain = result.pathAndMethod.reduce((combination: Middleware<any & DefaultContext, any>[], cur) => {
combination.push(async (ctx, next) => { combination.push(async (ctx, next) => {
const captures = cur.captures(rest); const captures = cur.captures(rest);
@ -47,11 +48,11 @@ export class AllContentRouter extends Router<ContentContext> {
constructor() { constructor() {
super(); super();
this.get("/:content_type", async (ctx, next) => { 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) => { this.get("/:content_type/:rest(.*)", async (ctx, next) => {
const cont = ctx.params["content_type"] as string; const cont = ctx.params.content_type as string;
return await all_middleware(cont, ctx.params["rest"])(ctx, next); return await all_middleware(cont, ctx.params.rest)(ctx, next);
}); });
} }
} }

View File

@ -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 Router from "koa-router";
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip, ZipAsync } from "../util/zipwrap"; import { createReadableStreamFromZip, entriesByNaturalOrder, readZip, type ZipAsync } from "../util/zipwrap";
import { ContentContext } from "./context"; import type { ContentContext } from "./context";
import { since_last_modified } from "./util"; import { since_last_modified } from "./util";
/** /**
* zip stream cache. * zip stream cache.
*/ */
let ZipStreamCache: { [path: string]: [ZipAsync, number] } = {}; const ZipStreamCache: { [path: string]: [ZipAsync, number] } = {};
async function acquireZip(path: string) { async function acquireZip(path: string) {
if (!(path in ZipStreamCache)) { if (!(path in ZipStreamCache)) {
@ -16,12 +16,11 @@ async function acquireZip(path: string) {
ZipStreamCache[path] = [ret, 1]; ZipStreamCache[path] = [ret, 1];
// console.log(`acquire ${path} 1`); // console.log(`acquire ${path} 1`);
return ret; return ret;
} else { }
const [ret, refCount] = ZipStreamCache[path]; const [ret, refCount] = ZipStreamCache[path];
ZipStreamCache[path] = [ret, refCount + 1]; ZipStreamCache[path] = [ret, refCount + 1];
// console.log(`acquire ${path} ${refCount + 1}`); // console.log(`acquire ${path} ${refCount + 1}`);
return ret; return ret;
}
} }
function releaseZip(path: string) { function releaseZip(path: string) {
@ -40,7 +39,7 @@ function releaseZip(path: string) {
async function renderZipImage(ctx: Context, path: string, page: number) { async function renderZipImage(ctx: Context, path: string, page: number) {
const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"]; const image_ext = ["gif", "png", "jpeg", "bmp", "webp", "jpg"];
// console.log(`opened ${page}`); // console.log(`opened ${page}`);
let zip = await acquireZip(path); const zip = await acquireZip(path);
const entries = (await entriesByNaturalOrder(zip)).filter((x) => { const entries = (await entriesByNaturalOrder(zip)).filter((x) => {
const ext = x.name.split(".").pop(); const ext = x.name.split(".").pop();
return ext !== undefined && image_ext.includes(ext); 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); await renderZipImage(ctx, ctx.state.location.path, 0);
}); });
this.get("/:page(\\d+)", async (ctx, next) => { 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); await renderZipImage(ctx, ctx.state.location.path, page);
}); });
this.get("/thumbnail", async (ctx, next) => { this.get("/thumbnail", async (ctx, next) => {

View File

@ -1,22 +1,22 @@
import { Context, Next } from "koa"; import type { Context, Next } from "koa";
import Router from "koa-router"; import Router from "koa-router";
import { join } from "path"; import { join } from "node:path";
import { Document, DocumentAccessor, isDocBody } from "../model/doc"; import { type Document, type DocumentAccessor, isDocBody } from "../model/doc";
import { QueryListOption } from "../model/doc"; import type { QueryListOption } from "../model/doc";
import { import {
AdminOnlyMiddleware as AdminOnly, AdminOnlyMiddleware as AdminOnly,
createPermissionCheckMiddleware as PerCheck, createPermissionCheckMiddleware as PerCheck,
Permission as Per, Permission as Per,
} from "../permission/permission"; } from "../permission/permission";
import { AllContentRouter } from "./all"; import { AllContentRouter } from "./all";
import { ContentLocation } from "./context"; import type { ContentLocation } from "./context";
import { sendError } from "./error_handler"; import { sendError } from "./error_handler";
import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util"; import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util";
const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
const num = Number.parseInt(ctx.params["num"]); const num = Number.parseInt(ctx.params.num);
let document = await controller.findById(num, true); const document = await controller.findById(num, true);
if (document == undefined) { if (document === undefined) {
return sendError(404, "document does not exist."); return sendError(404, "document does not exist.");
} }
ctx.body = document; ctx.body = document;
@ -24,28 +24,22 @@ const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context,
console.log(document.additional); console.log(document.additional);
}; };
const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const ContentTagIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
const num = Number.parseInt(ctx.params["num"]); const num = Number.parseInt(ctx.params.num);
let document = await controller.findById(num, true); const document = await controller.findById(num, true);
if (document == undefined) { if (document === undefined) {
return sendError(404, "document does not exist."); return sendError(404, "document does not exist.");
} }
ctx.body = document.tags; ctx.body = document.tags;
ctx.type = "json"; ctx.type = "json";
}; };
const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const ContentQueryHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
let query_limit = ctx.query["limit"]; const query_limit = ctx.query.limit;
let query_cursor = ctx.query["cursor"]; const query_cursor = ctx.query.cursor;
let query_word = ctx.query["word"]; const query_word = ctx.query.word;
let query_content_type = ctx.query["content_type"]; const query_content_type = ctx.query.content_type;
let query_offset = ctx.query["offset"]; const query_offset = ctx.query.offset;
let query_use_offset = ctx.query["use_offset"]; const query_use_offset = ctx.query.use_offset;
if ( 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)
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
) { ) {
return sendError(400, "paramter can not be array"); 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 word = ParseQueryArgString(query_word);
const content_type = ParseQueryArgString(query_content_type); const content_type = ParseQueryArgString(query_content_type);
const offset = ParseQueryNumber(query_offset); 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"); 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); const [ok, use_offset] = ParseQueryBoolean(query_use_offset);
if (!ok) { if (!ok) {
return sendError(400, "use_offset must be true or false."); 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, use_offset: use_offset,
content_type: content_type, content_type: content_type,
}; };
let document = await controller.findList(option); const document = await controller.findList(option);
ctx.body = document; ctx.body = document;
ctx.type = "json"; ctx.type = "json";
}; };
const UpdateContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { 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") { if (ctx.request.type !== "json") {
return sendError(400, "update fail. invalid document type: it is not 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) => { const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
let tag_name = ctx.params["tag"]; let tag_name = ctx.params.tag;
const num = Number.parseInt(ctx.params["num"]); const num = Number.parseInt(ctx.params.num);
if (typeof tag_name === undefined) { if (typeof tag_name === "undefined") {
return sendError(400, "??? Unreachable"); return sendError(400, "??? Unreachable");
} }
tag_name = String(tag_name); tag_name = String(tag_name);
@ -110,9 +104,9 @@ const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, nex
ctx.type = "json"; ctx.type = "json";
}; };
const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
let tag_name = ctx.params["tag"]; let tag_name = ctx.params.tag;
const num = Number.parseInt(ctx.params["num"]); const num = Number.parseInt(ctx.params.num);
if (typeof tag_name === undefined) { if (typeof tag_name === "undefined") {
return sendError(400, "?? Unreachable"); return sendError(400, "?? Unreachable");
} }
tag_name = String(tag_name); tag_name = String(tag_name);
@ -125,22 +119,22 @@ const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, nex
ctx.type = "json"; ctx.type = "json";
}; };
const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { 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); const r = await controller.del(num);
ctx.body = JSON.stringify(r); ctx.body = JSON.stringify(r);
ctx.type = "json"; ctx.type = "json";
}; };
const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => { const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
const num = Number.parseInt(ctx.params["num"]); const num = Number.parseInt(ctx.params.num);
let document = await controller.findById(num, true); const document = await controller.findById(num, true);
if (document == undefined) { if (document === undefined) {
return sendError(404, "document does not exist."); return sendError(404, "document does not exist.");
} }
if (document.deleted_at !== null) { if (document.deleted_at !== null) {
return sendError(404, "document has been removed."); return sendError(404, "document has been removed.");
} }
const path = join(document.basepath, document.filename); const path = join(document.basepath, document.filename);
ctx.state["location"] = { ctx.state.location = {
path: path, path: path,
type: document.content_type, type: document.content_type,
additional: document.additional, additional: document.additional,

View File

@ -1,4 +1,4 @@
import { Context, Next } from "koa"; import type { Context, Next } from "koa";
export interface ErrorFormat { export interface ErrorFormat {
code: number; code: number;

View File

@ -1,13 +1,13 @@
import { Context, Next } from "koa"; import { type Context, Next } from "koa";
import Router, { RouterContext } from "koa-router"; import Router, { type RouterContext } from "koa-router";
import { TagAccessor } from "../model/tag"; import type { TagAccessor } from "../model/tag";
import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission"; import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission";
import { sendError } from "./error_handler"; import { sendError } from "./error_handler";
export function getTagRounter(tagController: TagAccessor) { export function getTagRounter(tagController: TagAccessor) {
let router = new Router(); const router = new Router();
router.get("/", PerCheck(Permission.QueryContent), async (ctx: Context) => { router.get("/", PerCheck(Permission.QueryContent), async (ctx: Context) => {
if (ctx.query["withCount"]) { if (ctx.query.withCount) {
const c = await tagController.getAllTagCount(); const c = await tagController.getAllTagCount();
ctx.body = c; ctx.body = c;
} else { } else {
@ -17,7 +17,7 @@ export function getTagRounter(tagController: TagAccessor) {
ctx.type = "json"; ctx.type = "json";
}); });
router.get("/:tag_name", PerCheck(Permission.QueryContent), async (ctx: RouterContext) => { 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); const c = await tagController.getTagByName(tag_name);
if (!c) { if (!c) {
sendError(404, "tags not found"); sendError(404, "tags not found");

View File

@ -1,13 +1,13 @@
import { Context } from "koa"; import type { Context } from "koa";
export function ParseQueryNumber(s: string[] | string | undefined): number | undefined { export function ParseQueryNumber(s: string[] | string | undefined): number | undefined {
if (s === undefined) return undefined; if (s === undefined) return undefined;
else if (typeof s === "object") return undefined; if (typeof s === "object") return undefined;
else return Number.parseInt(s); return Number.parseInt(s);
} }
export function ParseQueryArray(s: string[] | string | undefined) { export function ParseQueryArray(s: string[] | string | undefined) {
s = s ?? []; const input = s ?? [];
const r = s instanceof Array ? s : [s]; const r = Array.isArray(input) ? input : [input];
return r.map((x) => decodeURIComponent(x)); return r.map((x) => decodeURIComponent(x));
} }
export function ParseQueryArgString(s: string[] | string | undefined) { export function ParseQueryArgString(s: string[] | string | undefined) {

View File

@ -1,7 +1,7 @@
import { createReadStream, promises } from "fs"; import { createReadStream, promises } from "node:fs";
import { Context } from "koa"; import type { Context } from "koa";
import Router from "koa-router"; import Router from "koa-router";
import { ContentContext } from "./context"; import type { ContentContext } from "./context";
export async function renderVideo(ctx: Context, path: string) { export async function renderVideo(ctx: Context, path: string) {
const ext = path.trim().split(".").pop(); const ext = path.trim().split(".").pop();
@ -27,7 +27,7 @@ export async function renderVideo(ctx: Context, path: string) {
} }
ctx.status = 200; ctx.status = 200;
ctx.length = stat.size; ctx.length = stat.size;
let stream = createReadStream(path); const stream = createReadStream(path);
ctx.body = stream; ctx.body = stream;
} else { } else {
const m = range_text.match(/^bytes=(\d+)-(\d*)/); const m = range_text.match(/^bytes=(\d+)-(\d*)/);
@ -35,8 +35,8 @@ export async function renderVideo(ctx: Context, path: string) {
ctx.status = 416; ctx.status = 416;
return; return;
} }
start = parseInt(m[1]); start = Number.parseInt(m[1]);
end = m[2].length > 0 ? parseInt(m[2]) : start + 1024 * 1024; end = m[2].length > 0 ? Number.parseInt(m[2]) : start + 1024 * 1024;
end = Math.min(end, stat.size - 1); end = Math.min(end, stat.size - 1);
if (start > end) { if (start > end) {
ctx.status = 416; ctx.status = 416;

View File

@ -5,18 +5,21 @@ import { connectDB } from "./database";
import { createDiffRouter, DiffManager } from "./diff/mod"; import { createDiffRouter, DiffManager } from "./diff/mod";
import { get_setting, SettingConfig } from "./SettingConfig"; import { get_setting, SettingConfig } from "./SettingConfig";
import { createReadStream, readFileSync } from "fs"; import { createReadStream, readFileSync } from "node:fs";
import bodyparser from "koa-bodyparser"; 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 { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login";
import getContentRouter from "./route/contents"; import getContentRouter from "./route/contents";
import { error_handler } from "./route/error_handler"; 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 { 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 { getTagRounter } from "./route/tags";
import { config } from "dotenv";
config();
class ServerApplication { class ServerApplication {
readonly userController: UserAccessor; readonly userController: UserAccessor;
readonly documentController: DocumentAccessor; readonly documentController: DocumentAccessor;
@ -61,12 +64,12 @@ class ServerApplication {
app.use(error_handler); app.use(error_handler);
app.use(createUserMiddleWare(this.userController)); app.use(createUserMiddleWare(this.userController));
let diff_router = createDiffRouter(this.diffManger); const diff_router = createDiffRouter(this.diffManger);
this.diffManger.register("comic", createComicWatcher()); this.diffManger.register("comic", createComicWatcher());
console.log("setup router"); console.log("setup router");
let router = new Router(); const router = new Router();
router.use("/api/(.*)", async (ctx, next) => { router.use("/api/(.*)", async (ctx, next) => {
// For CORS // For CORS
ctx.res.setHeader("access-control-allow-origin", "*"); ctx.res.setHeader("access-control-allow-origin", "*");
@ -92,12 +95,12 @@ class ServerApplication {
router.use("/user", login_router.routes()); router.use("/user", login_router.routes());
router.use("/user", login_router.allowedMethods()); router.use("/user", login_router.allowedMethods());
if (setting.mode == "development") { if (setting.mode === "development") {
let mm_count = 0; let mm_count = 0;
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
console.log(`==========================${mm_count++}`); console.log(`==========================${mm_count++}`);
const ip = ctx.get("X-Real-IP") ?? ctx.ip; 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}`); console.log(`${fromClient} : ${ctx.method} ${ctx.url}`);
await next(); await next();
// console.log(`404`); // console.log(`404`);
@ -132,8 +135,9 @@ class ServerApplication {
} }
private serve_with_meta_index(router: Router) { private serve_with_meta_index(router: Router) {
const DocMiddleware = async (ctx: Koa.ParameterizedContext) => { 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); const doc = await this.documentController.findById(docId, true);
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
let meta; let meta;
if (doc === undefined) { if (doc === undefined) {
ctx.status = 404; ctx.status = 404;
@ -190,7 +194,7 @@ class ServerApplication {
} }
private serve_static_file(router: Router) { private serve_static_file(router: Router) {
const static_file_server = (path: string, type: string) => { const static_file_server = (path: string, type: string) => {
router.get("/" + path, async (ctx, next) => { router.get(`/${path}`, async (ctx, next) => {
const setting = get_setting(); const setting = get_setting();
ctx.type = type; ctx.type = type;
ctx.body = createReadStream(path); ctx.body = createReadStream(path);
@ -211,19 +215,19 @@ class ServerApplication {
} }
} }
start_server() { start_server() {
let setting = get_setting(); const setting = get_setting();
// todo : support https // todo : support https
console.log(`listen on http://${setting.localmode ? "localhost" : "0.0.0.0"}:${setting.port}`); 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"); return this.app.listen(setting.port, setting.localmode ? "127.0.0.1" : "0.0.0.0");
} }
static async createServer() { static async createServer() {
const setting = get_setting(); const setting = get_setting();
let db = await connectDB(); const db = await connectDB();
const app = new ServerApplication({ const app = new ServerApplication({
userController: createKnexUserController(db), userController: createSqliteUserController(db),
documentController: createKnexDocumentAccessor(db), documentController: createSqliteDocumentAccessor(db),
tagController: createKnexTagController(db), tagController: createSqliteTagController(db),
}); });
await app.setup(); await app.setup();
return app; return app;

View File

@ -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;
};
}
}

View File

@ -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"; import { validate } from "jsonschema";
export class ConfigManager<T> { export class ConfigManager<T extends object> {
path: string; path: string;
default_config: T; default_config: T;
config: T | null; config: T | null;

View File

@ -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) { for (const it in check_proto) {
let defined = check_proto[it]; let defined = check_proto[it];
if (defined === undefined) return false; if (defined === undefined) return false;
defined = defined.trim(); defined = defined.trim();
if (defined.endsWith("[]")) { if (defined.endsWith("[]")) {
if (!(obj[it] instanceof Array)) { if (!Array.isArray((obj as Record<string, unknown>)[it])) {
return false; 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; return false;
} }
} }

View File

@ -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 { orderBy } from "natural-orderby";
import StreamZip from "node-stream-zip"; import StreamZip from "node-stream-zip";

View File

@ -6,7 +6,7 @@
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "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'. */ "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. */ // "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */

File diff suppressed because it is too large Load Diff