add: dprint fmt
This commit is contained in:
parent
04ab39a3ec
commit
edc6104a09
@ -4,21 +4,25 @@ Content File Management Program.
|
|||||||
For study about nodejs, typescript and react.
|
For study about nodejs, typescript and react.
|
||||||
|
|
||||||
### deployment
|
### deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run app:build
|
pnpm run app:build
|
||||||
```
|
```
|
||||||
|
|
||||||
### test
|
### test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pnpm run app
|
$ pnpm run app
|
||||||
```
|
```
|
||||||
|
|
||||||
### server build
|
### server build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pnpm run compile
|
$ pnpm run compile
|
||||||
```
|
```
|
||||||
|
|
||||||
### client build
|
### client build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pnpm run build
|
$ pnpm run build
|
||||||
```
|
```
|
||||||
|
194
app.ts
194
app.ts
@ -1,115 +1,113 @@
|
|||||||
import { app, BrowserWindow, session, dialog } from "electron";
|
import { app, BrowserWindow, dialog, session } from "electron";
|
||||||
import { get_setting } from "./src/SettingConfig";
|
import { ipcMain } from "electron";
|
||||||
import { create_server } from "./src/server";
|
|
||||||
import { getAdminAccessTokenValue,getAdminRefreshTokenValue, accessTokenName, refreshTokenName } from "./src/login";
|
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { ipcMain } from 'electron';
|
import { accessTokenName, getAdminAccessTokenValue, getAdminRefreshTokenValue, refreshTokenName } from "./src/login";
|
||||||
import { UserAccessor } from "./src/model/mod";
|
import { UserAccessor } from "./src/model/mod";
|
||||||
|
import { create_server } from "./src/server";
|
||||||
|
import { get_setting } from "./src/SettingConfig";
|
||||||
|
|
||||||
function registerChannel(cntr: UserAccessor){
|
function registerChannel(cntr: UserAccessor) {
|
||||||
ipcMain.handle('reset_password', async(event,username:string,password:string)=>{
|
ipcMain.handle("reset_password", async (event, username: string, password: string) => {
|
||||||
const user = await cntr.findUser(username);
|
const user = await cntr.findUser(username);
|
||||||
if(user === undefined){
|
if (user === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
user.reset_password(password);
|
user.reset_password(password);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
if (!setting.cli) {
|
if (!setting.cli) {
|
||||||
let wnd: BrowserWindow | null = null;
|
let wnd: BrowserWindow | null = null;
|
||||||
|
|
||||||
const createWindow = async () => {
|
const createWindow = async () => {
|
||||||
wnd = new BrowserWindow({
|
wnd = new BrowserWindow({
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
center: true,
|
center: true,
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
webPreferences:{
|
webPreferences: {
|
||||||
preload:join(__dirname,'preload.js'),
|
preload: join(__dirname, "preload.js"),
|
||||||
contextIsolation:true,
|
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,
|
|
||||||
});
|
});
|
||||||
}
|
await wnd.loadURL(`data:text/html;base64,` + Buffer.from(loading_html).toString("base64"));
|
||||||
else{
|
// await wnd.loadURL('../loading.html');
|
||||||
await dialog.showMessageBox({
|
// set admin cookies.
|
||||||
type: "error",
|
await session.defaultSession.cookies.set({
|
||||||
title:"error!",
|
url: `http://localhost:${setting.port}`,
|
||||||
message:String(e),
|
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();
|
||||||
}
|
}
|
||||||
wnd.on("closed", () => {
|
app.on("second-instance", () => {
|
||||||
wnd = null;
|
if (wnd != null) {
|
||||||
|
if (wnd.isMinimized()) {
|
||||||
|
wnd.restore();
|
||||||
|
}
|
||||||
|
wnd.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.on("ready", (event, info) => {
|
||||||
|
createWindow();
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const isPrimary = app.requestSingleInstanceLock();
|
app.on("window-all-closed", () => { // quit when all windows are closed
|
||||||
if (!isPrimary) {
|
if (process.platform != "darwin") app.quit(); // (except leave MacOS app active until Cmd+Q)
|
||||||
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
|
app.on("activate", () => { // re-recreate window when dock icon is clicked and no other windows open
|
||||||
if (process.platform != "darwin") app.quit(); // (except leave MacOS app active until Cmd+Q)
|
if (wnd == null) createWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("activate", () => { // re-recreate window when dock icon is clicked and no other windows open
|
|
||||||
if (wnd == null) createWindow();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const server = await create_server();
|
const server = await create_server();
|
||||||
server.start_server();
|
server.start_server();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
const loading_html = `<!DOCTYPE html>
|
const loading_html = `<!DOCTYPE html>
|
||||||
<html lang="ko"><head>
|
<html lang="ko"><head>
|
||||||
|
23
dprint.json
Normal file
23
dprint.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
@ -1,48 +1,48 @@
|
|||||||
import { promises } from 'fs';
|
import { promises } from "fs";
|
||||||
const { readdir, writeFile } = promises;
|
const { readdir, writeFile } = promises;
|
||||||
import {createGenerator} from 'ts-json-schema-generator';
|
import { dirname, join } from "path";
|
||||||
import {dirname,join} from 'path';
|
import { createGenerator } from "ts-json-schema-generator";
|
||||||
|
|
||||||
async function genSchema(path:string,typename:string){
|
async function genSchema(path: string, typename: string) {
|
||||||
const gen = createGenerator({
|
const gen = createGenerator({
|
||||||
path:path,
|
path: path,
|
||||||
type:typename,
|
type: typename,
|
||||||
tsconfig:"tsconfig.json"
|
tsconfig: "tsconfig.json",
|
||||||
});
|
});
|
||||||
const schema = gen.createSchema(typename);
|
const schema = gen.createSchema(typename);
|
||||||
if(schema.definitions != undefined){
|
if (schema.definitions != undefined) {
|
||||||
const definitions = schema.definitions;
|
const definitions = schema.definitions;
|
||||||
const definition = definitions[typename];
|
const definition = definitions[typename];
|
||||||
if(typeof definition == "object" ){
|
if (typeof definition == "object") {
|
||||||
let property = definition.properties;
|
let property = definition.properties;
|
||||||
if(property){
|
if (property) {
|
||||||
property['$schema'] = {
|
property["$schema"] = {
|
||||||
type:"string"
|
type: "string",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const text = JSON.stringify(schema);
|
const text = JSON.stringify(schema);
|
||||||
await writeFile(join(dirname(path),`${typename}.schema.json`),text);
|
await writeFile(join(dirname(path), `${typename}.schema.json`), text);
|
||||||
}
|
}
|
||||||
function capitalize(s:string){
|
function capitalize(s: string) {
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
}
|
}
|
||||||
async function setToALL(path:string) {
|
async function setToALL(path: string) {
|
||||||
console.log(`scan ${path}`)
|
console.log(`scan ${path}`);
|
||||||
const direntry = await readdir(path,{withFileTypes:true});
|
const direntry = await readdir(path, { withFileTypes: true });
|
||||||
const works = direntry.filter(x=>x.isFile()&&x.name.endsWith("Config.ts")).map(x=>{
|
const works = direntry.filter(x => x.isFile() && x.name.endsWith("Config.ts")).map(x => {
|
||||||
const name = x.name;
|
const name = x.name;
|
||||||
const m = /(.+)\.ts/.exec(name);
|
const m = /(.+)\.ts/.exec(name);
|
||||||
if(m !== null){
|
if (m !== null) {
|
||||||
const typename = m[1];
|
const typename = m[1];
|
||||||
return genSchema(join(path,typename),capitalize(typename));
|
return genSchema(join(path, typename), capitalize(typename));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
await Promise.all(works);
|
await Promise.all(works);
|
||||||
const subdir = direntry.filter(x=>x.isDirectory()).map(x=>x.name);
|
const subdir = direntry.filter(x => x.isDirectory()).map(x => x.name);
|
||||||
for(const x of subdir){
|
for (const x of subdir) {
|
||||||
await setToALL(join(path,x));
|
await setToALL(join(path, x));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setToALL("src")
|
setToALL("src");
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
require('ts-node').register();
|
require("ts-node").register();
|
||||||
const {Knex} = require('./src/config');
|
const { Knex } = require("./src/config");
|
||||||
// Update with your config settings.
|
// Update with your config settings.
|
||||||
|
|
||||||
module.exports = Knex.config;
|
module.exports = Knex.config;
|
||||||
|
@ -1,54 +1,54 @@
|
|||||||
import {Knex} from 'knex';
|
import { Knex } from "knex";
|
||||||
|
|
||||||
export async function up(knex:Knex) {
|
export async function up(knex: Knex) {
|
||||||
await knex.schema.createTable("schema_migration",(b)=>{
|
await knex.schema.createTable("schema_migration", (b) => {
|
||||||
b.string("version");
|
b.string("version");
|
||||||
b.boolean("dirty");
|
b.boolean("dirty");
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.schema.createTable("users",(b)=>{
|
await knex.schema.createTable("users", (b) => {
|
||||||
b.string("username").primary().comment("user's login id");
|
b.string("username").primary().comment("user's login id");
|
||||||
b.string("password_hash",64).notNullable();
|
b.string("password_hash", 64).notNullable();
|
||||||
b.string("password_salt",64).notNullable();
|
b.string("password_salt", 64).notNullable();
|
||||||
});
|
});
|
||||||
await knex.schema.createTable("document",(b)=>{
|
await knex.schema.createTable("document", (b) => {
|
||||||
b.increments("id").primary();
|
b.increments("id").primary();
|
||||||
b.string("title").notNullable();
|
b.string("title").notNullable();
|
||||||
b.string("content_type",16).notNullable();
|
b.string("content_type", 16).notNullable();
|
||||||
b.string("basepath",256).notNullable().comment("directory path for resource");
|
b.string("basepath", 256).notNullable().comment("directory path for resource");
|
||||||
b.string("filename",256).notNullable().comment("filename");
|
b.string("filename", 256).notNullable().comment("filename");
|
||||||
b.string("content_hash").nullable();
|
b.string("content_hash").nullable();
|
||||||
b.json("additional").nullable();
|
b.json("additional").nullable();
|
||||||
b.integer("created_at").notNullable();
|
b.integer("created_at").notNullable();
|
||||||
b.integer("modified_at").notNullable();
|
b.integer("modified_at").notNullable();
|
||||||
b.integer("deleted_at");
|
b.integer("deleted_at");
|
||||||
b.index("content_type","content_type_index");
|
b.index("content_type", "content_type_index");
|
||||||
});
|
});
|
||||||
await knex.schema.createTable("tags", (b)=>{
|
await knex.schema.createTable("tags", (b) => {
|
||||||
b.string("name").primary();
|
b.string("name").primary();
|
||||||
b.text("description");
|
b.text("description");
|
||||||
});
|
});
|
||||||
await knex.schema.createTable("doc_tag_relation",(b)=>{
|
await knex.schema.createTable("doc_tag_relation", (b) => {
|
||||||
b.integer("doc_id").unsigned().notNullable();
|
b.integer("doc_id").unsigned().notNullable();
|
||||||
b.string("tag_name").notNullable();
|
b.string("tag_name").notNullable();
|
||||||
b.foreign("doc_id").references("document.id");
|
b.foreign("doc_id").references("document.id");
|
||||||
b.foreign("tag_name").references("tags.name");
|
b.foreign("tag_name").references("tags.name");
|
||||||
b.primary(["doc_id","tag_name"]);
|
b.primary(["doc_id", "tag_name"]);
|
||||||
});
|
});
|
||||||
await knex.schema.createTable("permissions",b=>{
|
await knex.schema.createTable("permissions", b => {
|
||||||
b.string('username').notNullable();
|
b.string("username").notNullable();
|
||||||
b.string("name").notNullable();
|
b.string("name").notNullable();
|
||||||
b.primary(["username","name"]);
|
b.primary(["username", "name"]);
|
||||||
b.foreign('username').references('users.username');
|
b.foreign("username").references("users.username");
|
||||||
});
|
});
|
||||||
//create admin account.
|
// create admin account.
|
||||||
await knex.insert({
|
await knex.insert({
|
||||||
username:"admin",
|
username: "admin",
|
||||||
password_hash:"unchecked",
|
password_hash: "unchecked",
|
||||||
password_salt:"unchecked"
|
password_salt: "unchecked",
|
||||||
}).into('users');
|
}).into("users");
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function down(knex:Knex) {
|
export async function down(knex: Knex) {
|
||||||
throw new Error('Downward migrations are not supported. Restore from backup.');
|
throw new Error("Downward migrations are not supported. Restore from backup.");
|
||||||
};
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"compile:watch": "tsc -w",
|
"compile:watch": "tsc -w",
|
||||||
"build": "cd src/client && pnpm run build:prod",
|
"build": "cd src/client && pnpm run build:prod",
|
||||||
"build:watch": "cd src/client && pnpm run build:watch",
|
"build:watch": "cd src/client && pnpm run build:watch",
|
||||||
|
"fmt": "dprint fmt",
|
||||||
"app": "electron build/app.js",
|
"app": "electron build/app.js",
|
||||||
"app:build": "electron-builder",
|
"app:build": "electron-builder",
|
||||||
"app:pack": "electron-builder --dir",
|
"app:pack": "electron-builder --dir",
|
||||||
@ -56,6 +57,7 @@
|
|||||||
"@louislam/sqlite3": "^6.0.1",
|
"@louislam/sqlite3": "^6.0.1",
|
||||||
"@types/koa-compose": "^3.2.5",
|
"@types/koa-compose": "^3.2.5",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
|
"dprint": "^0.36.1",
|
||||||
"jsonschema": "^1.4.1",
|
"jsonschema": "^1.4.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"knex": "^0.95.15",
|
"knex": "^0.95.15",
|
||||||
|
2
plan.md
2
plan.md
@ -3,6 +3,7 @@
|
|||||||
## Routing
|
## Routing
|
||||||
|
|
||||||
### server routing
|
### server routing
|
||||||
|
|
||||||
- content
|
- content
|
||||||
- \d+
|
- \d+
|
||||||
- comic
|
- comic
|
||||||
@ -31,6 +32,7 @@
|
|||||||
- profile
|
- profile
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- server push
|
- server push
|
||||||
- ~~permission~~
|
- ~~permission~~
|
||||||
- diff
|
- diff
|
||||||
|
1133
pnpm-lock.yaml
1133
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
10
preload.ts
10
preload.ts
@ -1,7 +1,7 @@
|
|||||||
import {ipcRenderer, contextBridge} from 'electron';
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron',{
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
passwordReset:async (username:string,toPw:string)=>{
|
passwordReset: async (username: string, toPw: string) => {
|
||||||
return await ipcRenderer.invoke('reset_password',username,toPw);
|
return await ipcRenderer.invoke("reset_password", username, toPw);
|
||||||
}
|
},
|
||||||
});
|
});
|
@ -1,66 +1,66 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"$ref": "#/definitions/SettingConfig",
|
"$ref": "#/definitions/SettingConfig",
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"SettingConfig": {
|
"SettingConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"localmode": {
|
"localmode": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "if true, server will bind on '127.0.0.1' rather than '0.0.0.0'"
|
"description": "if true, server will bind on '127.0.0.1' rather than '0.0.0.0'"
|
||||||
},
|
|
||||||
"guest": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/Permission"
|
|
||||||
},
|
|
||||||
"description": "guest permission"
|
|
||||||
},
|
|
||||||
"jwt_secretkey": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "JWT secret key. if you change its value, all access tokens are invalidated."
|
|
||||||
},
|
|
||||||
"port": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "the port which running server is binding on."
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"development",
|
|
||||||
"production"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"cli": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "if true, do not show 'electron' window and show terminal only."
|
|
||||||
},
|
|
||||||
"forbid_remote_admin_login": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "forbid to login admin from remote client. but, it do not invalidate access token. \r if you want to invalidate access token, change 'jwt_secretkey'."
|
|
||||||
},
|
|
||||||
"$schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"localmode",
|
|
||||||
"guest",
|
|
||||||
"jwt_secretkey",
|
|
||||||
"port",
|
|
||||||
"mode",
|
|
||||||
"cli",
|
|
||||||
"forbid_remote_admin_login"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
},
|
||||||
"Permission": {
|
"guest": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"enum": [
|
"items": {
|
||||||
"ModifyTag",
|
"$ref": "#/definitions/Permission"
|
||||||
"QueryContent",
|
},
|
||||||
"ModifyTagDesc"
|
"description": "guest permission"
|
||||||
]
|
},
|
||||||
|
"jwt_secretkey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "JWT secret key. if you change its value, all access tokens are invalidated."
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "the port which running server is binding on."
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"development",
|
||||||
|
"production"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "if true, do not show 'electron' window and show terminal only."
|
||||||
|
},
|
||||||
|
"forbid_remote_admin_login": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "forbid to login admin from remote client. but, it do not invalidate access token. \r if you want to invalidate access token, change 'jwt_secretkey'."
|
||||||
|
},
|
||||||
|
"$schema": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"localmode",
|
||||||
|
"guest",
|
||||||
|
"jwt_secretkey",
|
||||||
|
"port",
|
||||||
|
"mode",
|
||||||
|
"cli",
|
||||||
|
"forbid_remote_admin_login"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"Permission": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ModifyTag",
|
||||||
|
"QueryContent",
|
||||||
|
"ModifyTagDesc"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,76 +1,76 @@
|
|||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from "crypto";
|
||||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||||
import { Permission } from './permission/permission';
|
import { Permission } from "./permission/permission";
|
||||||
|
|
||||||
export interface SettingConfig {
|
export interface SettingConfig {
|
||||||
/**
|
/**
|
||||||
* if true, server will bind on '127.0.0.1' rather than '0.0.0.0'
|
* if true, server will bind on '127.0.0.1' rather than '0.0.0.0'
|
||||||
*/
|
*/
|
||||||
localmode: boolean,
|
localmode: boolean;
|
||||||
/**
|
/**
|
||||||
* secure only
|
* secure only
|
||||||
*/
|
*/
|
||||||
secure: boolean,
|
secure: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* guest permission
|
* guest permission
|
||||||
*/
|
*/
|
||||||
guest: (Permission)[],
|
guest: (Permission)[];
|
||||||
/**
|
/**
|
||||||
* JWT secret key. if you change its value, all access tokens are invalidated.
|
* JWT secret key. if you change its value, all access tokens are invalidated.
|
||||||
*/
|
*/
|
||||||
jwt_secretkey: string,
|
jwt_secretkey: string;
|
||||||
/**
|
/**
|
||||||
* the port which running server is binding on.
|
* the port which running server is binding on.
|
||||||
*/
|
*/
|
||||||
port:number,
|
port: number;
|
||||||
|
|
||||||
mode:"development"|"production",
|
mode: "development" | "production";
|
||||||
/**
|
/**
|
||||||
* if true, do not show 'electron' window and show terminal only.
|
* if true, do not show 'electron' window and show terminal only.
|
||||||
*/
|
*/
|
||||||
cli:boolean,
|
cli: boolean;
|
||||||
/** forbid to login admin from remote client. but, it do not invalidate access token.
|
/** forbid to login admin from remote client. but, it do not invalidate access token.
|
||||||
* if you want to invalidate access token, change 'jwt_secretkey'.*/
|
* if you want to invalidate access token, change 'jwt_secretkey'. */
|
||||||
forbid_remote_admin_login:boolean,
|
forbid_remote_admin_login: boolean;
|
||||||
}
|
}
|
||||||
const default_setting:SettingConfig = {
|
const default_setting: SettingConfig = {
|
||||||
localmode: true,
|
localmode: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
guest:[],
|
guest: [],
|
||||||
jwt_secretkey:"itsRandom",
|
jwt_secretkey: "itsRandom",
|
||||||
port:8080,
|
port: 8080,
|
||||||
mode:"production",
|
mode: "production",
|
||||||
cli:false,
|
cli: false,
|
||||||
forbid_remote_admin_login:true,
|
forbid_remote_admin_login: true,
|
||||||
}
|
};
|
||||||
let setting: null|SettingConfig = null;
|
let setting: null | SettingConfig = null;
|
||||||
|
|
||||||
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) {
|
||||||
if(key === undefined || key in target){
|
if (key === undefined || key in target) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
target[key] = default_table[key as keyof SettingConfig];
|
target[key] = default_table[key as keyof SettingConfig];
|
||||||
diff_occur = true;
|
diff_occur = true;
|
||||||
}
|
}
|
||||||
return diff_occur;
|
return diff_occur;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const read_setting_from_file = ()=>{
|
export const read_setting_from_file = () => {
|
||||||
let ret = existsSync("settings.json") ? JSON.parse(readFileSync("settings.json",{encoding:"utf8"})) : {};
|
let 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));
|
||||||
}
|
}
|
||||||
return ret as SettingConfig;
|
return ret as SettingConfig;
|
||||||
}
|
};
|
||||||
export function get_setting():SettingConfig{
|
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;
|
||||||
|
@ -1,99 +1,99 @@
|
|||||||
import {Document, DocumentAccessor, DocumentBody, QueryListOption} from "../../model/doc";
|
import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../../model/doc";
|
||||||
import {toQueryString} from './util';
|
import { toQueryString } from "./util";
|
||||||
const baseurl = "/api/doc";
|
const baseurl = "/api/doc";
|
||||||
|
|
||||||
export * from "../../model/doc";
|
export * from "../../model/doc";
|
||||||
|
|
||||||
export class FetchFailError extends Error{}
|
export class FetchFailError extends Error {}
|
||||||
|
|
||||||
export class ClientDocumentAccessor implements DocumentAccessor{
|
export class ClientDocumentAccessor implements DocumentAccessor {
|
||||||
search: (search_word: string) => Promise<Document[]>;
|
search: (search_word: string) => Promise<Document[]>;
|
||||||
addList: (content_list: DocumentBody[]) => Promise<number[]>;
|
addList: (content_list: DocumentBody[]) => Promise<number[]>;
|
||||||
async findByPath(basepath: string, filename?: string): Promise<Document[]>{
|
async findByPath(basepath: string, filename?: string): Promise<Document[]> {
|
||||||
throw new Error("not allowed");
|
throw new Error("not allowed");
|
||||||
};
|
}
|
||||||
async findDeleted(content_type: string): Promise<Document[]>{
|
async findDeleted(content_type: string): Promise<Document[]> {
|
||||||
throw new Error("not allowed");
|
throw new Error("not allowed");
|
||||||
};
|
}
|
||||||
async findList(option?: QueryListOption | undefined): Promise<Document[]>{
|
async findList(option?: QueryListOption | undefined): Promise<Document[]> {
|
||||||
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
|
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
|
||||||
if(res.status == 401) throw new FetchFailError("Unauthorized")
|
if (res.status == 401) throw new FetchFailError("Unauthorized");
|
||||||
if(res.status !== 200) throw new FetchFailError("findList Failed");
|
if (res.status !== 200) throw new FetchFailError("findList Failed");
|
||||||
let ret = await res.json();
|
let ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async findById(id: number, tagload?: boolean | undefined): Promise<Document | undefined>{
|
async findById(id: number, tagload?: boolean | undefined): Promise<Document | undefined> {
|
||||||
let res = await fetch(`${baseurl}/${id}`);
|
let res = await fetch(`${baseurl}/${id}`);
|
||||||
if(res.status !== 200) throw new FetchFailError("findById Failed");;
|
if (res.status !== 200) throw new FetchFailError("findById Failed");
|
||||||
let ret = await res.json();
|
let ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* not implement
|
* not implement
|
||||||
*/
|
*/
|
||||||
async findListByBasePath(basepath: string): Promise<Document[]>{
|
async findListByBasePath(basepath: string): Promise<Document[]> {
|
||||||
throw new Error("not implement");
|
throw new Error("not implement");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
async update(c: Partial<Document> & { id: number; }): Promise<boolean>{
|
async update(c: Partial<Document> & { id: number }): Promise<boolean> {
|
||||||
const {id,...rest} = c;
|
const { id, ...rest } = c;
|
||||||
const res = await fetch(`${baseurl}/${id}`,{
|
const res = await fetch(`${baseurl}/${id}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(rest),
|
body: JSON.stringify(rest),
|
||||||
headers:{
|
headers: {
|
||||||
'content-type':"application/json"
|
"content-type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const ret = await res.json();
|
const ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async add(c: DocumentBody): Promise<number>{
|
async add(c: DocumentBody): Promise<number> {
|
||||||
throw new Error("not allow");
|
throw new Error("not allow");
|
||||||
const res = await fetch(`${baseurl}`,{
|
const res = await fetch(`${baseurl}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(c),
|
body: JSON.stringify(c),
|
||||||
headers:{
|
headers: {
|
||||||
'content-type':"application/json"
|
"content-type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const ret = await res.json();
|
const ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async del(id: number): Promise<boolean>{
|
async del(id: number): Promise<boolean> {
|
||||||
const res = await fetch(`${baseurl}/${id}`,{
|
const res = await fetch(`${baseurl}/${id}`, {
|
||||||
method: "DELETE"
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
const ret = await res.json();
|
const ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async addTag(c: Document, tag_name: string): Promise<boolean>{
|
async addTag(c: Document, tag_name: string): Promise<boolean> {
|
||||||
const {id,...rest} = c;
|
const { id, ...rest } = c;
|
||||||
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{
|
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(rest),
|
body: JSON.stringify(rest),
|
||||||
headers:{
|
headers: {
|
||||||
'content-type':"application/json"
|
"content-type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const ret = await res.json();
|
const ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async delTag(c: Document, tag_name: string): Promise<boolean>{
|
async delTag(c: Document, tag_name: string): Promise<boolean> {
|
||||||
const {id,...rest} = c;
|
const { id, ...rest } = c;
|
||||||
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{
|
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify(rest),
|
body: JSON.stringify(rest),
|
||||||
headers:{
|
headers: {
|
||||||
'content-type':"application/json"
|
"content-type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const ret = await res.json();
|
const ret = await res.json();
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const CDocumentAccessor = new ClientDocumentAccessor;
|
export const CDocumentAccessor = new ClientDocumentAccessor();
|
||||||
export const makeThumbnailUrl = (x: Document)=>{
|
export const makeThumbnailUrl = (x: Document) => {
|
||||||
return `${baseurl}/${x.id}/${x.content_type}/thumbnail`;
|
return `${baseurl}/${x.id}/${x.content_type}/thumbnail`;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default CDocumentAccessor;
|
export default CDocumentAccessor;
|
@ -1,35 +1,32 @@
|
|||||||
|
type Representable = string | number | boolean;
|
||||||
type Representable = string|number|boolean;
|
|
||||||
|
|
||||||
type ToQueryStringA = {
|
type ToQueryStringA = {
|
||||||
[name:string]:Representable|Representable[]|undefined
|
[name: string]: Representable | Representable[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toQueryString = (obj:ToQueryStringA)=> {
|
export const toQueryString = (obj: ToQueryStringA) => {
|
||||||
return Object.entries(obj)
|
return Object.entries(obj)
|
||||||
.filter((e): e is [string,Representable|Representable[]] =>
|
.filter((e): e is [string, Representable | Representable[]] => e[1] !== undefined)
|
||||||
e[1] !== undefined)
|
|
||||||
.map(e =>
|
.map(e =>
|
||||||
e[1] instanceof Array
|
e[1] instanceof Array
|
||||||
? e[1].map(f=>`${e[0]}=${(f)}`).join('&')
|
? e[1].map(f => `${e[0]}=${(f)}`).join("&")
|
||||||
: `${e[0]}=${(e[1])}`)
|
: `${e[0]}=${(e[1])}`
|
||||||
.join('&');
|
)
|
||||||
}
|
.join("&");
|
||||||
export const QueryStringToMap = (query:string) =>{
|
};
|
||||||
const keyValue = query.slice(query.indexOf("?")+1).split("&");
|
export const QueryStringToMap = (query: string) => {
|
||||||
const param:{[k:string]:string|string[]} = {};
|
const keyValue = query.slice(query.indexOf("?") + 1).split("&");
|
||||||
keyValue.forEach((p)=>{
|
const param: { [k: string]: string | string[] } = {};
|
||||||
const [k,v] = p.split("=");
|
keyValue.forEach((p) => {
|
||||||
|
const [k, v] = p.split("=");
|
||||||
const pv = param[k];
|
const pv = param[k];
|
||||||
if(pv === undefined){
|
if (pv === undefined) {
|
||||||
param[k] = v;
|
param[k] = v;
|
||||||
}
|
} else if (typeof pv === "string") {
|
||||||
else if(typeof pv === "string"){
|
param[k] = [pv, v];
|
||||||
param[k] = [pv,v];
|
} else {
|
||||||
}
|
|
||||||
else{
|
|
||||||
pv.push(v);
|
pv.push(v);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return param;
|
return param;
|
||||||
}
|
};
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import React, { createContext, useEffect, useRef, useState } from 'react';
|
import { createTheme, ThemeProvider } from "@mui/material";
|
||||||
import ReactDom from 'react-dom';
|
import React, { createContext, useEffect, useRef, useState } from "react";
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
import ReactDom from "react-dom";
|
||||||
|
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Gallery,
|
DifferencePage,
|
||||||
DocumentAbout,
|
DocumentAbout,
|
||||||
|
Gallery,
|
||||||
LoginPage,
|
LoginPage,
|
||||||
NotFoundPage,
|
NotFoundPage,
|
||||||
ProfilePage,
|
ProfilePage,
|
||||||
DifferencePage,
|
|
||||||
SettingPage,
|
|
||||||
ReaderPage,
|
ReaderPage,
|
||||||
TagsPage
|
SettingPage,
|
||||||
} from './page/mod';
|
TagsPage,
|
||||||
import { getInitialValue, UserContext } from './state';
|
} from "./page/mod";
|
||||||
import { ThemeProvider, createTheme } from '@mui/material';
|
import { getInitialValue, UserContext } from "./state";
|
||||||
|
|
||||||
import './css/style.css';
|
import "./css/style.css";
|
||||||
|
|
||||||
const theme = createTheme();
|
const theme = createTheme();
|
||||||
|
|
||||||
@ -29,18 +29,20 @@ const App = () => {
|
|||||||
setUserPermission(permission);
|
setUserPermission(permission);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
//useEffect(()=>{});
|
// useEffect(()=>{});
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{
|
<UserContext.Provider
|
||||||
username: user,
|
value={{
|
||||||
setUsername: setUser,
|
username: user,
|
||||||
permission: userPermission,
|
setUsername: setUser,
|
||||||
setPermission: setUserPermission
|
permission: userPermission,
|
||||||
}}>
|
setPermission: setUserPermission,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate replace to='/search?' />} />
|
<Route path="/" element={<Navigate replace to="/search?" />} />
|
||||||
<Route path="/search" element={<Gallery />} />
|
<Route path="/search" element={<Gallery />} />
|
||||||
<Route path="/doc/:id" element={<DocumentAbout />}></Route>
|
<Route path="/doc/:id" element={<DocumentAbout />}></Route>
|
||||||
<Route path="/doc/:id/reader" element={<ReaderPage />}></Route>
|
<Route path="/doc/:id/reader" element={<ReaderPage />}></Route>
|
||||||
@ -53,10 +55,11 @@ const App = () => {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</UserContext.Provider>);
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDom.render(
|
ReactDom.render(
|
||||||
<App />,
|
<App />,
|
||||||
document.getElementById("root")
|
document.getElementById("root"),
|
||||||
);
|
);
|
@ -1,25 +1,24 @@
|
|||||||
import esbuild from 'esbuild';
|
import esbuild from "esbuild";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
const result = await esbuild.build({
|
const result = await esbuild.build({
|
||||||
entryPoints: ['app.tsx'],
|
entryPoints: ["app.tsx"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
outfile: '../../dist/bundle.js',
|
outfile: "../../dist/bundle.js",
|
||||||
platform: 'browser',
|
platform: "browser",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
minify: true,
|
minify: true,
|
||||||
target: ['chrome100', 'firefox100'],
|
target: ["chrome100", "firefox100"],
|
||||||
watch: {
|
watch: {
|
||||||
onRebuild: async (err, _result) => {
|
onRebuild: async (err, _result) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('watch build failed: ',err);
|
console.error("watch build failed: ", err);
|
||||||
|
} else {
|
||||||
|
console.log("watch build success");
|
||||||
}
|
}
|
||||||
else{
|
},
|
||||||
console.log('watch build success');
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
console.log("watching...");
|
console.log("watching...");
|
||||||
return result;
|
return result;
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
import React, { } from 'react';
|
import React, {} from "react";
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { Document } from '../accessor/document';
|
import { Document } from "../accessor/document";
|
||||||
|
|
||||||
import { Link, Paper, Theme, Box, useTheme, Typography, Grid, Button } from '@mui/material';
|
import { Box, Button, Grid, Link, Paper, Theme, Typography, useTheme } from "@mui/material";
|
||||||
import { ThumbnailContainer } from '../page/reader/reader';
|
import { TagChip } from "../component/tagchip";
|
||||||
import { TagChip } from '../component/tagchip';
|
import { ThumbnailContainer } from "../page/reader/reader";
|
||||||
|
|
||||||
import DocumentAccessor from '../accessor/document';
|
import DocumentAccessor from "../accessor/document";
|
||||||
|
|
||||||
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
|
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
|
||||||
export const makeContentReaderUrl = (id: number) => `/doc/${id}/reader`;
|
export const makeContentReaderUrl = (id: number) => `/doc/${id}/reader`;
|
||||||
|
|
||||||
const useStyles = ((theme: Theme) => ({
|
const useStyles = (theme: Theme) => ({
|
||||||
thumbnail_content: {
|
thumbnail_content: {
|
||||||
maxHeight: '400px',
|
maxHeight: "400px",
|
||||||
maxWidth: 'min(400px, 100vw)',
|
maxWidth: "min(400px, 100vw)",
|
||||||
},
|
},
|
||||||
tag_list: {
|
tag_list: {
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
justifyContent: 'flex-start',
|
justifyContent: "flex-start",
|
||||||
flexWrap: 'wrap',
|
flexWrap: "wrap",
|
||||||
overflowY: 'hidden',
|
overflowY: "hidden",
|
||||||
'& > *': {
|
"& > *": {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -32,106 +32,131 @@ const useStyles = ((theme: Theme) => ({
|
|||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
},
|
},
|
||||||
subinfoContainer: {
|
subinfoContainer: {
|
||||||
display: 'grid',
|
display: "grid",
|
||||||
gridTemplateColumns: '100px auto',
|
gridTemplateColumns: "100px auto",
|
||||||
overflowY: 'hidden',
|
overflowY: "hidden",
|
||||||
alignItems: 'baseline',
|
alignItems: "baseline",
|
||||||
},
|
},
|
||||||
short_subinfoContainer: {
|
short_subinfoContainer: {
|
||||||
[theme.breakpoints.down("md")]: {
|
[theme.breakpoints.down("md")]: {
|
||||||
display: 'none',
|
display: "none",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
short_root: {
|
short_root: {
|
||||||
overflowY: 'hidden',
|
overflowY: "hidden",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexDirection: 'column',
|
flexDirection: "column",
|
||||||
[theme.breakpoints.up("sm")]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
height: 200,
|
height: 200,
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
short_thumbnail_anchor: {
|
short_thumbnail_anchor: {
|
||||||
background: '#272733',
|
background: "#272733",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
[theme.breakpoints.up("sm")]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
width: theme.spacing(25),
|
width: theme.spacing(25),
|
||||||
height: theme.spacing(25),
|
height: theme.spacing(25),
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
short_thumbnail_content: {
|
short_thumbnail_content: {
|
||||||
maxWidth: '100%',
|
maxWidth: "100%",
|
||||||
maxHeight: '100%',
|
maxHeight: "100%",
|
||||||
},
|
},
|
||||||
}))
|
});
|
||||||
|
|
||||||
export const ContentInfo = (props: {
|
export const ContentInfo = (props: {
|
||||||
document: Document, children?: React.ReactNode, classes?: {
|
document: Document;
|
||||||
root?: string,
|
children?: React.ReactNode;
|
||||||
thumbnail_anchor?: string,
|
classes?: {
|
||||||
thumbnail_content?: string,
|
root?: string;
|
||||||
tag_list?: string,
|
thumbnail_anchor?: string;
|
||||||
title?: string,
|
thumbnail_content?: string;
|
||||||
infoContainer?: string,
|
tag_list?: string;
|
||||||
subinfoContainer?: string
|
title?: string;
|
||||||
},
|
infoContainer?: string;
|
||||||
gallery?: string,
|
subinfoContainer?: string;
|
||||||
short?: boolean
|
};
|
||||||
|
gallery?: string;
|
||||||
|
short?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const document = props.document;
|
const document = props.document;
|
||||||
const url = props.gallery === undefined ? makeContentReaderUrl(document.id) : makeContentInfoUrl(document.id);
|
const url = props.gallery === undefined ? makeContentReaderUrl(document.id) : makeContentInfoUrl(document.id);
|
||||||
return (<Paper sx={{
|
return (
|
||||||
display: "flex",
|
<Paper
|
||||||
height: "400px",
|
sx={{
|
||||||
[theme.breakpoints.down("sm")]: {
|
display: "flex",
|
||||||
flexDirection: "column",
|
height: "400px",
|
||||||
alignItems: "center",
|
[theme.breakpoints.down("sm")]: {
|
||||||
height: "auto",
|
flexDirection: "column",
|
||||||
}
|
alignItems: "center",
|
||||||
}} elevation={4}>
|
height: "auto",
|
||||||
<Link component={RouterLink} to={{
|
},
|
||||||
pathname: makeContentReaderUrl(document.id)
|
}}
|
||||||
}}>
|
elevation={4}
|
||||||
{document.deleted_at === null ?
|
>
|
||||||
(<ThumbnailContainer content={document}/>)
|
<Link
|
||||||
: (<Typography variant='h4'>Deleted</Typography>)}
|
component={RouterLink}
|
||||||
</Link>
|
to={{
|
||||||
<Box>
|
pathname: makeContentReaderUrl(document.id),
|
||||||
<Link variant='h5' color='inherit' component={RouterLink} to={{pathname: url}}>
|
}}
|
||||||
{document.title}
|
>
|
||||||
|
{document.deleted_at === null
|
||||||
|
? <ThumbnailContainer content={document} />
|
||||||
|
: <Typography variant="h4">Deleted</Typography>}
|
||||||
</Link>
|
</Link>
|
||||||
<Box>
|
<Box>
|
||||||
{props.short ? (<Box>{document.tags.map(x =>
|
<Link variant="h5" color="inherit" component={RouterLink} to={{ pathname: url }}>
|
||||||
(<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>)
|
{document.title}
|
||||||
)}</Box>) : (
|
</Link>
|
||||||
<ComicDetailTag tags={document.tags} path={document.basepath+"/"+document.filename}
|
<Box>
|
||||||
createdAt={document.created_at}
|
{props.short
|
||||||
deletedAt={document.deleted_at != null ? document.deleted_at : undefined}
|
? (
|
||||||
></ComicDetailTag>)
|
<Box>
|
||||||
}
|
{document.tags.map(x => (
|
||||||
|
<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<ComicDetailTag
|
||||||
|
tags={document.tags}
|
||||||
|
path={document.basepath + "/" + document.filename}
|
||||||
|
createdAt={document.created_at}
|
||||||
|
deletedAt={document.deleted_at != null ? document.deleted_at : undefined}
|
||||||
|
>
|
||||||
|
</ComicDetailTag>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{document.deleted_at != null
|
||||||
|
&& (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
documentDelete(document.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{document.deleted_at != null &&
|
</Paper>
|
||||||
<Button onClick={()=>{documentDelete(document.id);}}>Delete</Button>
|
);
|
||||||
}
|
};
|
||||||
</Box>
|
async function documentDelete(id: number) {
|
||||||
</Paper>);
|
|
||||||
}
|
|
||||||
async function documentDelete(id: number){
|
|
||||||
const t = await DocumentAccessor.del(id);
|
const t = await DocumentAccessor.del(id);
|
||||||
if(t){
|
if (t) {
|
||||||
alert("document deleted!");
|
alert("document deleted!");
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
alert("document already deleted.");
|
alert("document already deleted.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComicDetailTag(prop: {
|
function ComicDetailTag(prop: {
|
||||||
tags: string[];/*classes:{
|
tags: string[]; /*classes:{
|
||||||
tag_list:string
|
tag_list:string
|
||||||
}*/
|
}*/
|
||||||
path?: string;
|
path?: string;
|
||||||
@ -146,40 +171,54 @@ function ComicDetailTag(prop: {
|
|||||||
tagTable[kind] = tags;
|
tagTable[kind] = tags;
|
||||||
allTag = allTag.filter(x => !x.startsWith(kind + ":"));
|
allTag = allTag.filter(x => !x.startsWith(kind + ":"));
|
||||||
}
|
}
|
||||||
return (<Grid container>
|
return (
|
||||||
{tagKind.map(key => (
|
<Grid container>
|
||||||
<React.Fragment key={key}>
|
{tagKind.map(key => (
|
||||||
<Grid item xs={3}>
|
<React.Fragment key={key}>
|
||||||
<Typography variant='subtitle1'>{key}</Typography>
|
<Grid item xs={3}>
|
||||||
</Grid>
|
<Typography variant="subtitle1">{key}</Typography>
|
||||||
<Grid item xs={9}>
|
</Grid>
|
||||||
<Box>{tagTable[key].length !== 0 ? tagTable[key].join(", ") : "N/A"}</Box>
|
<Grid item xs={9}>
|
||||||
</Grid>
|
<Box>{tagTable[key].length !== 0 ? tagTable[key].join(", ") : "N/A"}</Box>
|
||||||
</React.Fragment>
|
</Grid>
|
||||||
))}
|
</React.Fragment>
|
||||||
{ prop.path != undefined && <><Grid item xs={3}>
|
))}
|
||||||
<Typography variant='subtitle1'>Path</Typography>
|
{prop.path != undefined && (
|
||||||
</Grid><Grid item xs={9}>
|
<>
|
||||||
<Box>{prop.path}</Box>
|
<Grid item xs={3}>
|
||||||
</Grid></>
|
<Typography variant="subtitle1">Path</Typography>
|
||||||
}
|
</Grid>
|
||||||
{ prop.createdAt != undefined && <><Grid item xs={3}>
|
<Grid item xs={9}>
|
||||||
<Typography variant='subtitle1'>CreatedAt</Typography>
|
<Box>{prop.path}</Box>
|
||||||
</Grid><Grid item xs={9}>
|
</Grid>
|
||||||
<Box>{new Date(prop.createdAt).toUTCString()}</Box>
|
</>
|
||||||
</Grid></>
|
)}
|
||||||
}
|
{prop.createdAt != undefined && (
|
||||||
{ prop.deletedAt != undefined && <><Grid item xs={3}>
|
<>
|
||||||
<Typography variant='subtitle1'>DeletedAt</Typography>
|
<Grid item xs={3}>
|
||||||
</Grid><Grid item xs={9}>
|
<Typography variant="subtitle1">CreatedAt</Typography>
|
||||||
<Box>{new Date(prop.deletedAt).toUTCString()}</Box>
|
</Grid>
|
||||||
</Grid></>
|
<Grid item xs={9}>
|
||||||
}
|
<Box>{new Date(prop.createdAt).toUTCString()}</Box>
|
||||||
<Grid item xs={3}>
|
</Grid>
|
||||||
<Typography variant='subtitle1'>Tags</Typography>
|
</>
|
||||||
|
)}
|
||||||
|
{prop.deletedAt != undefined && (
|
||||||
|
<>
|
||||||
|
<Grid item xs={3}>
|
||||||
|
<Typography variant="subtitle1">DeletedAt</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={9}>
|
||||||
|
<Box>{new Date(prop.deletedAt).toUTCString()}</Box>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Grid item xs={3}>
|
||||||
|
<Typography variant="subtitle1">Tags</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={9}>
|
||||||
|
{allTag.map(x => <TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>)}
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={9}>
|
);
|
||||||
{allTag.map(x => (<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>))}
|
|
||||||
</Grid>
|
|
||||||
</Grid>);
|
|
||||||
}
|
}
|
@ -1,21 +1,35 @@
|
|||||||
import React, { useContext, useState } from 'react';
|
import { AccountCircle, ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Button, CssBaseline, Divider, IconButton, List, ListItem, Drawer,
|
AppBar,
|
||||||
AppBar, Toolbar, Typography, InputBase, ListItemIcon, ListItemText, Menu, MenuItem,
|
Button,
|
||||||
Hidden, Tooltip, Link, styled
|
CssBaseline,
|
||||||
} from '@mui/material';
|
Divider,
|
||||||
import { alpha, Theme, useTheme } from '@mui/material/styles';
|
Drawer,
|
||||||
import {
|
Hidden,
|
||||||
ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon, AccountCircle
|
IconButton,
|
||||||
} from '@mui/icons-material';
|
InputBase,
|
||||||
|
Link,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
styled,
|
||||||
|
Toolbar,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { alpha, Theme, useTheme } from "@mui/material/styles";
|
||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
|
||||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import { doLogout, UserContext } from '../state';
|
import { doLogout, UserContext } from "../state";
|
||||||
|
|
||||||
const drawerWidth = 270;
|
const drawerWidth = 270;
|
||||||
|
|
||||||
const DrawerHeader = styled('div')(({ theme }) => ({
|
const DrawerHeader = styled("div")(({ theme }) => ({
|
||||||
...theme.mixins.toolbar
|
...theme.mixins.toolbar,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledDrawer = styled(Drawer)(({ theme }) => ({
|
const StyledDrawer = styled(Drawer)(({ theme }) => ({
|
||||||
@ -24,58 +38,56 @@ const StyledDrawer = styled(Drawer)(({ theme }) => ({
|
|||||||
[theme.breakpoints.up("sm")]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
width: drawerWidth,
|
width: drawerWidth,
|
||||||
},
|
},
|
||||||
}
|
}));
|
||||||
));
|
const StyledSearchBar = styled("div")(({ theme }) => ({
|
||||||
const StyledSearchBar = styled('div')(({ theme }) => ({
|
position: "relative",
|
||||||
position: 'relative',
|
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
backgroundColor: alpha(theme.palette.common.white, 0.15),
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
backgroundColor: alpha(theme.palette.common.white, 0.25),
|
backgroundColor: alpha(theme.palette.common.white, 0.25),
|
||||||
},
|
},
|
||||||
marginLeft: 0,
|
marginLeft: 0,
|
||||||
width: '100%',
|
width: "100%",
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
marginLeft: theme.spacing(1),
|
marginLeft: theme.spacing(1),
|
||||||
width: 'auto',
|
width: "auto",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
const StyledInputBase = styled(InputBase)(({ theme }) => ({
|
||||||
color: 'inherit',
|
color: "inherit",
|
||||||
'& .MuiInputBase-input': {
|
"& .MuiInputBase-input": {
|
||||||
padding: theme.spacing(1, 1, 1, 0),
|
padding: theme.spacing(1, 1, 1, 0),
|
||||||
// vertical padding + font size from searchIcon
|
// vertical padding + font size from searchIcon
|
||||||
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
|
||||||
transition: theme.transitions.create('width'),
|
transition: theme.transitions.create("width"),
|
||||||
width: '100%',
|
width: "100%",
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
width: '12ch',
|
width: "12ch",
|
||||||
'&:focus': {
|
"&:focus": {
|
||||||
width: '20ch',
|
width: "20ch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledNav = styled('nav')(({theme}) => ({
|
const StyledNav = styled("nav")(({ theme }) => ({
|
||||||
[theme.breakpoints.up("sm")]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
width: theme.spacing(7)
|
width: theme.spacing(7),
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const closedMixin = (theme: Theme) => ({
|
const closedMixin = (theme: Theme) => ({
|
||||||
overflowX: 'hidden',
|
overflowX: "hidden",
|
||||||
width: `calc(${theme.spacing(7)} + 1px)`,
|
width: `calc(${theme.spacing(7)} + 1px)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export const Headline = (prop: {
|
export const Headline = (prop: {
|
||||||
children?: React.ReactNode,
|
children?: React.ReactNode;
|
||||||
classes?: {
|
classes?: {
|
||||||
content?: string,
|
content?: string;
|
||||||
toolbar?: string,
|
toolbar?: string;
|
||||||
},
|
};
|
||||||
menu: React.ReactNode
|
menu: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const [v, setv] = useState(false);
|
const [v, setv] = useState(false);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
@ -84,136 +96,177 @@ export const Headline = (prop: {
|
|||||||
const handleProfileMenuOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget);
|
const handleProfileMenuOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget);
|
||||||
const handleProfileMenuClose = () => setAnchorEl(null);
|
const handleProfileMenuClose = () => setAnchorEl(null);
|
||||||
const isProfileMenuOpened = Boolean(anchorEl);
|
const isProfileMenuOpened = Boolean(anchorEl);
|
||||||
const menuId = 'primary-search-account-menu';
|
const menuId = "primary-search-account-menu";
|
||||||
const user_ctx = useContext(UserContext);
|
const user_ctx = useContext(UserContext);
|
||||||
const isLogin = user_ctx.username !== "";
|
const isLogin = user_ctx.username !== "";
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const renderProfileMenu = (<Menu
|
const renderProfileMenu = (
|
||||||
anchorEl={anchorEl}
|
<Menu
|
||||||
anchorOrigin={{ horizontal: 'right', vertical: "top" }}
|
anchorEl={anchorEl}
|
||||||
id={menuId}
|
anchorOrigin={{ horizontal: "right", vertical: "top" }}
|
||||||
open={isProfileMenuOpened}
|
id={menuId}
|
||||||
keepMounted
|
open={isProfileMenuOpened}
|
||||||
transformOrigin={{ horizontal: 'right', vertical: "top" }}
|
keepMounted
|
||||||
onClose={handleProfileMenuClose}
|
transformOrigin={{ horizontal: "right", vertical: "top" }}
|
||||||
>
|
onClose={handleProfileMenuClose}
|
||||||
<MenuItem component={RouterLink} to='/profile'>Profile</MenuItem>
|
>
|
||||||
<MenuItem onClick={async () => { handleProfileMenuClose(); await doLogout(); user_ctx.setUsername(""); }}>Logout</MenuItem>
|
<MenuItem component={RouterLink} to="/profile">Profile</MenuItem>
|
||||||
</Menu>);
|
<MenuItem
|
||||||
const drawer_contents = (<>
|
onClick={async () => {
|
||||||
<DrawerHeader>
|
handleProfileMenuClose();
|
||||||
<IconButton onClick={toggleV}>
|
await doLogout();
|
||||||
{theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />}
|
user_ctx.setUsername("");
|
||||||
</IconButton>
|
}}
|
||||||
</DrawerHeader>
|
>
|
||||||
<Divider />
|
Logout
|
||||||
{prop.menu}
|
</MenuItem>
|
||||||
</>);
|
</Menu>
|
||||||
|
);
|
||||||
return (<div style={{ display: 'flex' }}>
|
const drawer_contents = (
|
||||||
<CssBaseline />
|
<>
|
||||||
<AppBar position="fixed" sx={{
|
<DrawerHeader>
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
<IconButton onClick={toggleV}>
|
||||||
transition: theme.transitions.create(['width', 'margin'], {
|
{theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />}
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.leavingScreen
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<Toolbar>
|
|
||||||
<IconButton color="inherit"
|
|
||||||
aria-label="open drawer"
|
|
||||||
onClick={toggleV}
|
|
||||||
edge="start"
|
|
||||||
style={{ marginRight: 36 }}
|
|
||||||
>
|
|
||||||
<MenuIcon></MenuIcon>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Link variant="h5" noWrap sx={{
|
</DrawerHeader>
|
||||||
display: 'none',
|
<Divider />
|
||||||
[theme.breakpoints.up("sm")]: {
|
{prop.menu}
|
||||||
display: 'block'
|
</>
|
||||||
}
|
);
|
||||||
}} color="inherit" component={RouterLink} to="/">
|
|
||||||
Ionian
|
return (
|
||||||
</Link>
|
<div style={{ display: "flex" }}>
|
||||||
<div style={{ flexGrow: 1 }}></div>
|
<CssBaseline />
|
||||||
<StyledSearchBar >
|
<AppBar
|
||||||
<div style={{
|
position="fixed"
|
||||||
padding: theme.spacing(0, 2),
|
sx={{
|
||||||
height: '100%',
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
position: 'absolute',
|
transition: theme.transitions.create(["width", "margin"], {
|
||||||
pointerEvents: 'none',
|
easing: theme.transitions.easing.sharp,
|
||||||
display: 'flex',
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
alignItems: 'center',
|
}),
|
||||||
justifyContent: 'center'
|
}}
|
||||||
}}>
|
>
|
||||||
<SearchIcon onClick={() => navSearch(search)} />
|
<Toolbar>
|
||||||
</div>
|
<IconButton
|
||||||
<StyledInputBase placeholder="search"
|
color="inherit"
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
aria-label="open drawer"
|
||||||
onKeyUp={(e) => {
|
onClick={toggleV}
|
||||||
if (e.key === "Enter") {
|
edge="start"
|
||||||
navSearch(search);
|
style={{ marginRight: 36 }}
|
||||||
}
|
>
|
||||||
|
<MenuIcon></MenuIcon>
|
||||||
|
</IconButton>
|
||||||
|
<Link
|
||||||
|
variant="h5"
|
||||||
|
noWrap
|
||||||
|
sx={{
|
||||||
|
display: "none",
|
||||||
|
[theme.breakpoints.up("sm")]: {
|
||||||
|
display: "block",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
value={search}></StyledInputBase>
|
color="inherit"
|
||||||
</StyledSearchBar>
|
component={RouterLink}
|
||||||
{
|
to="/"
|
||||||
isLogin ?
|
>
|
||||||
<IconButton
|
Ionian
|
||||||
edge="end"
|
</Link>
|
||||||
aria-label="account of current user"
|
<div style={{ flexGrow: 1 }}></div>
|
||||||
aria-controls={menuId}
|
<StyledSearchBar>
|
||||||
aria-haspopup="true"
|
<div
|
||||||
onClick={handleProfileMenuOpen}
|
style={{
|
||||||
color="inherit">
|
padding: theme.spacing(0, 2),
|
||||||
<AccountCircle />
|
height: "100%",
|
||||||
</IconButton>
|
position: "absolute",
|
||||||
: <Button color="inherit" component={RouterLink} to="/login">Login</Button>
|
pointerEvents: "none",
|
||||||
}
|
display: "flex",
|
||||||
</Toolbar>
|
alignItems: "center",
|
||||||
</AppBar>
|
justifyContent: "center",
|
||||||
{renderProfileMenu}
|
}}
|
||||||
<StyledNav>
|
>
|
||||||
<Hidden smUp implementation="css">
|
<SearchIcon onClick={() => navSearch(search)} />
|
||||||
<StyledDrawer variant="temporary" anchor='left' open={v} onClose={toggleV}
|
</div>
|
||||||
sx={{
|
<StyledInputBase
|
||||||
width: drawerWidth
|
placeholder="search"
|
||||||
}}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
>
|
onKeyUp={(e) => {
|
||||||
{drawer_contents}
|
if (e.key === "Enter") {
|
||||||
</StyledDrawer>
|
navSearch(search);
|
||||||
</Hidden>
|
}
|
||||||
<Hidden smDown implementation="css">
|
}}
|
||||||
<StyledDrawer variant='permanent' anchor='left'
|
value={search}
|
||||||
sx={{
|
>
|
||||||
...closedMixin(theme),
|
</StyledInputBase>
|
||||||
'& .MuiDrawer-paper': closedMixin(theme),
|
</StyledSearchBar>
|
||||||
}}>
|
{isLogin
|
||||||
{drawer_contents}
|
? (
|
||||||
</StyledDrawer>
|
<IconButton
|
||||||
</Hidden>
|
edge="end"
|
||||||
</StyledNav>
|
aria-label="account of current user"
|
||||||
<main style={{
|
aria-controls={menuId}
|
||||||
display: 'flex',
|
aria-haspopup="true"
|
||||||
flexFlow: 'column',
|
onClick={handleProfileMenuOpen}
|
||||||
flexGrow: 1,
|
color="inherit"
|
||||||
padding: theme.spacing(3),
|
>
|
||||||
marginTop: theme.spacing(6),
|
<AccountCircle />
|
||||||
}}>
|
</IconButton>
|
||||||
<div style={{
|
)
|
||||||
}} ></div>
|
: <Button color="inherit" component={RouterLink} to="/login">Login</Button>}
|
||||||
{prop.children}
|
</Toolbar>
|
||||||
</main>
|
</AppBar>
|
||||||
</div>);
|
{renderProfileMenu}
|
||||||
function navSearch(search: string){
|
<StyledNav>
|
||||||
|
<Hidden smUp implementation="css">
|
||||||
|
<StyledDrawer
|
||||||
|
variant="temporary"
|
||||||
|
anchor="left"
|
||||||
|
open={v}
|
||||||
|
onClose={toggleV}
|
||||||
|
sx={{
|
||||||
|
width: drawerWidth,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawer_contents}
|
||||||
|
</StyledDrawer>
|
||||||
|
</Hidden>
|
||||||
|
<Hidden smDown implementation="css">
|
||||||
|
<StyledDrawer
|
||||||
|
variant="permanent"
|
||||||
|
anchor="left"
|
||||||
|
sx={{
|
||||||
|
...closedMixin(theme),
|
||||||
|
"& .MuiDrawer-paper": closedMixin(theme),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{drawer_contents}
|
||||||
|
</StyledDrawer>
|
||||||
|
</Hidden>
|
||||||
|
</StyledNav>
|
||||||
|
<main
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexFlow: "column",
|
||||||
|
flexGrow: 1,
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
marginTop: theme.spacing(6),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{}}></div>
|
||||||
|
{prop.children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
function navSearch(search: string) {
|
||||||
let words = search.includes("&") ? search.split("&") : [search];
|
let words = search.includes("&") ? search.split("&") : [search];
|
||||||
words = words.map(w => w.trim())
|
words = words.map(w => w.trim())
|
||||||
.map(w => w.includes(":") ?
|
.map(w =>
|
||||||
`allow_tag=${w}`
|
w.includes(":")
|
||||||
: `word=${encodeURIComponent(w)}`);
|
? `allow_tag=${w}`
|
||||||
|
: `word=${encodeURIComponent(w)}`
|
||||||
|
);
|
||||||
navigate(`/search?${words.join("&")}`);
|
navigate(`/search?${words.join("&")}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import { Box, CircularProgress } from "@mui/material";
|
||||||
import {Box, CircularProgress} from '@mui/material';
|
import React from "react";
|
||||||
|
|
||||||
export const LoadingCircle = ()=>{
|
export const LoadingCircle = () => {
|
||||||
return (<Box style={{position:"absolute", top:"50%", left:"50%", transform:"translate(-50%,-50%)"}}>
|
return (
|
||||||
<CircularProgress title="loading" />
|
<Box style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)" }}>
|
||||||
</Box>);
|
<CircularProgress title="loading" />
|
||||||
}
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
export * from './contentinfo';
|
export * from "./contentinfo";
|
||||||
export * from './loading';
|
export * from "./headline";
|
||||||
export * from './tagchip';
|
export * from "./loading";
|
||||||
export * from './navlist';
|
export * from "./navlist";
|
||||||
export * from './headline';
|
export * from "./tagchip";
|
||||||
|
@ -1,43 +1,58 @@
|
|||||||
import React from 'react';
|
import {
|
||||||
import {List, ListItem, ListItemIcon, Tooltip, ListItemText, Divider} from '@mui/material';
|
ArrowBack as ArrowBackIcon,
|
||||||
import {ArrowBack as ArrowBackIcon, Settings as SettingIcon,
|
Collections as CollectionIcon,
|
||||||
Collections as CollectionIcon, VideoLibrary as VideoIcon, Home as HomeIcon,
|
Folder as FolderIcon,
|
||||||
|
Home as HomeIcon,
|
||||||
List as ListIcon,
|
List as ListIcon,
|
||||||
Folder as FolderIcon } from '@mui/icons-material';
|
Settings as SettingIcon,
|
||||||
import {Link as RouterLink} from 'react-router-dom';
|
VideoLibrary as VideoIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { Divider, List, ListItem, ListItemIcon, ListItemText, Tooltip } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
export const NavItem = (props:{name:string,to:string, icon:React.ReactElement<any,any>})=>{
|
export const NavItem = (props: { name: string; to: string; icon: React.ReactElement<any, any> }) => {
|
||||||
return (<ListItem button key={props.name} component={RouterLink} to={props.to}>
|
return (
|
||||||
<ListItemIcon>
|
<ListItem button key={props.name} component={RouterLink} to={props.to}>
|
||||||
<Tooltip title={props.name.toLocaleLowerCase()} placement="bottom">
|
<ListItemIcon>
|
||||||
{props.icon}
|
<Tooltip title={props.name.toLocaleLowerCase()} placement="bottom">
|
||||||
</Tooltip>
|
{props.icon}
|
||||||
</ListItemIcon>
|
</Tooltip>
|
||||||
<ListItemText primary={props.name}></ListItemText>
|
</ListItemIcon>
|
||||||
</ListItem>);
|
<ListItemText primary={props.name}></ListItemText>
|
||||||
}
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NavList = (props: {children?:React.ReactNode})=>{
|
export const NavList = (props: { children?: React.ReactNode }) => {
|
||||||
return (<List>
|
return (
|
||||||
|
<List>
|
||||||
{props.children}
|
{props.children}
|
||||||
</List>);
|
</List>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const BackItem = (props:{to?:string})=>{
|
export const BackItem = (props: { to?: string }) => {
|
||||||
return <NavItem name="Back" to={props.to ?? "/"} icon={<ArrowBackIcon></ArrowBackIcon>}/>;
|
return <NavItem name="Back" to={props.to ?? "/"} icon={<ArrowBackIcon></ArrowBackIcon>} />;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function CommonMenuList(props?:{url?:string}) {
|
export function CommonMenuList(props?: { url?: string }) {
|
||||||
let url = props?.url ?? "";
|
let url = props?.url ?? "";
|
||||||
return (<NavList>
|
return (
|
||||||
{url !== "" && <><BackItem to={url} /> <Divider /></>}
|
<NavList>
|
||||||
<NavItem name="All" to="/" icon={<HomeIcon />} />
|
{url !== "" && (
|
||||||
<NavItem name="Comic" to="/search?content_type=comic" icon={<CollectionIcon />}></NavItem>
|
<>
|
||||||
<NavItem name="Video" to="/search?content_type=video" icon={<VideoIcon />} />
|
<BackItem to={url} /> <Divider />
|
||||||
<Divider />
|
</>
|
||||||
<NavItem name="Tags" to="/tags" icon={<ListIcon/>}/>
|
)}
|
||||||
<Divider />
|
<NavItem name="All" to="/" icon={<HomeIcon />} />
|
||||||
<NavItem name="Difference" to="/difference" icon={<FolderIcon/>}></NavItem>
|
<NavItem name="Comic" to="/search?content_type=comic" icon={<CollectionIcon />}></NavItem>
|
||||||
<NavItem name="Settings" to="/setting" icon={<SettingIcon />} />
|
<NavItem name="Video" to="/search?content_type=video" icon={<VideoIcon />} />
|
||||||
</NavList>);
|
<Divider />
|
||||||
|
<NavItem name="Tags" to="/tags" icon={<ListIcon />} />
|
||||||
|
<Divider />
|
||||||
|
<NavItem name="Difference" to="/difference" icon={<FolderIcon />}></NavItem>
|
||||||
|
<NavItem name="Settings" to="/setting" icon={<SettingIcon />} />
|
||||||
|
</NavList>
|
||||||
|
);
|
||||||
}
|
}
|
@ -1,86 +1,95 @@
|
|||||||
import React from 'react';
|
import { Chip, colors } from "@mui/material";
|
||||||
import {ChipTypeMap} from '@mui/material/Chip';
|
import { ChipTypeMap } from "@mui/material/Chip";
|
||||||
import { Chip, colors } from '@mui/material';
|
import { emphasize, Theme } from "@mui/material/styles";
|
||||||
import { Theme, emphasize} from '@mui/material/styles';
|
import React from "react";
|
||||||
import {Link as RouterLink} from 'react-router-dom';
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
type TagChipStyleProp = {
|
type TagChipStyleProp = {
|
||||||
color: string
|
color: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const useTagStyles = ((theme:Theme)=>({
|
const useTagStyles = (theme: Theme) => ({
|
||||||
root:(props:TagChipStyleProp)=>({
|
root: (props: TagChipStyleProp) => ({
|
||||||
color: theme.palette.getContrastText(props.color),
|
color: theme.palette.getContrastText(props.color),
|
||||||
backgroundColor: props.color,
|
backgroundColor: props.color,
|
||||||
}),
|
}),
|
||||||
clickable:(props:TagChipStyleProp)=>({
|
clickable: (props: TagChipStyleProp) => ({
|
||||||
'&:hover, &:focus':{
|
"&:hover, &:focus": {
|
||||||
backgroundColor:emphasize(props.color,0.08)
|
backgroundColor: emphasize(props.color, 0.08),
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
deletable: {
|
deletable: {
|
||||||
'&:focus': {
|
"&:focus": {
|
||||||
backgroundColor: (props:TagChipStyleProp)=>emphasize(props.color, 0.2),
|
backgroundColor: (props: TagChipStyleProp) => emphasize(props.color, 0.2),
|
||||||
}
|
|
||||||
},
|
|
||||||
outlined:{
|
|
||||||
color: (props:TagChipStyleProp)=>props.color,
|
|
||||||
border: (props:TagChipStyleProp)=> `1px solid ${props.color}`,
|
|
||||||
'$clickable&:hover, $clickable&:focus, $deletable&:focus': {
|
|
||||||
//backgroundColor:(props:TagChipStyleProp)=> (props.color,theme.palette.action.hoverOpacity),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
icon:{
|
outlined: {
|
||||||
color:"inherit",
|
color: (props: TagChipStyleProp) => props.color,
|
||||||
|
border: (props: TagChipStyleProp) => `1px solid ${props.color}`,
|
||||||
|
"$clickable&:hover, $clickable&:focus, $deletable&:focus": {
|
||||||
|
// backgroundColor:(props:TagChipStyleProp)=> (props.color,theme.palette.action.hoverOpacity),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
deleteIcon:{
|
icon: {
|
||||||
//color:(props:TagChipStyleProp)=> (theme.palette.getContrastText(props.color),0.7),
|
color: "inherit",
|
||||||
"&:hover, &:active":{
|
},
|
||||||
color:(props:TagChipStyleProp)=>theme.palette.getContrastText(props.color),
|
deleteIcon: {
|
||||||
}
|
// color:(props:TagChipStyleProp)=> (theme.palette.getContrastText(props.color),0.7),
|
||||||
}
|
"&:hover, &:active": {
|
||||||
}));
|
color: (props: TagChipStyleProp) => theme.palette.getContrastText(props.color),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const {blue, pink} = colors;
|
const { blue, pink } = colors;
|
||||||
const getTagColorName = (tagname :string):string=>{
|
const getTagColorName = (tagname: string): string => {
|
||||||
if(tagname.startsWith("female")){
|
if (tagname.startsWith("female")) {
|
||||||
return pink[600];
|
return pink[600];
|
||||||
}
|
} else if (tagname.startsWith("male")) {
|
||||||
else if(tagname.startsWith("male")){
|
|
||||||
return blue[600];
|
return blue[600];
|
||||||
}
|
} else return "default";
|
||||||
else return "default";
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type ColorChipProp = Omit<ChipTypeMap['props'],"color"> & TagChipStyleProp & {
|
type ColorChipProp = Omit<ChipTypeMap["props"], "color"> & TagChipStyleProp & {
|
||||||
component?: React.ElementType,
|
component?: React.ElementType;
|
||||||
to?: string
|
to?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ColorChip = (props:ColorChipProp)=>{
|
export const ColorChip = (props: ColorChipProp) => {
|
||||||
const {color,...rest} = props;
|
const { color, ...rest } = props;
|
||||||
//const classes = useTagStyles({color : color !== "default" ? color : "#000"});
|
// const classes = useTagStyles({color : color !== "default" ? color : "#000"});
|
||||||
return <Chip color="default" {...rest}></Chip>;
|
return <Chip color="default" {...rest}></Chip>;
|
||||||
}
|
};
|
||||||
|
|
||||||
type TagChipProp = Omit<ChipTypeMap['props'],"color"> & {
|
type TagChipProp = Omit<ChipTypeMap["props"], "color"> & {
|
||||||
tagname:string
|
tagname: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const TagChip = (props:TagChipProp)=>{
|
export const TagChip = (props: TagChipProp) => {
|
||||||
const {tagname,label,clickable,...rest} = props;
|
const { tagname, label, clickable, ...rest } = props;
|
||||||
let newlabel:string|undefined = undefined;
|
let newlabel: string | undefined = undefined;
|
||||||
if(typeof label === "string"){
|
if (typeof label === "string") {
|
||||||
if(label.startsWith("female:")){
|
if (label.startsWith("female:")) {
|
||||||
newlabel ="♀ "+label.slice(7);
|
newlabel = "♀ " + label.slice(7);
|
||||||
}
|
} else if (label.startsWith("male:")) {
|
||||||
else if(label.startsWith("male:")){
|
newlabel = "♂ " + label.slice(5);
|
||||||
newlabel = "♂ "+label.slice(5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const inner = clickable ?
|
const inner = clickable
|
||||||
(<ColorChip color={getTagColorName(tagname)} clickable={clickable} label={newlabel??label} {...rest}
|
? (
|
||||||
component={RouterLink} to={`/search?allow_tag=${tagname}`}></ColorChip>):
|
<ColorChip
|
||||||
(<ColorChip color={getTagColorName(tagname)} clickable={clickable} label={newlabel??label} {...rest}></ColorChip>);
|
color={getTagColorName(tagname)}
|
||||||
|
clickable={clickable}
|
||||||
|
label={newlabel ?? label}
|
||||||
|
{...rest}
|
||||||
|
component={RouterLink}
|
||||||
|
to={`/search?allow_tag=${tagname}`}
|
||||||
|
>
|
||||||
|
</ColorChip>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<ColorChip color={getTagColorName(tagname)} clickable={clickable} label={newlabel ?? label} {...rest}>
|
||||||
|
</ColorChip>
|
||||||
|
);
|
||||||
return inner;
|
return inner;
|
||||||
}
|
};
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "ionian_client",
|
"name": "ionian_client",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "client of ionian",
|
"description": "client of ionian",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:watch": "ts-node build.ts"
|
"build:watch": "ts-node build.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.9.0",
|
"@emotion/react": "^11.9.0",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/styled": "^11.8.1",
|
||||||
"@mui/icons-material": "^5.6.2",
|
"@mui/icons-material": "^5.6.2",
|
||||||
"@mui/material": "^5.6.2",
|
"@mui/material": "^5.6.2",
|
||||||
"@mui/x-data-grid": "^5.12.3",
|
"@mui/x-data-grid": "^5.12.3",
|
||||||
"@types/react": "^18.0.5",
|
"@types/react": "^18.0.5",
|
||||||
"@types/react-dom": "^18.0.1",
|
"@types/react-dom": "^18.0.1",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
"react-router-dom": "^6.3.0"
|
"react-router-dom": "^6.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.14.36",
|
"esbuild": "^0.14.36",
|
||||||
"ts-node": "^10.7.0"
|
"ts-node": "^10.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import { ArrowBack as ArrowBackIcon } from "@mui/icons-material";
|
||||||
import {Typography} from '@mui/material';
|
import { Typography } from "@mui/material";
|
||||||
import {ArrowBack as ArrowBackIcon} from '@mui/icons-material';
|
import React from "react";
|
||||||
import { Headline, BackItem, NavList, CommonMenuList } from '../component/mod';
|
import { BackItem, CommonMenuList, Headline, NavList } from "../component/mod";
|
||||||
|
|
||||||
export const NotFoundPage = ()=>{
|
export const NotFoundPage = () => {
|
||||||
const menu = CommonMenuList();
|
const menu = CommonMenuList();
|
||||||
return <Headline menu={menu}>
|
return (
|
||||||
<Typography variant='h2'>404 Not Found</Typography>
|
<Headline menu={menu}>
|
||||||
</Headline>
|
<Typography variant="h2">404 Not Found</Typography>
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
};
|
};
|
@ -1,31 +1,31 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import { Theme, Typography } from "@mui/material";
|
||||||
import { Route, Routes, useLocation, useParams } from 'react-router-dom';
|
import React, { useEffect, useState } from "react";
|
||||||
import DocumentAccessor, { Document } from '../accessor/document';
|
import { Route, Routes, useLocation, useParams } from "react-router-dom";
|
||||||
import { LoadingCircle } from '../component/loading';
|
import DocumentAccessor, { Document } from "../accessor/document";
|
||||||
import { Theme, Typography } from '@mui/material';
|
import { LoadingCircle } from "../component/loading";
|
||||||
import { getPresenter } from './reader/reader';
|
import { CommonMenuList, ContentInfo, Headline } from "../component/mod";
|
||||||
import { CommonMenuList, ContentInfo, Headline } from '../component/mod';
|
import { NotFoundPage } from "./404";
|
||||||
import { NotFoundPage } from './404';
|
import { getPresenter } from "./reader/reader";
|
||||||
|
|
||||||
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
|
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
|
||||||
export const makeComicReaderUrl = (id: number) => `/doc/${id}/reader`;
|
export const makeComicReaderUrl = (id: number) => `/doc/${id}/reader`;
|
||||||
|
|
||||||
type DocumentState = {
|
type DocumentState = {
|
||||||
doc: Document | undefined,
|
doc: Document | undefined;
|
||||||
notfound: boolean,
|
notfound: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
const styles = ((theme: Theme) => ({
|
const styles = (theme: Theme) => ({
|
||||||
noPaddingContent: {
|
noPaddingContent: {
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexDirection: 'column',
|
flexDirection: "column",
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
},
|
},
|
||||||
noPaddingToolbar: {
|
noPaddingToolbar: {
|
||||||
flex: '0 1 auto',
|
flex: "0 1 auto",
|
||||||
...theme.mixins.toolbar,
|
...theme.mixins.toolbar,
|
||||||
}
|
},
|
||||||
}));
|
});
|
||||||
|
|
||||||
export function ReaderPage(props?: {}) {
|
export function ReaderPage(props?: {}) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -49,28 +49,28 @@ export function ReaderPage(props?: {}) {
|
|||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return (
|
return (
|
||||||
<Headline menu={menu_list()}>
|
<Headline menu={menu_list()}>
|
||||||
<Typography variant='h2'>Oops. Invalid ID</Typography>
|
<Typography variant="h2">Oops. Invalid ID</Typography>
|
||||||
</Headline>
|
</Headline>
|
||||||
);
|
);
|
||||||
}
|
} else if (info.notfound) {
|
||||||
else if (info.notfound) {
|
|
||||||
return (
|
return (
|
||||||
<Headline menu={menu_list()}>
|
<Headline menu={menu_list()}>
|
||||||
<Typography variant='h2'>Content has been removed.</Typography>
|
<Typography variant="h2">Content has been removed.</Typography>
|
||||||
</Headline>
|
</Headline>
|
||||||
)
|
|
||||||
}
|
|
||||||
else if (info.doc === undefined) {
|
|
||||||
return (<Headline menu={menu_list()}>
|
|
||||||
<LoadingCircle />
|
|
||||||
</Headline>
|
|
||||||
);
|
);
|
||||||
}
|
} else if (info.doc === undefined) {
|
||||||
else {
|
return (
|
||||||
|
<Headline menu={menu_list()}>
|
||||||
|
<LoadingCircle />
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const ReaderPage = getPresenter(info.doc);
|
const ReaderPage = getPresenter(info.doc);
|
||||||
return <Headline menu={menu_list(location.pathname)}>
|
return (
|
||||||
<ReaderPage doc={info.doc}></ReaderPage>
|
<Headline menu={menu_list(location.pathname)}>
|
||||||
</Headline>
|
<ReaderPage doc={info.doc}></ReaderPage>
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,28 +95,26 @@ export const DocumentAbout = (prop?: {}) => {
|
|||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
return (
|
return (
|
||||||
<Headline menu={menu_list()}>
|
<Headline menu={menu_list()}>
|
||||||
<Typography variant='h2'>Oops. Invalid ID</Typography>
|
<Typography variant="h2">Oops. Invalid ID</Typography>
|
||||||
</Headline>
|
</Headline>
|
||||||
);
|
);
|
||||||
}
|
} else if (info.notfound) {
|
||||||
else if (info.notfound) {
|
|
||||||
return (
|
return (
|
||||||
<Headline menu={menu_list()}>
|
<Headline menu={menu_list()}>
|
||||||
<Typography variant='h2'>Content has been removed.</Typography>
|
<Typography variant="h2">Content has been removed.</Typography>
|
||||||
</Headline>
|
</Headline>
|
||||||
)
|
|
||||||
}
|
|
||||||
else if (info.doc === undefined) {
|
|
||||||
return (<Headline menu={menu_list()}>
|
|
||||||
<LoadingCircle />
|
|
||||||
</Headline>
|
|
||||||
);
|
);
|
||||||
}
|
} else if (info.doc === undefined) {
|
||||||
else {
|
return (
|
||||||
|
<Headline menu={menu_list()}>
|
||||||
|
<LoadingCircle />
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
return (
|
return (
|
||||||
<Headline menu={menu_list()}>
|
<Headline menu={menu_list()}>
|
||||||
<ContentInfo document={info.doc}></ContentInfo>
|
<ContentInfo document={info.doc}></ContentInfo>
|
||||||
</Headline>
|
</Headline>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -1,127 +1,141 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import { Box, Button, Grid, Paper, Theme, Typography } from "@mui/material";
|
||||||
|
import { Stack } from "@mui/material";
|
||||||
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { CommonMenuList, Headline } from "../component/mod";
|
import { CommonMenuList, Headline } from "../component/mod";
|
||||||
import { UserContext } from "../state";
|
import { UserContext } from "../state";
|
||||||
import { Box, Grid, Paper, Typography,Button, Theme } from "@mui/material";
|
|
||||||
import {Stack} from '@mui/material';
|
|
||||||
|
|
||||||
const useStyles = ((theme:Theme)=>({
|
const useStyles = (theme: Theme) => ({
|
||||||
paper:{
|
paper: {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
},
|
},
|
||||||
commitable:{
|
commitable: {
|
||||||
display:'grid',
|
display: "grid",
|
||||||
gridTemplateColumns: `100px auto`,
|
gridTemplateColumns: `100px auto`,
|
||||||
},
|
},
|
||||||
contentTitle:{
|
contentTitle: {
|
||||||
marginLeft: theme.spacing(2)
|
marginLeft: theme.spacing(2),
|
||||||
}
|
},
|
||||||
}));
|
});
|
||||||
type FileDifference = {
|
type FileDifference = {
|
||||||
type:string,
|
type: string;
|
||||||
value:{
|
value: {
|
||||||
type:string,
|
type: string;
|
||||||
path:string,
|
path: string;
|
||||||
}[]
|
}[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
function TypeDifference(prop: {
|
||||||
function TypeDifference(prop:{
|
content: FileDifference;
|
||||||
content:FileDifference,
|
onCommit: (v: { type: string; path: string }) => void;
|
||||||
onCommit:(v:{type:string,path:string})=>void,
|
onCommitAll: (type: string) => void;
|
||||||
onCommitAll:(type:string) => void
|
}) {
|
||||||
}){
|
// const classes = useStyles();
|
||||||
//const classes = useStyles();
|
|
||||||
const x = prop.content;
|
const x = prop.content;
|
||||||
const [button_disable,set_disable] = useState(false);
|
const [button_disable, set_disable] = useState(false);
|
||||||
|
|
||||||
return (<Paper /*className={classes.paper}*/>
|
return (
|
||||||
<Box /*className={classes.contentTitle}*/>
|
<Paper /*className={classes.paper}*/>
|
||||||
<Typography variant='h3' >{x.type}</Typography>
|
<Box /*className={classes.contentTitle}*/>
|
||||||
<Button variant="contained" key={x.type} onClick={()=>{
|
<Typography variant="h3">{x.type}</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
key={x.type}
|
||||||
|
onClick={() => {
|
||||||
set_disable(true);
|
set_disable(true);
|
||||||
prop.onCommitAll(x.type);
|
prop.onCommitAll(x.type);
|
||||||
set_disable(false);
|
set_disable(false);
|
||||||
}}>Commit all</Button>
|
}}
|
||||||
</Box>
|
>
|
||||||
{x.value.map(y=>(
|
Commit all
|
||||||
<Box sx={{display:"flex"}} key={y.path}>
|
</Button>
|
||||||
<Button variant="contained" onClick={()=>{
|
</Box>
|
||||||
set_disable(true);
|
{x.value.map(y => (
|
||||||
prop.onCommit(y);
|
<Box sx={{ display: "flex" }} key={y.path}>
|
||||||
set_disable(false);
|
<Button
|
||||||
}}
|
variant="contained"
|
||||||
disabled={button_disable}>Commit</Button>
|
onClick={() => {
|
||||||
<Typography variant='h5'>{y.path}</Typography>
|
set_disable(true);
|
||||||
</Box>
|
prop.onCommit(y);
|
||||||
))}
|
set_disable(false);
|
||||||
</Paper>);
|
}}
|
||||||
|
disabled={button_disable}
|
||||||
|
>
|
||||||
|
Commit
|
||||||
|
</Button>
|
||||||
|
<Typography variant="h5">{y.path}</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DifferencePage(){
|
export function DifferencePage() {
|
||||||
const ctx = useContext(UserContext);
|
const ctx = useContext(UserContext);
|
||||||
//const classes = useStyles();
|
// const classes = useStyles();
|
||||||
const [diffList,setDiffList] = useState<
|
const [diffList, setDiffList] = useState<
|
||||||
FileDifference[]
|
FileDifference[]
|
||||||
>([]);
|
>([]);
|
||||||
const doLoad = async ()=>{
|
const doLoad = async () => {
|
||||||
const list = await fetch('/api/diff/list');
|
const list = await fetch("/api/diff/list");
|
||||||
if(list.ok){
|
if (list.ok) {
|
||||||
const inner = await list.json();
|
const inner = await list.json();
|
||||||
setDiffList(inner);
|
setDiffList(inner);
|
||||||
}
|
} else {
|
||||||
else{
|
// setDiffList([]);
|
||||||
//setDiffList([]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const Commit = async(x:{type:string,path:string})=>{
|
const Commit = async (x: { type: string; path: string }) => {
|
||||||
const res = await fetch('/api/diff/commit',{
|
const res = await fetch("/api/diff/commit", {
|
||||||
method:'POST',
|
method: "POST",
|
||||||
body: JSON.stringify([{...x}]),
|
body: JSON.stringify([{ ...x }]),
|
||||||
headers:{
|
headers: {
|
||||||
'content-type':'application/json'
|
"content-type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const bb = await res.json();
|
const bb = await res.json();
|
||||||
if(bb.ok){
|
if (bb.ok) {
|
||||||
doLoad();
|
doLoad();
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
console.error("fail to add document");
|
console.error("fail to add document");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
const CommitAll = async (type :string)=>{
|
const CommitAll = async (type: string) => {
|
||||||
const res = await fetch("/api/diff/commitall",{
|
const res = await fetch("/api/diff/commitall", {
|
||||||
method:"POST",
|
method: "POST",
|
||||||
body: JSON.stringify({type:type}),
|
body: JSON.stringify({ type: type }),
|
||||||
headers:{
|
headers: {
|
||||||
'content-type':'application/json'
|
"content-type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const bb = await res.json();
|
const bb = await res.json();
|
||||||
if(bb.ok){
|
if (bb.ok) {
|
||||||
doLoad();
|
doLoad();
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
console.error("fail to add document");
|
console.error("fail to add document");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
useEffect(
|
useEffect(
|
||||||
()=>{
|
() => {
|
||||||
doLoad();
|
doLoad();
|
||||||
const i = setInterval(doLoad,5000);
|
const i = setInterval(doLoad, 5000);
|
||||||
return ()=>{
|
return () => {
|
||||||
clearInterval(i);
|
clearInterval(i);
|
||||||
}
|
};
|
||||||
},[]
|
},
|
||||||
)
|
[],
|
||||||
|
);
|
||||||
const menu = CommonMenuList();
|
const menu = CommonMenuList();
|
||||||
return (<Headline menu={menu}>
|
return (
|
||||||
{(ctx.username == "admin") ? (<div>
|
<Headline menu={menu}>
|
||||||
{(diffList.map(x=>
|
{(ctx.username == "admin")
|
||||||
<TypeDifference key={x.type} content={x} onCommit={Commit} onCommitAll={CommitAll}/>))}
|
? (
|
||||||
</div>)
|
<div>
|
||||||
:(<Typography variant='h2'>Not Allowed : please login as an admin</Typography>)
|
{diffList.map(x => (
|
||||||
}
|
<TypeDifference key={x.type} content={x} onCommit={Commit} onCommitAll={CommitAll} />
|
||||||
|
))}
|
||||||
</Headline>)
|
</div>
|
||||||
|
)
|
||||||
|
: <Typography variant="h2">Not Allowed : please login as an admin</Typography>}
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
}
|
}
|
@ -1,15 +1,13 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { Headline, CommonMenuList, LoadingCircle, ContentInfo, NavList, NavItem, TagChip } from '../component/mod';
|
import { CommonMenuList, ContentInfo, Headline, LoadingCircle, NavItem, NavList, TagChip } from "../component/mod";
|
||||||
|
|
||||||
import { Box, Typography, Chip, Pagination, Button } from '@mui/material';
|
|
||||||
import ContentAccessor, { QueryListOption, Document } from '../accessor/document';
|
|
||||||
import { toQueryString } from '../accessor/util';
|
|
||||||
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { QueryStringToMap } from '../accessor/util';
|
|
||||||
import { useIsElementInViewport } from './reader/reader';
|
|
||||||
|
|
||||||
|
import { Box, Button, Chip, Pagination, Typography } from "@mui/material";
|
||||||
|
import ContentAccessor, { Document, QueryListOption } from "../accessor/document";
|
||||||
|
import { toQueryString } from "../accessor/util";
|
||||||
|
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { QueryStringToMap } from "../accessor/util";
|
||||||
|
import { useIsElementInViewport } from "./reader/reader";
|
||||||
|
|
||||||
export type GalleryProp = {
|
export type GalleryProp = {
|
||||||
option?: QueryListOption;
|
option?: QueryListOption;
|
||||||
@ -17,77 +15,89 @@ export type GalleryProp = {
|
|||||||
};
|
};
|
||||||
type GalleryState = {
|
type GalleryState = {
|
||||||
documents: Document[] | undefined;
|
documents: Document[] | undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const GalleryInfo = (props: GalleryProp) => {
|
export const GalleryInfo = (props: GalleryProp) => {
|
||||||
const [state, setState] = useState<GalleryState>({ documents: undefined });
|
const [state, setState] = useState<GalleryState>({ documents: undefined });
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loadAll, setLoadAll] = useState(false);
|
const [loadAll, setLoadAll] = useState(false);
|
||||||
const {elementRef, isVisible: isLoadVisible} = useIsElementInViewport<HTMLButtonElement>({});
|
const { elementRef, isVisible: isLoadVisible } = useIsElementInViewport<HTMLButtonElement>({});
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
if(isLoadVisible && (!loadAll) && (state.documents != undefined)){
|
if (isLoadVisible && (!loadAll) && (state.documents != undefined)) {
|
||||||
loadMore();
|
loadMore();
|
||||||
}
|
}
|
||||||
},[isLoadVisible]);
|
}, [isLoadVisible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
console.log('load first',props.option);
|
console.log("load first", props.option);
|
||||||
const load = (async () => {
|
const load = async () => {
|
||||||
try{
|
try {
|
||||||
const c = await ContentAccessor.findList(props.option);
|
const c = await ContentAccessor.findList(props.option);
|
||||||
//todo : if c is undefined, retry to fetch 3 times. and show error message.
|
// todo : if c is undefined, retry to fetch 3 times. and show error message.
|
||||||
setState({ documents: c });
|
setState({ documents: c });
|
||||||
setLoadAll(c.length == 0);
|
setLoadAll(c.length == 0);
|
||||||
}
|
} catch (e) {
|
||||||
catch(e){
|
if (e instanceof Error) {
|
||||||
if(e instanceof Error){
|
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
setError("unknown error");
|
setError("unknown error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
load();
|
load();
|
||||||
}, [props.diff]);
|
}, [props.diff]);
|
||||||
const queryString = toQueryString(props.option ?? {});
|
const queryString = toQueryString(props.option ?? {});
|
||||||
if (state.documents === undefined && error == null) {
|
if (state.documents === undefined && error == null) {
|
||||||
return (<LoadingCircle />);
|
return <LoadingCircle />;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box
|
||||||
display: 'grid',
|
sx={{
|
||||||
gridRowGap: '1rem'
|
display: "grid",
|
||||||
}}>
|
gridRowGap: "1rem",
|
||||||
{props.option !== undefined && props.diff !== "" && <Box>
|
}}
|
||||||
<Typography variant="h6">search for</Typography>
|
>
|
||||||
{props.option.word !== undefined && <Chip label={"search : " + decodeURIComponent(props.option.word)}></Chip>}
|
{props.option !== undefined && props.diff !== "" && (
|
||||||
{props.option.content_type !== undefined && <Chip label={"type : " + props.option.content_type}></Chip>}
|
<Box>
|
||||||
{props.option.allow_tag !== undefined && props.option.allow_tag.map(x => (
|
<Typography variant="h6">search for</Typography>
|
||||||
<TagChip key={x} tagname={decodeURIComponent(x)} label={decodeURIComponent(x)}></TagChip>))}
|
{props.option.word !== undefined && (
|
||||||
</Box>}
|
<Chip label={"search : " + decodeURIComponent(props.option.word)}></Chip>
|
||||||
{
|
)}
|
||||||
state.documents && state.documents.map(x => {
|
{props.option.content_type !== undefined && (
|
||||||
return (<ContentInfo document={x} key={x.id}
|
<Chip label={"type : " + props.option.content_type}></Chip>
|
||||||
gallery={`/search?${queryString}`} short />);
|
)}
|
||||||
})
|
{props.option.allow_tag !== undefined
|
||||||
}
|
&& props.option.allow_tag.map(x => (
|
||||||
{(error && <Typography variant="h5">Error : {error}</Typography>)}
|
<TagChip key={x} tagname={decodeURIComponent(x)} label={decodeURIComponent(x)}>
|
||||||
<Typography variant="body1" sx={{
|
</TagChip>
|
||||||
justifyContent: "center",
|
))}
|
||||||
textAlign:"center"
|
</Box>
|
||||||
}}>{state.documents ? state.documents.length : "null"} loaded...</Typography>
|
)}
|
||||||
<Button onClick={()=>loadMore()} disabled={loadAll} ref={elementRef} >{loadAll ? "Load All" : "Load More"}</Button>
|
{state.documents && state.documents.map(x => {
|
||||||
|
return <ContentInfo document={x} key={x.id} gallery={`/search?${queryString}`} short />;
|
||||||
|
})}
|
||||||
|
{error && <Typography variant="h5">Error : {error}</Typography>}
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
justifyContent: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{state.documents ? state.documents.length : "null"} loaded...
|
||||||
|
</Typography>
|
||||||
|
<Button onClick={() => loadMore()} disabled={loadAll} ref={elementRef}>
|
||||||
|
{loadAll ? "Load All" : "Load More"}
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
let option = {...props.option};
|
let option = { ...props.option };
|
||||||
console.log(elementRef)
|
console.log(elementRef);
|
||||||
if(state.documents === undefined || state.documents.length === 0){
|
if (state.documents === undefined || state.documents.length === 0) {
|
||||||
console.log("loadall");
|
console.log("loadall");
|
||||||
setLoadAll(true);
|
setLoadAll(true);
|
||||||
return;
|
return;
|
||||||
@ -95,18 +105,17 @@ export const GalleryInfo = (props: GalleryProp) => {
|
|||||||
const prev_documents = state.documents;
|
const prev_documents = state.documents;
|
||||||
option.cursor = prev_documents[prev_documents.length - 1].id;
|
option.cursor = prev_documents[prev_documents.length - 1].id;
|
||||||
console.log("load more", option);
|
console.log("load more", option);
|
||||||
const load = (async () => {
|
const load = async () => {
|
||||||
const c = await ContentAccessor.findList(option);
|
const c = await ContentAccessor.findList(option);
|
||||||
if (c.length === 0) {
|
if (c.length === 0) {
|
||||||
setLoadAll(true);
|
setLoadAll(true);
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
setState({ documents: [...prev_documents, ...c] });
|
setState({ documents: [...prev_documents, ...c] });
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Gallery = () => {
|
export const Gallery = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -114,8 +123,10 @@ export const Gallery = () => {
|
|||||||
const menu_list = CommonMenuList({ url: location.search });
|
const menu_list = CommonMenuList({ url: location.search });
|
||||||
let option: QueryListOption = query;
|
let option: QueryListOption = query;
|
||||||
option.allow_tag = typeof option.allow_tag === "string" ? [option.allow_tag] : option.allow_tag;
|
option.allow_tag = typeof option.allow_tag === "string" ? [option.allow_tag] : option.allow_tag;
|
||||||
option.limit = typeof query['limit'] === "string" ? parseInt(query['limit']) : undefined;
|
option.limit = typeof query["limit"] === "string" ? parseInt(query["limit"]) : undefined;
|
||||||
return (<Headline menu={menu_list}>
|
return (
|
||||||
<GalleryInfo diff={location.search} option={query}></GalleryInfo>
|
<Headline menu={menu_list}>
|
||||||
</Headline>)
|
<GalleryInfo diff={location.search} option={query}></GalleryInfo>
|
||||||
}
|
</Headline>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,66 +1,86 @@
|
|||||||
import React, { useContext, useState } from 'react';
|
import {
|
||||||
import {CommonMenuList, Headline} from '../component/mod';
|
Button,
|
||||||
import { Button, Dialog, DialogActions, DialogContent, DialogContentText,
|
Dialog,
|
||||||
DialogTitle, MenuList, Paper, TextField, Typography, useTheme } from '@mui/material';
|
DialogActions,
|
||||||
import { UserContext } from '../state';
|
DialogContent,
|
||||||
import { useNavigate } from 'react-router-dom';
|
DialogContentText,
|
||||||
import {doLogin as doSessionLogin} from '../state';
|
DialogTitle,
|
||||||
|
MenuList,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { useContext, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { CommonMenuList, Headline } from "../component/mod";
|
||||||
|
import { UserContext } from "../state";
|
||||||
|
import { doLogin as doSessionLogin } from "../state";
|
||||||
|
|
||||||
export const LoginPage = ()=>{
|
export const LoginPage = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [userLoginInfo,setUserLoginInfo]= useState({username:"",password:""});
|
const [userLoginInfo, setUserLoginInfo] = useState({ username: "", password: "" });
|
||||||
const [openDialog,setOpenDialog] = useState({open:false,message:""});
|
const [openDialog, setOpenDialog] = useState({ open: false, message: "" });
|
||||||
const {setUsername,setPermission} = useContext(UserContext);
|
const { setUsername, setPermission } = useContext(UserContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleDialogClose = ()=>{
|
const handleDialogClose = () => {
|
||||||
setOpenDialog({...openDialog,open:false});
|
setOpenDialog({ ...openDialog, open: false });
|
||||||
}
|
};
|
||||||
const doLogin = async ()=>{
|
const doLogin = async () => {
|
||||||
try{
|
try {
|
||||||
const b = await doSessionLogin(userLoginInfo);
|
const b = await doSessionLogin(userLoginInfo);
|
||||||
if(typeof b === "string"){
|
if (typeof b === "string") {
|
||||||
setOpenDialog({open:true,message: b});
|
setOpenDialog({ open: true, message: b });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`login as ${b.username}`);
|
console.log(`login as ${b.username}`);
|
||||||
setUsername(b.username);
|
setUsername(b.username);
|
||||||
setPermission(b.permission);
|
setPermission(b.permission);
|
||||||
}
|
} catch (e) {
|
||||||
catch(e){
|
if (e instanceof Error) {
|
||||||
if(e instanceof Error){
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setOpenDialog({open:true,message:e.message});
|
setOpenDialog({ open: true, message: e.message });
|
||||||
}
|
} else console.error(e);
|
||||||
else console.error(e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
};
|
||||||
const menu = CommonMenuList();
|
const menu = CommonMenuList();
|
||||||
return <Headline menu={menu}>
|
return (
|
||||||
<Paper style={{ width: theme.spacing(40), padding: theme.spacing(4), alignSelf:'center'}}>
|
<Headline menu={menu}>
|
||||||
<Typography variant="h4">Login</Typography>
|
<Paper style={{ width: theme.spacing(40), padding: theme.spacing(4), alignSelf: "center" }}>
|
||||||
<div style={{minHeight:theme.spacing(2)}}></div>
|
<Typography variant="h4">Login</Typography>
|
||||||
<form style={{display:'flex', flexFlow:'column', alignItems:'stretch'}}>
|
<div style={{ minHeight: theme.spacing(2) }}></div>
|
||||||
<TextField label="username" onChange={(e)=>setUserLoginInfo({...userLoginInfo,username:e.target.value ?? "",})}></TextField>
|
<form style={{ display: "flex", flexFlow: "column", alignItems: "stretch" }}>
|
||||||
<TextField label="password" type="password" onKeyDown={(e)=>{if(e.key === 'Enter') doLogin();}}
|
<TextField
|
||||||
onChange={(e)=>setUserLoginInfo({...userLoginInfo,password:e.target.value ?? "",})}/>
|
label="username"
|
||||||
<div style={{minHeight:theme.spacing(2)}}></div>
|
onChange={(e) => setUserLoginInfo({ ...userLoginInfo, username: e.target.value ?? "" })}
|
||||||
<div style={{display:'flex'}}>
|
>
|
||||||
<Button onClick={doLogin}>login</Button>
|
</TextField>
|
||||||
<Button>signin</Button>
|
<TextField
|
||||||
</div>
|
label="password"
|
||||||
</form>
|
type="password"
|
||||||
</Paper>
|
onKeyDown={(e) => {
|
||||||
<Dialog open={openDialog.open}
|
if (e.key === "Enter") doLogin();
|
||||||
onClose={handleDialogClose}>
|
}}
|
||||||
<DialogTitle>Login Failed</DialogTitle>
|
onChange={(e) => setUserLoginInfo({ ...userLoginInfo, password: e.target.value ?? "" })}
|
||||||
<DialogContent>
|
/>
|
||||||
<DialogContentText>detail : {openDialog.message}</DialogContentText>
|
<div style={{ minHeight: theme.spacing(2) }}></div>
|
||||||
</DialogContent>
|
<div style={{ display: "flex" }}>
|
||||||
<DialogActions>
|
<Button onClick={doLogin}>login</Button>
|
||||||
<Button onClick={handleDialogClose} color="primary" autoFocus>Close</Button>
|
<Button>signin</Button>
|
||||||
</DialogActions>
|
</div>
|
||||||
</Dialog>
|
</form>
|
||||||
</Headline>
|
</Paper>
|
||||||
}
|
<Dialog open={openDialog.open} onClose={handleDialogClose}>
|
||||||
|
<DialogTitle>Login Failed</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>detail : {openDialog.message}</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleDialogClose} color="primary" autoFocus>Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
export * from './contentinfo';
|
export * from "./404";
|
||||||
export * from './gallery';
|
export * from "./contentinfo";
|
||||||
export * from './login';
|
export * from "./difference";
|
||||||
export * from './404';
|
export * from "./gallery";
|
||||||
export * from './profile';
|
export * from "./login";
|
||||||
export * from './difference';
|
export * from "./profile";
|
||||||
export * from './setting';
|
export * from "./setting";
|
||||||
export * from './tags';
|
export * from "./tags";
|
||||||
|
@ -1,114 +1,147 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { useContext, useState } from "react";
|
||||||
import { CommonMenuList, Headline } from "../component/mod";
|
import { CommonMenuList, Headline } from "../component/mod";
|
||||||
import React, { useContext, useState } from 'react';
|
|
||||||
import { UserContext } from "../state";
|
import { UserContext } from "../state";
|
||||||
import { Chip, Grid, Paper, Theme, Typography, Divider, Button,
|
|
||||||
Dialog, DialogTitle, DialogContentText, DialogContent, TextField, DialogActions } from "@mui/material";
|
|
||||||
|
|
||||||
const useStyles = ((theme:Theme)=>({
|
const useStyles = (theme: Theme) => ({
|
||||||
paper:{
|
paper: {
|
||||||
alignSelf:"center",
|
alignSelf: "center",
|
||||||
padding:theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
},
|
},
|
||||||
formfield:{
|
formfield: {
|
||||||
display:'flex',
|
display: "flex",
|
||||||
flexFlow:'column',
|
flexFlow: "column",
|
||||||
}
|
},
|
||||||
}));
|
});
|
||||||
|
|
||||||
export function ProfilePage(){
|
export function ProfilePage() {
|
||||||
const userctx = useContext(UserContext);
|
const userctx = useContext(UserContext);
|
||||||
//const classes = useStyles();
|
// const classes = useStyles();
|
||||||
const menu = CommonMenuList();
|
const menu = CommonMenuList();
|
||||||
const [pw_open,set_pw_open] = useState(false);
|
const [pw_open, set_pw_open] = useState(false);
|
||||||
const [oldpw,setOldpw] = useState("");
|
const [oldpw, setOldpw] = useState("");
|
||||||
const [newpw,setNewpw] = useState("");
|
const [newpw, setNewpw] = useState("");
|
||||||
const [newpwch,setNewpwch] = useState("");
|
const [newpwch, setNewpwch] = useState("");
|
||||||
const [msg_dialog,set_msg_dialog] = useState({opened:false,msg:""});
|
const [msg_dialog, set_msg_dialog] = useState({ opened: false, msg: "" });
|
||||||
const permission_list =userctx.permission.map(p=>(
|
const permission_list = userctx.permission.map(p => <Chip key={p} label={p}></Chip>);
|
||||||
<Chip key={p} label={p}></Chip>
|
const isElectronContent = ((window["electron"] as any) !== undefined) as boolean;
|
||||||
));
|
const handle_open = () => set_pw_open(true);
|
||||||
const isElectronContent = (((window['electron'] as any) !== undefined) as boolean);
|
const handle_close = () => {
|
||||||
const handle_open = ()=>set_pw_open(true);
|
|
||||||
const handle_close = ()=>{
|
|
||||||
set_pw_open(false);
|
set_pw_open(false);
|
||||||
setNewpw("");
|
setNewpw("");
|
||||||
setNewpwch("");
|
setNewpwch("");
|
||||||
};
|
};
|
||||||
const handle_ok= async ()=>{
|
const handle_ok = async () => {
|
||||||
if(newpw != newpwch){
|
if (newpw != newpwch) {
|
||||||
set_msg_dialog({opened:true,msg:"password and password check is not equal."});
|
set_msg_dialog({ opened: true, msg: "password and password check is not equal." });
|
||||||
handle_close();
|
handle_close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(isElectronContent){
|
if (isElectronContent) {
|
||||||
const elec = window['electron'] as any;
|
const elec = window["electron"] as any;
|
||||||
const success = elec.passwordReset(userctx.username,newpw);
|
const success = elec.passwordReset(userctx.username, newpw);
|
||||||
if(!success){
|
if (!success) {
|
||||||
set_msg_dialog({opened:true,msg:"user not exist."});
|
set_msg_dialog({ opened: true, msg: "user not exist." });
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else{
|
const res = await fetch("/user/reset", {
|
||||||
const res = await fetch("/user/reset",{
|
method: "POST",
|
||||||
method: 'POST',
|
body: JSON.stringify({
|
||||||
body:JSON.stringify({
|
username: userctx.username,
|
||||||
username:userctx.username,
|
oldpassword: oldpw,
|
||||||
oldpassword:oldpw,
|
newpassword: newpw,
|
||||||
newpassword:newpw,
|
|
||||||
}),
|
}),
|
||||||
headers:{
|
headers: {
|
||||||
"content-type":"application/json"
|
"content-type": "application/json",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if(res.status != 200){
|
if (res.status != 200) {
|
||||||
set_msg_dialog({opened:true,msg:"failed to change password."});
|
set_msg_dialog({ opened: true, msg: "failed to change password." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handle_close();
|
handle_close();
|
||||||
}
|
};
|
||||||
return (<Headline menu={menu}>
|
return (
|
||||||
<Paper /*className={classes.paper}*/>
|
<Headline menu={menu}>
|
||||||
<Grid container direction="column" alignItems="center">
|
<Paper /*className={classes.paper}*/>
|
||||||
<Grid item>
|
<Grid container direction="column" alignItems="center">
|
||||||
<Typography variant='h4'>{userctx.username}</Typography>
|
<Grid item>
|
||||||
|
<Typography variant="h4">{userctx.username}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Divider></Divider>
|
||||||
|
<Grid item>
|
||||||
|
Permission
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
{permission_list.length == 0 ? "-" : permission_list}
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button onClick={handle_open}>Password Reset</Button>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Divider></Divider>
|
</Paper>
|
||||||
<Grid item>
|
<Dialog open={pw_open} onClose={handle_close}>
|
||||||
Permission
|
<DialogTitle>Password Reset</DialogTitle>
|
||||||
</Grid>
|
<DialogContent>
|
||||||
<Grid item>
|
<Typography>type the old and new password</Typography>
|
||||||
{permission_list.length == 0 ? "-" : permission_list}
|
<div /*className={classes.formfield}*/>
|
||||||
</Grid>
|
{(!isElectronContent) && (
|
||||||
<Grid item>
|
<TextField
|
||||||
<Button onClick={handle_open}>Password Reset</Button>
|
autoFocus
|
||||||
</Grid>
|
margin="dense"
|
||||||
</Grid>
|
type="password"
|
||||||
</Paper>
|
label="old password"
|
||||||
<Dialog open={pw_open} onClose={handle_close}>
|
value={oldpw}
|
||||||
<DialogTitle>Password Reset</DialogTitle>
|
onChange={(e) => setOldpw(e.target.value)}
|
||||||
<DialogContent>
|
>
|
||||||
<Typography>type the old and new password</Typography>
|
</TextField>
|
||||||
<div /*className={classes.formfield}*/>
|
)}
|
||||||
{(!isElectronContent) && (<TextField autoFocus margin='dense' type="password" label="old password"
|
<TextField
|
||||||
value={oldpw} onChange={(e)=>setOldpw(e.target.value)}></TextField>)}
|
margin="dense"
|
||||||
<TextField margin='dense' type="password" label="new password"
|
type="password"
|
||||||
value={newpw} onChange={e=>setNewpw(e.target.value)}></TextField>
|
label="new password"
|
||||||
<TextField margin='dense' type="password" label="new password check"
|
value={newpw}
|
||||||
value={newpwch} onChange={e=>setNewpwch(e.target.value)}></TextField>
|
onChange={e => setNewpw(e.target.value)}
|
||||||
</div>
|
>
|
||||||
</DialogContent>
|
</TextField>
|
||||||
<DialogActions>
|
<TextField
|
||||||
<Button onClick={handle_close} color="primary">Cancel</Button>
|
margin="dense"
|
||||||
<Button onClick={handle_ok} color="primary">Ok</Button>
|
type="password"
|
||||||
</DialogActions>
|
label="new password check"
|
||||||
</Dialog>
|
value={newpwch}
|
||||||
<Dialog open={msg_dialog.opened} onClose={()=>set_msg_dialog({opened:false,msg:""})}>
|
onChange={e => setNewpwch(e.target.value)}
|
||||||
<DialogTitle>Alert!</DialogTitle>
|
>
|
||||||
<DialogContent>
|
</TextField>
|
||||||
<DialogContentText>{msg_dialog.msg}</DialogContentText>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={()=>set_msg_dialog({opened:false,msg:""})} color="primary">Close</Button>
|
<Button onClick={handle_close} color="primary">Cancel</Button>
|
||||||
</DialogActions>
|
<Button onClick={handle_ok} color="primary">Ok</Button>
|
||||||
</Dialog>
|
</DialogActions>
|
||||||
</Headline>)
|
</Dialog>
|
||||||
|
<Dialog open={msg_dialog.opened} onClose={() => set_msg_dialog({ opened: false, msg: "" })}>
|
||||||
|
<DialogTitle>Alert!</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>{msg_dialog.msg}</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => set_msg_dialog({ opened: false, msg: "" })} color="primary">Close</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
}
|
}
|
@ -1,47 +1,52 @@
|
|||||||
import React, {useState, useEffect} from 'react';
|
import { Typography, useTheme } from "@mui/material";
|
||||||
import { Typography, useTheme } from '@mui/material';
|
import React, { useEffect, useState } from "react";
|
||||||
import { Document } from '../../accessor/document';
|
import { Document } from "../../accessor/document";
|
||||||
|
|
||||||
type ComicType = "comic"|"artist cg"|"donjinshi"|"western";
|
type ComicType = "comic" | "artist cg" | "donjinshi" | "western";
|
||||||
|
|
||||||
export type PresentableTag = {
|
export type PresentableTag = {
|
||||||
artist:string[],
|
artist: string[];
|
||||||
group: string[],
|
group: string[];
|
||||||
series: string[],
|
series: string[];
|
||||||
type: ComicType,
|
type: ComicType;
|
||||||
character: string[],
|
character: string[];
|
||||||
tags: string[],
|
tags: string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ComicReader = (props:{doc:Document})=>{
|
export const ComicReader = (props: { doc: Document }) => {
|
||||||
const additional = props.doc.additional;
|
const additional = props.doc.additional;
|
||||||
const [curPage,setCurPage] = useState(0);
|
const [curPage, setCurPage] = useState(0);
|
||||||
if(!('page' in additional)){
|
if (!("page" in additional)) {
|
||||||
console.error("invalid content : page read fail : "+ JSON.stringify(additional));
|
console.error("invalid content : page read fail : " + JSON.stringify(additional));
|
||||||
return <Typography>Error. DB error. page restriction</Typography>
|
return <Typography>Error. DB error. page restriction</Typography>;
|
||||||
}
|
}
|
||||||
const PageDown = ()=>setCurPage(Math.max(curPage - 1 , 0));
|
const PageDown = () => setCurPage(Math.max(curPage - 1, 0));
|
||||||
const PageUP = ()=>setCurPage(Math.min(curPage + 1, page - 1));
|
const PageUP = () => setCurPage(Math.min(curPage + 1, page - 1));
|
||||||
const page:number = additional['page'] as number;
|
const page: number = additional["page"] as number;
|
||||||
const onKeyUp = (e: KeyboardEvent)=>{
|
const onKeyUp = (e: KeyboardEvent) => {
|
||||||
if(e.code === "ArrowLeft"){
|
if (e.code === "ArrowLeft") {
|
||||||
PageDown();
|
PageDown();
|
||||||
}
|
} else if (e.code === "ArrowRight") {
|
||||||
else if(e.code === "ArrowRight"){
|
|
||||||
PageUP();
|
PageUP();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
document.addEventListener("keydown",onKeyUp);
|
document.addEventListener("keydown", onKeyUp);
|
||||||
return ()=>{
|
return () => {
|
||||||
document.removeEventListener("keydown",onKeyUp);
|
document.removeEventListener("keydown", onKeyUp);
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
//theme.mixins.toolbar.minHeight;
|
// theme.mixins.toolbar.minHeight;
|
||||||
return (<div style={{overflow: 'hidden', alignSelf:'center'}}>
|
return (
|
||||||
<img onClick={PageUP} src={`/api/doc/${props.doc.id}/comic/${curPage}`}
|
<div style={{ overflow: "hidden", alignSelf: "center" }}>
|
||||||
style={{maxWidth:'100%', maxHeight:'calc(100vh - 64px)'}}></img>
|
<img
|
||||||
</div>);
|
onClick={PageUP}
|
||||||
}
|
src={`/api/doc/${props.doc.id}/comic/${curPage}`}
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "calc(100vh - 64px)" }}
|
||||||
|
>
|
||||||
|
</img>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ComicReader;
|
export default ComicReader;
|
@ -1,80 +1,77 @@
|
|||||||
import { Typography, styled } from '@mui/material';
|
import { styled, Typography } from "@mui/material";
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Document, makeThumbnailUrl } from '../../accessor/document';
|
import { Document, makeThumbnailUrl } from "../../accessor/document";
|
||||||
import {ComicReader} from './comic';
|
import { ComicReader } from "./comic";
|
||||||
import {VideoReader} from './video'
|
import { VideoReader } from "./video";
|
||||||
|
|
||||||
export interface PagePresenterProp{
|
export interface PagePresenterProp {
|
||||||
doc:Document,
|
doc: Document;
|
||||||
className?:string
|
className?: string;
|
||||||
}
|
}
|
||||||
interface PagePresenter{
|
interface PagePresenter {
|
||||||
(prop:PagePresenterProp):JSX.Element
|
(prop: PagePresenterProp): JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPresenter = (content:Document):PagePresenter => {
|
export const getPresenter = (content: Document): PagePresenter => {
|
||||||
switch (content.content_type) {
|
switch (content.content_type) {
|
||||||
case "comic":
|
case "comic":
|
||||||
return ComicReader;
|
return ComicReader;
|
||||||
case "video":
|
case "video":
|
||||||
return VideoReader;
|
return VideoReader;
|
||||||
}
|
}
|
||||||
return ()=><Typography variant='h2'>Not implemented reader</Typography>;
|
return () => <Typography variant="h2">Not implemented reader</Typography>;
|
||||||
}
|
};
|
||||||
const BackgroundDiv = styled("div")({
|
const BackgroundDiv = styled("div")({
|
||||||
height: '400px',
|
height: "400px",
|
||||||
width:'300px',
|
width: "300px",
|
||||||
backgroundColor:"#272733",
|
backgroundColor: "#272733",
|
||||||
display:"flex",
|
display: "flex",
|
||||||
alignItems:"center",
|
alignItems: "center",
|
||||||
justifyContent:"center"}
|
justifyContent: "center",
|
||||||
);
|
});
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useRef, useState, useEffect } from 'react';
|
import "./thumbnail.css";
|
||||||
import "./thumbnail.css"
|
|
||||||
|
|
||||||
export function useIsElementInViewport<T extends HTMLElement>(options?: IntersectionObserverInit) {
|
export function useIsElementInViewport<T extends HTMLElement>(options?: IntersectionObserverInit) {
|
||||||
const elementRef = useRef<T>(null);
|
const elementRef = useRef<T>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
const callback = (entries: IntersectionObserverEntry[]) => {
|
const callback = (entries: IntersectionObserverEntry[]) => {
|
||||||
const [entry] = entries;
|
const [entry] = entries;
|
||||||
setIsVisible(entry.isIntersecting);
|
setIsVisible(entry.isIntersecting);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(callback, options);
|
const observer = new IntersectionObserver(callback, options);
|
||||||
elementRef.current && observer.observe(elementRef.current);
|
elementRef.current && observer.observe(elementRef.current);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [elementRef, options]);
|
}, [elementRef, options]);
|
||||||
|
|
||||||
return { elementRef, isVisible };
|
return { elementRef, isVisible };
|
||||||
};
|
}
|
||||||
|
|
||||||
export function ThumbnailContainer(props:{
|
export function ThumbnailContainer(props: {
|
||||||
content:Document,
|
content: Document;
|
||||||
className?:string,
|
className?: string;
|
||||||
}){
|
}) {
|
||||||
const {elementRef, isVisible} = useIsElementInViewport<HTMLDivElement>({});
|
const { elementRef, isVisible } = useIsElementInViewport<HTMLDivElement>({});
|
||||||
const [loaded, setLoaded] = useState(false);
|
const [loaded, setLoaded] = useState(false);
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
if(isVisible){
|
if (isVisible) {
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
}
|
}
|
||||||
},[isVisible])
|
}, [isVisible]);
|
||||||
const style = {
|
const style = {
|
||||||
maxHeight: '400px',
|
maxHeight: "400px",
|
||||||
maxWidth: 'min(400px, 100vw)',
|
maxWidth: "min(400px, 100vw)",
|
||||||
};
|
};
|
||||||
const thumbnailurl = makeThumbnailUrl(props.content);
|
const thumbnailurl = makeThumbnailUrl(props.content);
|
||||||
if(props.content.content_type === "video"){
|
if (props.content.content_type === "video") {
|
||||||
return (<video src={thumbnailurl} muted autoPlay loop className={props.className} style={style}></video>)
|
return <video src={thumbnailurl} muted autoPlay loop className={props.className} style={style}></video>;
|
||||||
}
|
} else {return (
|
||||||
else return (<BackgroundDiv ref={elementRef}>
|
<BackgroundDiv ref={elementRef}>
|
||||||
{loaded && <img src={thumbnailurl}
|
{loaded && <img src={thumbnailurl} className={props.className + " thumbnail_img"} loading="lazy"></img>}
|
||||||
className={props.className + " thumbnail_img"}
|
</BackgroundDiv>
|
||||||
|
);}
|
||||||
loading="lazy"></img>}
|
|
||||||
</BackgroundDiv>)
|
|
||||||
}
|
}
|
@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Document } from '../../accessor/document';
|
import { Document } from "../../accessor/document";
|
||||||
|
|
||||||
export const VideoReader = (props:{doc:Document})=>{
|
export const VideoReader = (props: { doc: Document }) => {
|
||||||
const id = props.doc.id;
|
const id = props.doc.id;
|
||||||
return <video controls autoPlay src={`/api/doc/${props.doc.id}/video`} style={{maxHeight:'100%',maxWidth:'100%'}}></video>;
|
return (
|
||||||
}
|
<video controls autoPlay src={`/api/doc/${props.doc.id}/video`} style={{ maxHeight: "100%", maxWidth: "100%" }}>
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import React from 'react';
|
import { ArrowBack as ArrowBackIcon } from "@mui/icons-material";
|
||||||
import {Typography, Paper} from '@mui/material';
|
import { Paper, Typography } from "@mui/material";
|
||||||
import {ArrowBack as ArrowBackIcon} from '@mui/icons-material';
|
import React from "react";
|
||||||
import { Headline, BackItem, NavList, CommonMenuList } from '../component/mod';
|
import { BackItem, CommonMenuList, Headline, NavList } from "../component/mod";
|
||||||
|
|
||||||
export const SettingPage = ()=>{
|
export const SettingPage = () => {
|
||||||
const menu = CommonMenuList();
|
const menu = CommonMenuList();
|
||||||
return (<Headline menu={menu}>
|
return (
|
||||||
|
<Headline menu={menu}>
|
||||||
<Paper>
|
<Paper>
|
||||||
<Typography variant='h2'>Setting</Typography>
|
<Typography variant="h2">Setting</Typography>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Headline>);
|
</Headline>
|
||||||
|
);
|
||||||
};
|
};
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { Box, Paper, Typography } from "@mui/material";
|
||||||
import {Typography, Box, Paper} from '@mui/material';
|
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||||
import {LoadingCircle} from "../component/loading";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Headline, CommonMenuList } from '../component/mod';
|
import { LoadingCircle } from "../component/loading";
|
||||||
import {DataGrid, GridColDef} from "@mui/x-data-grid"
|
import { CommonMenuList, Headline } from "../component/mod";
|
||||||
|
|
||||||
type TagCount = {
|
type TagCount = {
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
@ -10,52 +10,52 @@ type TagCount = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tagTableColumn: GridColDef[] = [
|
const tagTableColumn: GridColDef[] = [
|
||||||
{
|
{
|
||||||
field:"tag_name",
|
field: "tag_name",
|
||||||
headerName:"Tag Name",
|
headerName: "Tag Name",
|
||||||
width: 200,
|
width: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field:"occurs",
|
field: "occurs",
|
||||||
headerName:"Occurs",
|
headerName: "Occurs",
|
||||||
width:100,
|
width: 100,
|
||||||
type:"number"
|
type: "number",
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
function TagTable(){
|
function TagTable() {
|
||||||
const [data,setData] = useState<TagCount[] | undefined>();
|
const [data, setData] = useState<TagCount[] | undefined>();
|
||||||
const [error, setErrorMsg] = useState<string|undefined>(undefined);
|
const [error, setErrorMsg] = useState<string | undefined>(undefined);
|
||||||
const isLoading = data === undefined;
|
const isLoading = data === undefined;
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
},[]);
|
}, []);
|
||||||
|
|
||||||
if(isLoading){
|
if (isLoading) {
|
||||||
return <LoadingCircle/>;
|
return <LoadingCircle />;
|
||||||
}
|
}
|
||||||
if(error !== undefined){
|
if (error !== undefined) {
|
||||||
return <Typography variant="h3">{error}</Typography>
|
return <Typography variant="h3">{error}</Typography>;
|
||||||
}
|
}
|
||||||
return <Box sx={{height:"400px",width:"100%"}}>
|
return (
|
||||||
<Paper sx={{height:"100%"}} elevation={2}>
|
<Box sx={{ height: "400px", width: "100%" }}>
|
||||||
<DataGrid rows={data} columns={tagTableColumn} getRowId={(t)=>t.tag_name} ></DataGrid>
|
<Paper sx={{ height: "100%" }} elevation={2}>
|
||||||
</Paper>
|
<DataGrid rows={data} columns={tagTableColumn} getRowId={(t) => t.tag_name}></DataGrid>
|
||||||
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
async function loadData(){
|
async function loadData() {
|
||||||
try{
|
try {
|
||||||
const res = await fetch("/api/tags?withCount=true");
|
const res = await fetch("/api/tags?withCount=true");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setData(data);
|
setData(data);
|
||||||
}
|
} catch (e) {
|
||||||
catch(e){
|
|
||||||
setData([]);
|
setData([]);
|
||||||
if(e instanceof Error){
|
if (e instanceof Error) {
|
||||||
setErrorMsg(e.message);
|
setErrorMsg(e.message);
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
console.log(e);
|
console.log(e);
|
||||||
setErrorMsg("");
|
setErrorMsg("");
|
||||||
}
|
}
|
||||||
@ -63,9 +63,11 @@ function TagTable(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagsPage = ()=>{
|
export const TagsPage = () => {
|
||||||
const menu = CommonMenuList();
|
const menu = CommonMenuList();
|
||||||
return <Headline menu={menu}>
|
return (
|
||||||
<TagTable></TagTable>
|
<Headline menu={menu}>
|
||||||
</Headline>
|
<TagTable></TagTable>
|
||||||
|
</Headline>
|
||||||
|
);
|
||||||
};
|
};
|
@ -1,16 +1,16 @@
|
|||||||
import React, { createContext, useRef, useState } from 'react';
|
import React, { createContext, useRef, useState } from "react";
|
||||||
export const BackLinkContext = createContext({backLink:"",setBackLink:(s:string)=>{} });
|
export const BackLinkContext = createContext({ backLink: "", setBackLink: (s: string) => {} });
|
||||||
export const UserContext = createContext({
|
export const UserContext = createContext({
|
||||||
username: "",
|
username: "",
|
||||||
permission: [] as string[],
|
permission: [] as string[],
|
||||||
setUsername: (s: string) => { },
|
setUsername: (s: string) => {},
|
||||||
setPermission: (permission: string[]) => { }
|
setPermission: (permission: string[]) => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
type LoginLocalStorage = {
|
type LoginLocalStorage = {
|
||||||
username: string,
|
username: string;
|
||||||
permission: string[],
|
permission: string[];
|
||||||
accessExpired: number
|
accessExpired: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
let localObj: LoginLocalStorage | null = null;
|
let localObj: LoginLocalStorage | null = null;
|
||||||
@ -25,71 +25,70 @@ export const getInitialValue = async () => {
|
|||||||
return {
|
return {
|
||||||
username: localObj.username,
|
username: localObj.username,
|
||||||
permission: localObj.permission,
|
permission: localObj.permission,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
const res = await fetch('/user/refresh', {
|
const res = await fetch("/user/refresh", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
});
|
});
|
||||||
if (res.status !== 200) throw new Error("Maybe Network Error")
|
if (res.status !== 200) throw new Error("Maybe Network Error");
|
||||||
const r = await res.json() as LoginLocalStorage & { refresh: boolean };
|
const r = await res.json() as LoginLocalStorage & { refresh: boolean };
|
||||||
if (r.refresh) {
|
if (r.refresh) {
|
||||||
localObj = {
|
localObj = {
|
||||||
username: r.username,
|
username: r.username,
|
||||||
permission: r.permission,
|
permission: r.permission,
|
||||||
accessExpired: r.accessExpired
|
accessExpired: r.accessExpired,
|
||||||
}
|
};
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
localObj = {
|
localObj = {
|
||||||
accessExpired: 0,
|
accessExpired: 0,
|
||||||
username: "",
|
username: "",
|
||||||
permission: r.permission
|
permission: r.permission,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||||
return {
|
return {
|
||||||
username: r.username,
|
username: r.username,
|
||||||
permission: r.permission
|
permission: r.permission,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
export const doLogout = async () => {
|
export const doLogout = async () => {
|
||||||
const req = await fetch('/user/logout', {
|
const req = await fetch("/user/logout", {
|
||||||
method: 'POST'
|
method: "POST",
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const res = await req.json();
|
const res = await req.json();
|
||||||
localObj = {
|
localObj = {
|
||||||
accessExpired: 0,
|
accessExpired: 0,
|
||||||
username: "",
|
username: "",
|
||||||
permission: res["permission"]
|
permission: res["permission"],
|
||||||
}
|
};
|
||||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||||
return {
|
return {
|
||||||
username: localObj.username,
|
username: localObj.username,
|
||||||
permission: localObj.permission,
|
permission: localObj.permission,
|
||||||
}
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Server Error ${error}`);
|
console.error(`Server Error ${error}`);
|
||||||
return {
|
return {
|
||||||
username: "",
|
username: "",
|
||||||
permission: [],
|
permission: [],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
export const doLogin = async (userLoginInfo:{
|
export const doLogin = async (userLoginInfo: {
|
||||||
username:string,
|
username: string;
|
||||||
password:string,
|
password: string;
|
||||||
}): Promise<string|LoginLocalStorage>=>{
|
}): Promise<string | LoginLocalStorage> => {
|
||||||
const res = await fetch('/user/login',{
|
const res = await fetch("/user/login", {
|
||||||
method:'POST',
|
method: "POST",
|
||||||
body:JSON.stringify(userLoginInfo),
|
body: JSON.stringify(userLoginInfo),
|
||||||
headers:{"content-type":"application/json"}
|
headers: { "content-type": "application/json" },
|
||||||
});
|
});
|
||||||
const b = await res.json();
|
const b = await res.json();
|
||||||
if(res.status !== 200){
|
if (res.status !== 200) {
|
||||||
return b.detail as string;
|
return b.detail as string;
|
||||||
}
|
}
|
||||||
localObj = b;
|
localObj = b;
|
||||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||||
return b;
|
return b;
|
||||||
}
|
};
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
import {Knex as k} from "knex";
|
import { Knex as k } from "knex";
|
||||||
|
|
||||||
export namespace Knex {
|
export namespace Knex {
|
||||||
export const config: {
|
export const config: {
|
||||||
development: k.Config,
|
development: k.Config;
|
||||||
production: k.Config
|
production: k.Config;
|
||||||
} = {
|
} = {
|
||||||
development: {
|
development: {
|
||||||
client: 'sqlite3',
|
client: "sqlite3",
|
||||||
connection: {
|
connection: {
|
||||||
filename: './devdb.sqlite3'
|
filename: "./devdb.sqlite3",
|
||||||
|
},
|
||||||
|
debug: true,
|
||||||
},
|
},
|
||||||
debug: true,
|
production: {
|
||||||
},
|
client: "sqlite3",
|
||||||
production: {
|
connection: {
|
||||||
client: 'sqlite3',
|
filename: "./db.sqlite3",
|
||||||
connection: {
|
},
|
||||||
filename: './db.sqlite3',
|
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,65 +1,66 @@
|
|||||||
import {ContentFile, createDefaultClass,registerContentReferrer, ContentConstructOption} from './file';
|
import { extname } from "path";
|
||||||
import {readZip, readAllFromZip} from '../util/zipwrap';
|
import { DocumentBody } from "../model/doc";
|
||||||
import { DocumentBody } from '../model/doc';
|
import { readAllFromZip, readZip } from "../util/zipwrap";
|
||||||
import {extname} from 'path';
|
import { ContentConstructOption, ContentFile, createDefaultClass, registerContentReferrer } from "./file";
|
||||||
|
|
||||||
type ComicType = "doujinshi"|"artist cg"|"manga"|"western";
|
type ComicType = "doujinshi" | "artist cg" | "manga" | "western";
|
||||||
interface ComicDesc{
|
interface ComicDesc {
|
||||||
title:string,
|
title: string;
|
||||||
artist?:string[],
|
artist?: string[];
|
||||||
group?:string[],
|
group?: string[];
|
||||||
series?:string[],
|
series?: string[];
|
||||||
type:ComicType|[ComicType],
|
type: ComicType | [ComicType];
|
||||||
character?:string[],
|
character?: string[];
|
||||||
tags?:string[]
|
tags?: string[];
|
||||||
}
|
}
|
||||||
const ImageExt = ['.gif', '.png', '.jpeg', '.bmp', '.webp', '.jpg'];
|
const ImageExt = [".gif", ".png", ".jpeg", ".bmp", ".webp", ".jpg"];
|
||||||
export class ComicReferrer extends createDefaultClass("comic"){
|
export class ComicReferrer extends createDefaultClass("comic") {
|
||||||
desc: ComicDesc|undefined;
|
desc: ComicDesc | undefined;
|
||||||
pagenum: number;
|
pagenum: number;
|
||||||
additional: ContentConstructOption| undefined;
|
additional: ContentConstructOption | undefined;
|
||||||
constructor(path:string,option?:ContentConstructOption){
|
constructor(path: string, option?: ContentConstructOption) {
|
||||||
super(path);
|
super(path);
|
||||||
this.additional = option;
|
this.additional = option;
|
||||||
this.pagenum = 0;
|
this.pagenum = 0;
|
||||||
}
|
}
|
||||||
async initDesc():Promise<void>{
|
async initDesc(): Promise<void> {
|
||||||
if(this.desc !== undefined) return;
|
if (this.desc !== undefined) return;
|
||||||
const zip = await readZip(this.path);
|
const zip = await readZip(this.path);
|
||||||
const entries = await zip.entries();
|
const entries = await zip.entries();
|
||||||
this.pagenum = Object.keys(entries).filter(x=>ImageExt.includes(extname(x))).length;
|
this.pagenum = Object.keys(entries).filter(x => ImageExt.includes(extname(x))).length;
|
||||||
const entry = entries["desc.json"];
|
const entry = entries["desc.json"];
|
||||||
if(entry === undefined){
|
if (entry === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = (await readAllFromZip(zip,entry)).toString('utf-8');
|
const data = (await readAllFromZip(zip, entry)).toString("utf-8");
|
||||||
this.desc = JSON.parse(data);
|
this.desc = JSON.parse(data);
|
||||||
if(this.desc === undefined)
|
if (this.desc === undefined) {
|
||||||
throw new Error(`JSON.parse is returning undefined. ${this.path} desc.json format error`);
|
throw new Error(`JSON.parse is returning undefined. ${this.path} desc.json format error`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDocumentBody(): Promise<DocumentBody>{
|
async createDocumentBody(): Promise<DocumentBody> {
|
||||||
await this.initDesc();
|
await this.initDesc();
|
||||||
const basebody = await super.createDocumentBody();
|
const basebody = await super.createDocumentBody();
|
||||||
this.desc?.title;
|
this.desc?.title;
|
||||||
if(this.desc === undefined){
|
if (this.desc === undefined) {
|
||||||
return basebody;
|
return basebody;
|
||||||
}
|
}
|
||||||
let tags:string[] = this.desc.tags ?? [];
|
let tags: string[] = this.desc.tags ?? [];
|
||||||
tags = tags.concat(this.desc.artist?.map(x=>`artist:${x}`) ?? []);
|
tags = tags.concat(this.desc.artist?.map(x => `artist:${x}`) ?? []);
|
||||||
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 = this.desc.type instanceof Array ? this.desc.type[0] : this.desc.type;
|
||||||
tags.push(`type:${type}`);
|
tags.push(`type:${type}`);
|
||||||
return {
|
return {
|
||||||
...basebody,
|
...basebody,
|
||||||
title:this.desc.title,
|
title: this.desc.title,
|
||||||
additional:{
|
additional: {
|
||||||
page:this.pagenum
|
page: this.pagenum,
|
||||||
},
|
},
|
||||||
tags:tags
|
tags: tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
registerContentReferrer(ComicReferrer);
|
registerContentReferrer(ComicReferrer);
|
@ -1,42 +1,44 @@
|
|||||||
import {Context, DefaultState, DefaultContext, Middleware, Next} from 'koa';
|
import { createHash } from "crypto";
|
||||||
import Router from 'koa-router';
|
import { promises, Stats } from "fs";
|
||||||
import {createHash} from 'crypto';
|
import { Context, DefaultContext, DefaultState, Middleware, Next } from "koa";
|
||||||
import {promises, Stats} from 'fs'
|
import Router from "koa-router";
|
||||||
import {extname} from 'path';
|
import { extname } from "path";
|
||||||
import { DocumentBody } from '../model/mod';
|
import path from "path";
|
||||||
import path from 'path';
|
import { DocumentBody } from "../model/mod";
|
||||||
/**
|
/**
|
||||||
* content file or directory referrer
|
* content file or directory referrer
|
||||||
*/
|
*/
|
||||||
export interface ContentFile{
|
export interface ContentFile {
|
||||||
getHash():Promise<string>;
|
getHash(): Promise<string>;
|
||||||
createDocumentBody():Promise<DocumentBody>;
|
createDocumentBody(): Promise<DocumentBody>;
|
||||||
readonly path: string;
|
readonly path: string;
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
}
|
}
|
||||||
export type ContentConstructOption = {
|
export type ContentConstructOption = {
|
||||||
hash: string,
|
hash: string;
|
||||||
}
|
};
|
||||||
type ContentFileConstructor = (new (path:string,option?:ContentConstructOption) => ContentFile)&{content_type:string};
|
type ContentFileConstructor = (new(path: string, option?: ContentConstructOption) => ContentFile) & {
|
||||||
export const createDefaultClass = (type:string):ContentFileConstructor=>{
|
content_type: string;
|
||||||
let cons = class implements ContentFile{
|
};
|
||||||
|
export const createDefaultClass = (type: string): ContentFileConstructor => {
|
||||||
|
let 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;
|
||||||
|
|
||||||
constructor(path:string,option?:ContentConstructOption){
|
constructor(path: string, option?: ContentConstructOption) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.hash = option?.hash;
|
this.hash = option?.hash;
|
||||||
this.stat = undefined;
|
this.stat = undefined;
|
||||||
}
|
}
|
||||||
async createDocumentBody(): Promise<DocumentBody> {
|
async createDocumentBody(): Promise<DocumentBody> {
|
||||||
const {base,dir, name} = path.parse(this.path);
|
const { base, dir, name } = path.parse(this.path);
|
||||||
|
|
||||||
const ret = {
|
const ret = {
|
||||||
title : name,
|
title: name,
|
||||||
basepath : dir,
|
basepath: dir,
|
||||||
additional: {},
|
additional: {},
|
||||||
content_type: cons.content_type,
|
content_type: cons.content_type,
|
||||||
filename: base,
|
filename: base,
|
||||||
@ -46,43 +48,43 @@ export const createDefaultClass = (type:string):ContentFileConstructor=>{
|
|||||||
} as DocumentBody;
|
} as DocumentBody;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
get type():string{
|
get type(): string {
|
||||||
return cons.content_type;
|
return cons.content_type;
|
||||||
}
|
}
|
||||||
async getHash():Promise<string>{
|
async getHash(): Promise<string> {
|
||||||
if(this.hash !== undefined) return this.hash;
|
if (this.hash !== undefined) return this.hash;
|
||||||
this.stat = await promises.stat(this.path);
|
this.stat = await promises.stat(this.path);
|
||||||
const hash = createHash("sha512");
|
const hash = createHash("sha512");
|
||||||
hash.update(extname(this.path));
|
hash.update(extname(this.path));
|
||||||
hash.update(this.stat.mode.toString());
|
hash.update(this.stat.mode.toString());
|
||||||
//if(this.desc !== undefined)
|
// if(this.desc !== undefined)
|
||||||
// hash.update(JSON.stringify(this.desc));
|
// hash.update(JSON.stringify(this.desc));
|
||||||
hash.update(this.stat.size.toString());
|
hash.update(this.stat.size.toString());
|
||||||
this.hash = hash.digest("base64");
|
this.hash = hash.digest("base64");
|
||||||
return this.hash;
|
return this.hash;
|
||||||
}
|
}
|
||||||
async getMtime():Promise<number>{
|
async getMtime(): Promise<number> {
|
||||||
if(this.stat !== undefined) return this.stat.mtimeMs;
|
if (this.stat !== undefined) return this.stat.mtimeMs;
|
||||||
await this.getHash();
|
await this.getHash();
|
||||||
return this.stat!.mtimeMs;
|
return this.stat!.mtimeMs;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return cons;
|
return cons;
|
||||||
}
|
};
|
||||||
let ContstructorTable:{[k:string]:ContentFileConstructor} = {};
|
let 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;
|
||||||
}
|
}
|
||||||
export function createContentFile(type:string,path:string,option?:ContentConstructOption){
|
export function createContentFile(type: string, path: string, option?: ContentConstructOption) {
|
||||||
const constructorMethod = ContstructorTable[type];
|
const constructorMethod = ContstructorTable[type];
|
||||||
if(constructorMethod === undefined){
|
if (constructorMethod === undefined) {
|
||||||
console.log(`${type} are not in ${JSON.stringify(ContstructorTable)}`);
|
console.log(`${type} are not in ${JSON.stringify(ContstructorTable)}`);
|
||||||
throw new Error("construction method of the content type is undefined");
|
throw new Error("construction method of the content type is undefined");
|
||||||
}
|
}
|
||||||
return new constructorMethod(path,option);
|
return new constructorMethod(path, option);
|
||||||
}
|
}
|
||||||
export function getContentFileConstructor(type:string): ContentFileConstructor|undefined{
|
export function getContentFileConstructor(type: string): ContentFileConstructor | undefined {
|
||||||
const ret = ContstructorTable[type];
|
const ret = ContstructorTable[type];
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
@ -1,3 +1,3 @@
|
|||||||
import './comic';
|
import "./comic";
|
||||||
import './video';
|
import "./video";
|
||||||
export {ContentFile, createContentFile} from './file';
|
export { ContentFile, createContentFile } from "./file";
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import {ContentFile, registerContentReferrer, ContentConstructOption} from './file';
|
import { ContentConstructOption, ContentFile, 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){
|
constructor(path: string, desc?: ContentConstructOption) {
|
||||||
super(path,desc);
|
super(path, desc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
registerContentReferrer(VideoReferrer);
|
registerContentReferrer(VideoReferrer);
|
@ -1,46 +1,44 @@
|
|||||||
import { existsSync } from 'fs';
|
import { existsSync } from "fs";
|
||||||
import Knex from 'knex';
|
import Knex from "knex";
|
||||||
import {Knex as KnexConfig} from './config';
|
import { Knex as KnexConfig } from "./config";
|
||||||
import { get_setting } from './SettingConfig';
|
import { get_setting } from "./SettingConfig";
|
||||||
|
|
||||||
export async function connectDB(){
|
export async function connectDB() {
|
||||||
const env = get_setting().mode;
|
const env = get_setting().mode;
|
||||||
const config = KnexConfig.config[env];
|
const config = KnexConfig.config[env];
|
||||||
if(!config.connection){
|
if (!config.connection) {
|
||||||
throw new Error("connection options required.");
|
throw new Error("connection options required.");
|
||||||
}
|
}
|
||||||
const connection = config.connection
|
const connection = config.connection;
|
||||||
if(typeof connection === "string"){
|
if (typeof connection === "string") {
|
||||||
throw new Error("unknown connection options");
|
throw new Error("unknown connection options");
|
||||||
}
|
}
|
||||||
if(typeof connection === "function"){
|
if (typeof connection === "function") {
|
||||||
throw new Error("connection provider not supported...");
|
throw new Error("connection provider not supported...");
|
||||||
}
|
}
|
||||||
if(!("filename" in connection) ){
|
if (!("filename" in connection)) {
|
||||||
throw new Error("sqlite3 config need");
|
throw new Error("sqlite3 config need");
|
||||||
}
|
}
|
||||||
const init_need = !existsSync(connection.filename);
|
const init_need = !existsSync(connection.filename);
|
||||||
const knex = Knex(config);
|
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 knex.raw("select 1 + 1;");
|
||||||
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...`);
|
||||||
continue;
|
continue;
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(init_need){
|
if (init_need) {
|
||||||
console.log("first execute: initialize database...");
|
console.log("first execute: initialize database...");
|
||||||
const migrate = await import("../migrations/initial");
|
const migrate = await import("../migrations/initial");
|
||||||
await migrate.up(knex);
|
await migrate.up(knex);
|
||||||
|
231
src/db/doc.ts
231
src/db/doc.ts
@ -1,118 +1,118 @@
|
|||||||
import { Document, DocumentBody, DocumentAccessor, QueryListOption } from '../model/doc';
|
import { Knex } from "knex";
|
||||||
import {Knex} from 'knex';
|
import { Document, DocumentAccessor, DocumentBody, QueryListOption } from "../model/doc";
|
||||||
import {createKnexTagController} from './tag';
|
import { TagAccessor } from "../model/tag";
|
||||||
import { TagAccessor } from '../model/tag';
|
import { createKnexTagController } from "./tag";
|
||||||
|
|
||||||
export type DBTagContentRelation = {
|
export type DBTagContentRelation = {
|
||||||
doc_id:number,
|
doc_id: number;
|
||||||
tag_name:string
|
tag_name: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
class KnexDocumentAccessor implements DocumentAccessor{
|
class KnexDocumentAccessor implements DocumentAccessor {
|
||||||
knex : Knex;
|
knex: Knex;
|
||||||
tagController: TagAccessor;
|
tagController: TagAccessor;
|
||||||
constructor(knex : Knex){
|
constructor(knex: Knex) {
|
||||||
this.knex = knex;
|
this.knex = knex;
|
||||||
this.tagController = createKnexTagController(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 sw = `%${search_word}%`;
|
||||||
const docs = await this.knex.select("*").from("document")
|
const docs = await this.knex.select("*").from("document")
|
||||||
.where("title","like",sw);
|
.where("title", "like", sw);
|
||||||
return docs;
|
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.knex.transaction(async (trx) => {
|
||||||
//add tags
|
// add tags
|
||||||
const tagCollected = new Set<string>();
|
const tagCollected = new Set<string>();
|
||||||
content_list.map(x=>x.tags).forEach((x)=>{
|
content_list.map(x => x.tags).forEach((x) => {
|
||||||
x.forEach(x=>{
|
x.forEach(x => {
|
||||||
tagCollected.add(x);
|
tagCollected.add(x);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const tagCollectPromiseList = [];
|
const tagCollectPromiseList = [];
|
||||||
const tagController = createKnexTagController(trx);
|
const tagController = createKnexTagController(trx);
|
||||||
for (const it of tagCollected){
|
for (const it of tagCollected) {
|
||||||
const p = tagController.addTag({name:it});
|
const p = tagController.addTag({ name: it });
|
||||||
tagCollectPromiseList.push(p);
|
tagCollectPromiseList.push(p);
|
||||||
}
|
}
|
||||||
await Promise.all(tagCollectPromiseList);
|
await Promise.all(tagCollectPromiseList);
|
||||||
//add for each contents
|
// add for each contents
|
||||||
const ret = [];
|
const ret = [];
|
||||||
for (const content of content_list) {
|
for (const content of content_list) {
|
||||||
const {tags,additional, ...rest} = content;
|
const { tags, additional, ...rest } = content;
|
||||||
const id_lst = await trx.insert({
|
const id_lst = await trx.insert({
|
||||||
additional:JSON.stringify(additional),
|
additional: JSON.stringify(additional),
|
||||||
created_at:Date.now(),
|
created_at: Date.now(),
|
||||||
...rest
|
...rest,
|
||||||
}).into("document");
|
}).into("document");
|
||||||
const id = id_lst[0];
|
const id = id_lst[0];
|
||||||
if(tags.length > 0){
|
if (tags.length > 0) {
|
||||||
await trx.insert(tags.map(y=>({
|
await trx.insert(tags.map(y => ({
|
||||||
doc_id:id,
|
doc_id: id,
|
||||||
tag_name:y
|
tag_name: y,
|
||||||
}))).into('doc_tag_relation');
|
}))).into("doc_tag_relation");
|
||||||
}
|
}
|
||||||
ret.push(id);
|
ret.push(id);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async add(c: DocumentBody){
|
async add(c: DocumentBody) {
|
||||||
const {tags,additional, ...rest} = c;
|
const { tags, additional, ...rest } = c;
|
||||||
const id_lst = await this.knex.insert({
|
const id_lst = await this.knex.insert({
|
||||||
additional:JSON.stringify(additional),
|
additional: JSON.stringify(additional),
|
||||||
created_at:Date.now(),
|
created_at: Date.now(),
|
||||||
...rest
|
...rest,
|
||||||
}).into('document');
|
}).into("document");
|
||||||
const id = id_lst[0];
|
const id = id_lst[0];
|
||||||
for (const it of tags) {
|
for (const it of tags) {
|
||||||
this.tagController.addTag({name:it});
|
this.tagController.addTag({ name: it });
|
||||||
}
|
}
|
||||||
if(tags.length > 0){
|
if (tags.length > 0) {
|
||||||
await this.knex.insert<DBTagContentRelation>(
|
await this.knex.insert<DBTagContentRelation>(
|
||||||
tags.map(x=>({doc_id:id,tag_name:x}))
|
tags.map(x => ({ doc_id: id, tag_name: x })),
|
||||||
).into("doc_tag_relation");
|
).into("doc_tag_relation");
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
};
|
}
|
||||||
async del(id:number) {
|
async del(id: number) {
|
||||||
if (await this.findById(id) !== undefined){
|
if (await this.findById(id) !== undefined) {
|
||||||
await this.knex.delete().from("doc_tag_relation").where({doc_id:id});
|
await this.knex.delete().from("doc_tag_relation").where({ doc_id: id });
|
||||||
await this.knex.delete().from("document").where({id:id});
|
await this.knex.delete().from("document").where({ id: id });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
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 s = await this.knex.select("*").from("document").where({ id: id });
|
||||||
if(s.length === 0) return undefined;
|
if (s.length === 0) return undefined;
|
||||||
const first = s[0];
|
const first = s[0];
|
||||||
let ret_tags:string[] = []
|
let ret_tags: string[] = [];
|
||||||
if(tagload === true){
|
if (tagload === true) {
|
||||||
const tags : DBTagContentRelation[] = await this.knex.select("*")
|
const tags: DBTagContentRelation[] = await this.knex.select("*")
|
||||||
.from("doc_tag_relation").where({doc_id:first.id});
|
.from("doc_tag_relation").where({ doc_id: first.id });
|
||||||
ret_tags = tags.map(x=>x.tag_name);
|
ret_tags = tags.map(x => x.tag_name);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...first,
|
...first,
|
||||||
tags:ret_tags,
|
tags: ret_tags,
|
||||||
additional: first.additional !== null ? JSON.parse(first.additional) : {},
|
additional: first.additional !== null ? JSON.parse(first.additional) : {},
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
async findDeleted(content_type:string){
|
async findDeleted(content_type: string) {
|
||||||
const s = await this.knex.select("*")
|
const s = await this.knex.select("*")
|
||||||
.where({content_type:content_type})
|
.where({ content_type: content_type })
|
||||||
.whereNotNull("update_at")
|
.whereNotNull("update_at")
|
||||||
.from("document");
|
.from("document");
|
||||||
return s.map(x=>({
|
return s.map(x => ({
|
||||||
...x,
|
...x,
|
||||||
tags:[],
|
tags: [],
|
||||||
additional:{}
|
additional: {},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
async findList(option?:QueryListOption){
|
async findList(option?: QueryListOption) {
|
||||||
option = option ?? {};
|
option = option ?? {};
|
||||||
const allow_tag = option.allow_tag ?? [];
|
const allow_tag = option.allow_tag ?? [];
|
||||||
const eager_loading = option.eager_loading ?? true;
|
const eager_loading = option.eager_loading ?? true;
|
||||||
@ -123,98 +123,101 @@ class KnexDocumentAccessor implements DocumentAccessor{
|
|||||||
const content_type = option.content_type;
|
const content_type = option.content_type;
|
||||||
const cursor = option.cursor;
|
const cursor = option.cursor;
|
||||||
|
|
||||||
const buildquery = ()=>{
|
const buildquery = () => {
|
||||||
let query = this.knex.select("document.*");
|
let query = this.knex.select("document.*");
|
||||||
if(allow_tag.length > 0){
|
if (allow_tag.length > 0) {
|
||||||
query = query.from("doc_tag_relation as tags_0");
|
query = query.from("doc_tag_relation as tags_0");
|
||||||
query = query.where("tags_0.tag_name","=",allow_tag[0]);
|
query = query.where("tags_0.tag_name", "=", allow_tag[0]);
|
||||||
for (let index = 1; index < allow_tag.length; index++) {
|
for (let index = 1; index < allow_tag.length; index++) {
|
||||||
const element = allow_tag[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.innerJoin(
|
||||||
query = query.where(`tags_${index}.tag_name`,'=',element);
|
`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");
|
query = query.innerJoin("document", "tags_0.doc_id", "document.id");
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
query = query.from("document");
|
query = query.from("document");
|
||||||
}
|
}
|
||||||
if(word !== undefined){
|
if (word !== undefined) {
|
||||||
//don't worry about sql injection.
|
// don't worry about sql injection.
|
||||||
query = query.where('title','like',`%${word}%`);
|
query = query.where("title", "like", `%${word}%`);
|
||||||
}
|
}
|
||||||
if(content_type !== undefined){
|
if (content_type !== undefined) {
|
||||||
query = query.where('content_type','=',content_type);
|
query = query.where("content_type", "=", content_type);
|
||||||
}
|
}
|
||||||
if(use_offset){
|
if (use_offset) {
|
||||||
query = query.offset(offset);
|
query = query.offset(offset);
|
||||||
}
|
} else {
|
||||||
else{
|
if (cursor !== undefined) {
|
||||||
if(cursor !== undefined){
|
query = query.where("id", "<", cursor);
|
||||||
query = query.where('id','<',cursor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
query = query.limit(limit);
|
query = query.limit(limit);
|
||||||
query = query.orderBy('id',"desc");
|
query = query.orderBy("id", "desc");
|
||||||
return query;
|
return query;
|
||||||
}
|
};
|
||||||
let query = buildquery();
|
let query = buildquery();
|
||||||
//console.log(query.toSQL());
|
// console.log(query.toSQL());
|
||||||
let result:Document[] = await query;
|
let result: Document[] = await query;
|
||||||
for(let i of result){
|
for (let i of result) {
|
||||||
i.additional = JSON.parse((i.additional as unknown) as string);
|
i.additional = JSON.parse((i.additional as unknown) as string);
|
||||||
}
|
}
|
||||||
if(eager_loading){
|
if (eager_loading) {
|
||||||
let idmap: {[index:number]:Document} = {};
|
let idmap: { [index: number]: Document } = {};
|
||||||
for(const r of result){
|
for (const r of result) {
|
||||||
idmap[r.id] = r;
|
idmap[r.id] = r;
|
||||||
r.tags = [];
|
r.tags = [];
|
||||||
}
|
}
|
||||||
let subquery = buildquery();
|
let subquery = buildquery();
|
||||||
let tagquery= this.knex.select("id","doc_tag_relation.tag_name").from(subquery)
|
let tagquery = this.knex.select("id", "doc_tag_relation.tag_name").from(subquery)
|
||||||
.innerJoin("doc_tag_relation","doc_tag_relation.doc_id","id");
|
.innerJoin("doc_tag_relation", "doc_tag_relation.doc_id", "id");
|
||||||
//console.log(tagquery.toSQL());
|
// console.log(tagquery.toSQL());
|
||||||
let tagresult:{id:number,tag_name:string}[] = await tagquery;
|
let tagresult: { id: number; tag_name: string }[] = await tagquery;
|
||||||
for(const {id,tag_name} of tagresult){
|
for (const { id, tag_name } of tagresult) {
|
||||||
idmap[id].tags.push(tag_name);
|
idmap[id].tags.push(tag_name);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else{
|
result.forEach(v => {
|
||||||
result.forEach(v=>{v.tags = [];});
|
v.tags = [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
|
||||||
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});
|
|
||||||
return results.map(x=>({
|
|
||||||
...x,
|
|
||||||
tags:[],
|
|
||||||
additional:{}
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
async update(c:Partial<Document> & { id:number }){
|
async findByPath(path: string, filename?: string): Promise<Document[]> {
|
||||||
const {id,tags,...rest} = c;
|
const e = filename == undefined ? {} : { filename: filename };
|
||||||
if (await this.findById(id) !== undefined){
|
const results = await this.knex.select("*").from("document").where({ basepath: path, ...e });
|
||||||
await this.knex.update(rest).where({id: id}).from("document");
|
return results.map(x => ({
|
||||||
|
...x,
|
||||||
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
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});
|
this.tagController.addTag({ name: tag_name });
|
||||||
await this.knex.insert<DBTagContentRelation>({tag_name: tag_name, doc_id: c.id})
|
await this.knex.insert<DBTagContentRelation>({ tag_name: tag_name, doc_id: c.id })
|
||||||
.into("doc_tag_relation");
|
.into("doc_tag_relation");
|
||||||
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.knex.delete().where({ tag_name: tag_name, doc_id: c.id }).from("doc_tag_relation");
|
||||||
c.tags.push(tag_name);
|
c.tags.push(tag_name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const createKnexDocumentAccessor = (knex:Knex): DocumentAccessor=>{
|
export const createKnexDocumentAccessor = (knex: Knex): DocumentAccessor => {
|
||||||
return new KnexDocumentAccessor(knex);
|
return new KnexDocumentAccessor(knex);
|
||||||
}
|
};
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export * from './doc';
|
export * from "./doc";
|
||||||
export * from './tag';
|
export * from "./tag";
|
||||||
export * from './user';
|
export * from "./user";
|
||||||
|
@ -1,57 +1,57 @@
|
|||||||
import {Tag, TagAccessor, TagCount} from '../model/tag';
|
import { Knex } from "knex";
|
||||||
import {Knex} from 'knex';
|
import { Tag, TagAccessor, TagCount } from "../model/tag";
|
||||||
import {DBTagContentRelation} from './doc';
|
import { DBTagContentRelation } from "./doc";
|
||||||
|
|
||||||
type DBTags = {
|
type DBTags = {
|
||||||
name: string,
|
name: string;
|
||||||
description?: string
|
description?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
class KnexTagAccessor implements TagAccessor{
|
class KnexTagAccessor implements TagAccessor {
|
||||||
knex:Knex<DBTags>
|
knex: Knex<DBTags>;
|
||||||
constructor(knex:Knex){
|
constructor(knex: Knex) {
|
||||||
this.knex = knex;
|
this.knex = knex;
|
||||||
}
|
}
|
||||||
async getAllTagCount(): Promise<TagCount[]> {
|
async getAllTagCount(): Promise<TagCount[]> {
|
||||||
const result = await this.knex<DBTagContentRelation>("doc_tag_relation").select("tag_name")
|
const result = await this.knex<DBTagContentRelation>("doc_tag_relation").select("tag_name")
|
||||||
.count("*",{as:"occurs"}).groupBy<TagCount[]>("tag_name");
|
.count("*", { as: "occurs" }).groupBy<TagCount[]>("tag_name");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
async getAllTagList(onlyname?:boolean){
|
async getAllTagList(onlyname?: boolean) {
|
||||||
onlyname = onlyname ?? false;
|
onlyname = onlyname ?? false;
|
||||||
const t:DBTags[] = await this.knex.select(onlyname ? "*" : "name").from("tags")
|
const t: DBTags[] = await this.knex.select(onlyname ? "*" : "name").from("tags");
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
async getTagByName(name:string){
|
async getTagByName(name: string) {
|
||||||
const t:DBTags[] = await this.knex.select('*').from("tags").where({name: name});
|
const t: DBTags[] = await this.knex.select("*").from("tags").where({ name: name });
|
||||||
if(t.length === 0) return undefined;
|
if (t.length === 0) return undefined;
|
||||||
return t[0];
|
return t[0];
|
||||||
}
|
}
|
||||||
async addTag(tag: Tag){
|
async addTag(tag: Tag) {
|
||||||
if(await this.getTagByName(tag.name) === undefined){
|
if (await this.getTagByName(tag.name) === undefined) {
|
||||||
await this.knex.insert<DBTags>({
|
await this.knex.insert<DBTags>({
|
||||||
name:tag.name,
|
name: tag.name,
|
||||||
description:tag.description === undefined ? "" : tag.description
|
description: tag.description === undefined ? "" : tag.description,
|
||||||
}).into("tags");
|
}).into("tags");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
async delTag(name:string){
|
async delTag(name: string) {
|
||||||
if(await this.getTagByName(name) !== undefined){
|
if (await this.getTagByName(name) !== undefined) {
|
||||||
await this.knex.delete().where({name:name}).from("tags");
|
await this.knex.delete().where({ name: name }).from("tags");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
async updateTag(name:string,desc:string){
|
async updateTag(name: string, desc: string) {
|
||||||
if(await this.getTagByName(name) !== undefined){
|
if (await this.getTagByName(name) !== undefined) {
|
||||||
await this.knex.update({description:desc}).where({name:name}).from("tags");
|
await this.knex.update({ description: desc }).where({ name: name }).from("tags");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
export const createKnexTagController = (knex:Knex):TagAccessor=>{
|
|
||||||
return new KnexTagAccessor(knex);
|
|
||||||
}
|
}
|
||||||
|
export const createKnexTagController = (knex: Knex): TagAccessor => {
|
||||||
|
return new KnexTagAccessor(knex);
|
||||||
|
};
|
||||||
|
@ -1,41 +1,41 @@
|
|||||||
import {Knex} from 'knex';
|
import { Knex } from "knex";
|
||||||
import {IUser,UserCreateInput, UserAccessor, Password} from '../model/user';
|
import { IUser, Password, UserAccessor, UserCreateInput } from "../model/user";
|
||||||
|
|
||||||
type PermissionTable = {
|
type PermissionTable = {
|
||||||
username:string,
|
username: string;
|
||||||
name:string
|
name: string;
|
||||||
};
|
};
|
||||||
type DBUser = {
|
type DBUser = {
|
||||||
username : string,
|
username: string;
|
||||||
password_hash: string,
|
password_hash: string;
|
||||||
password_salt: string
|
password_salt: string;
|
||||||
}
|
};
|
||||||
class KnexUser implements IUser{
|
class KnexUser implements IUser {
|
||||||
private knex: Knex;
|
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, knex: Knex) {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.password = pw;
|
this.password = pw;
|
||||||
this.knex = knex;
|
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.from("users")
|
await this.knex.from("users")
|
||||||
.where({username:this.username})
|
.where({ username: this.username })
|
||||||
.update({password_hash:this.password.hash,password_salt:this.password.salt});
|
.update({ password_hash: this.password.hash, password_salt: this.password.salt });
|
||||||
}
|
}
|
||||||
async get_permissions(){
|
async get_permissions() {
|
||||||
let b = (await this.knex.select('*').from("permissions")
|
let b = (await this.knex.select("*").from("permissions")
|
||||||
.where({username : this.username})) as PermissionTable[];
|
.where({ username: this.username })) as PermissionTable[];
|
||||||
return b.map(x=>x.name);
|
return b.map(x => x.name);
|
||||||
}
|
}
|
||||||
async add(name: string) {
|
async add(name: string) {
|
||||||
if(!(await this.get_permissions()).includes(name)){
|
if (!(await this.get_permissions()).includes(name)) {
|
||||||
const r = await this.knex.insert({
|
const r = await this.knex.insert({
|
||||||
username: this.username,
|
username: this.username,
|
||||||
name: name
|
name: name,
|
||||||
}).into("permissions");
|
}).into("permissions");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -45,38 +45,43 @@ class KnexUser implements IUser{
|
|||||||
const r = await this.knex
|
const r = await this.knex
|
||||||
.from("permissions")
|
.from("permissions")
|
||||||
.where({
|
.where({
|
||||||
username:this.username, name:name
|
username: this.username,
|
||||||
|
name: name,
|
||||||
}).delete();
|
}).delete();
|
||||||
return r !== 0;
|
return r !== 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createKnexUserController = (knex: Knex):UserAccessor=>{
|
export const createKnexUserController = (knex: Knex): UserAccessor => {
|
||||||
const createUserKnex = async (input:UserCreateInput)=>{
|
const createUserKnex = async (input: UserCreateInput) => {
|
||||||
if(undefined !== (await findUserKenx(input.username))){
|
if (undefined !== (await findUserKenx(input.username))) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const user = new KnexUser(input.username,new Password(input.password),knex);
|
const user = new KnexUser(input.username, new Password(input.password), knex);
|
||||||
await knex.insert<DBUser>({
|
await knex.insert<DBUser>({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
password_hash: user.password.hash,
|
password_hash: user.password.hash,
|
||||||
password_salt: user.password.salt}).into("users");
|
password_salt: user.password.salt,
|
||||||
|
}).into("users");
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
const findUserKenx = async (id:string)=>{
|
const findUserKenx = async (id: string) => {
|
||||||
let user:DBUser[] = await knex.select("*").from("users").where({username:id});
|
let user: DBUser[] = await knex.select("*").from("users").where({ username: id });
|
||||||
if(user.length == 0) return undefined;
|
if (user.length == 0) return undefined;
|
||||||
const first = user[0];
|
const first = user[0];
|
||||||
return new KnexUser(first.username,
|
return new KnexUser(
|
||||||
new Password({hash: first.password_hash, salt: first.password_salt}), knex);
|
first.username,
|
||||||
}
|
new Password({ hash: first.password_hash, salt: first.password_salt }),
|
||||||
const delUserKnex = async (id:string) => {
|
knex,
|
||||||
let r = await knex.delete().from("users").where({username:id});
|
);
|
||||||
return r===0;
|
};
|
||||||
}
|
const delUserKnex = async (id: string) => {
|
||||||
|
let r = await knex.delete().from("users").where({ username: id });
|
||||||
|
return r === 0;
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
createUser: createUserKnex,
|
createUser: createUserKnex,
|
||||||
findUser: findUserKenx,
|
findUser: findUserKenx,
|
||||||
delUser: delUserKnex,
|
delUser: delUserKnex,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { basename, dirname, join as pathjoin } from 'path';
|
import { basename, dirname, join as pathjoin } from "path";
|
||||||
import { Document, DocumentAccessor } from '../model/mod';
|
import { ContentFile, createContentFile } from "../content/mod";
|
||||||
import { ContentFile, createContentFile } from '../content/mod';
|
import { Document, DocumentAccessor } from "../model/mod";
|
||||||
import { IDiffWatcher } from './watcher';
|
import { ContentList } from "./content_list";
|
||||||
import { ContentList } from './content_list';
|
import { IDiffWatcher } from "./watcher";
|
||||||
|
|
||||||
//refactoring needed.
|
// refactoring needed.
|
||||||
export class ContentDiffHandler {
|
export class ContentDiffHandler {
|
||||||
/** content file list waiting to add */
|
/** content file list waiting to add */
|
||||||
waiting_list: ContentList;
|
waiting_list: ContentList;
|
||||||
/** deleted contents */
|
/** deleted contents */
|
||||||
tombstone: Map<string, Document>;//hash, contentfile
|
tombstone: Map<string, Document>; // hash, contentfile
|
||||||
doc_cntr: DocumentAccessor;
|
doc_cntr: DocumentAccessor;
|
||||||
/** content type of handle */
|
/** content type of handle */
|
||||||
content_type: string;
|
content_type: string;
|
||||||
@ -26,21 +26,21 @@ export class ContentDiffHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
register(diff: IDiffWatcher) {
|
register(diff: IDiffWatcher) {
|
||||||
diff.on('create', (path) => this.OnCreated(path))
|
diff.on("create", (path) => this.OnCreated(path))
|
||||||
.on('delete', (path) => this.OnDeleted(path))
|
.on("delete", (path) => this.OnDeleted(path))
|
||||||
.on('change', (prev, cur) => this.OnChanged(prev, cur));
|
.on("change", (prev, cur) => this.OnChanged(prev, cur));
|
||||||
}
|
}
|
||||||
private async OnDeleted(cpath: string) {
|
private async OnDeleted(cpath: string) {
|
||||||
const basepath = dirname(cpath);
|
const basepath = dirname(cpath);
|
||||||
const filename = basename(cpath);
|
const filename = basename(cpath);
|
||||||
console.log("deleted ", cpath);
|
console.log("deleted ", cpath);
|
||||||
//if it wait to add, delete it from waiting list.
|
// if it wait to add, delete it from waiting list.
|
||||||
if (this.waiting_list.hasByPath(cpath)) {
|
if (this.waiting_list.hasByPath(cpath)) {
|
||||||
this.waiting_list.deleteByPath(cpath);
|
this.waiting_list.deleteByPath(cpath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dbc = await this.doc_cntr.findByPath(basepath, filename);
|
const dbc = await this.doc_cntr.findByPath(basepath, filename);
|
||||||
//when there is no related content in db, ignore.
|
// when there is no related content in db, ignore.
|
||||||
if (dbc.length === 0) {
|
if (dbc.length === 0) {
|
||||||
console.log("its not in waiting_list and db!!!: ", cpath);
|
console.log("its not in waiting_list and db!!!: ", cpath);
|
||||||
return;
|
return;
|
||||||
@ -51,7 +51,7 @@ export class ContentDiffHandler {
|
|||||||
// the change event.
|
// the change event.
|
||||||
const cf = this.waiting_list.getByHash(content_hash);
|
const cf = this.waiting_list.getByHash(content_hash);
|
||||||
if (cf) {
|
if (cf) {
|
||||||
//if a path is changed, update the changed path.
|
// if a path is changed, update the changed path.
|
||||||
console.log("update path from", cpath, "to", cf.path);
|
console.log("update path from", cpath, "to", cf.path);
|
||||||
const newFilename = basename(cf.path);
|
const newFilename = basename(cf.path);
|
||||||
const newBasepath = dirname(cf.path);
|
const newBasepath = dirname(cf.path);
|
||||||
@ -64,7 +64,7 @@ export class ContentDiffHandler {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//invalidate db and add it to tombstone.
|
// invalidate db and add it to tombstone.
|
||||||
await this.doc_cntr.update({
|
await this.doc_cntr.update({
|
||||||
id: dbc[0].id,
|
id: dbc[0].id,
|
||||||
deleted_at: Date.now(),
|
deleted_at: Date.now(),
|
||||||
@ -83,14 +83,13 @@ export class ContentDiffHandler {
|
|||||||
id: c.id,
|
id: c.id,
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
basepath: basepath
|
basepath: basepath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.waiting_list.hasByHash(hash)) {
|
if (this.waiting_list.hasByHash(hash)) {
|
||||||
console.log("Hash Conflict!!!");
|
console.log("Hash Conflict!!!");
|
||||||
}
|
}
|
||||||
this.waiting_list.set(content);
|
this.waiting_list.set(content);
|
||||||
|
|
||||||
}
|
}
|
||||||
private async OnChanged(prev_path: string, cur_path: string) {
|
private async OnChanged(prev_path: string, cur_path: string) {
|
||||||
const prev_basepath = dirname(prev_path);
|
const prev_basepath = dirname(prev_path);
|
||||||
@ -99,7 +98,7 @@ export class ContentDiffHandler {
|
|||||||
const cur_filename = basename(cur_path);
|
const cur_filename = basename(cur_path);
|
||||||
console.log("modify", cur_path, "from", prev_path);
|
console.log("modify", cur_path, "from", prev_path);
|
||||||
const c = this.waiting_list.getByPath(prev_path);
|
const c = this.waiting_list.getByPath(prev_path);
|
||||||
if(c !== undefined){
|
if (c !== undefined) {
|
||||||
await this.waiting_list.delete(c);
|
await this.waiting_list.delete(c);
|
||||||
const content = createContentFile(this.content_type, cur_path);
|
const content = createContentFile(this.content_type, cur_path);
|
||||||
await this.waiting_list.set(content);
|
await this.waiting_list.set(content);
|
||||||
@ -107,7 +106,7 @@ export class ContentDiffHandler {
|
|||||||
}
|
}
|
||||||
const doc = await this.doc_cntr.findByPath(prev_basepath, prev_filename);
|
const doc = await this.doc_cntr.findByPath(prev_basepath, prev_filename);
|
||||||
|
|
||||||
if(doc.length === 0){
|
if (doc.length === 0) {
|
||||||
await this.OnCreated(cur_path);
|
await this.OnCreated(cur_path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -115,7 +114,7 @@ export class ContentDiffHandler {
|
|||||||
await this.doc_cntr.update({
|
await this.doc_cntr.update({
|
||||||
...doc[0],
|
...doc[0],
|
||||||
basepath: cur_basepath,
|
basepath: cur_basepath,
|
||||||
filename: cur_filename
|
filename: cur_filename,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,59 +1,59 @@
|
|||||||
import { ContentFile } from '../content/mod';
|
import { ContentFile } from "../content/mod";
|
||||||
|
|
||||||
export class ContentList{
|
export class ContentList {
|
||||||
/** path map */
|
/** path map */
|
||||||
private cl:Map<string,ContentFile>;
|
private cl: Map<string, ContentFile>;
|
||||||
/** hash map */
|
/** hash map */
|
||||||
private hl:Map<string,ContentFile>;
|
private hl: Map<string, ContentFile>;
|
||||||
|
|
||||||
constructor(){
|
constructor() {
|
||||||
this.cl = new Map;
|
this.cl = new Map();
|
||||||
this.hl = new Map;
|
this.hl = new Map();
|
||||||
}
|
}
|
||||||
hasByHash(s:string){
|
hasByHash(s: string) {
|
||||||
return this.hl.has(s);
|
return this.hl.has(s);
|
||||||
}
|
}
|
||||||
hasByPath(p:string){
|
hasByPath(p: string) {
|
||||||
return this.cl.has(p);
|
return this.cl.has(p);
|
||||||
}
|
}
|
||||||
getByHash(s:string){
|
getByHash(s: string) {
|
||||||
return this.hl.get(s)
|
return this.hl.get(s);
|
||||||
}
|
}
|
||||||
getByPath(p:string){
|
getByPath(p: string) {
|
||||||
return this.cl.get(p);
|
return this.cl.get(p);
|
||||||
}
|
}
|
||||||
async set(c:ContentFile){
|
async set(c: ContentFile) {
|
||||||
const path = c.path;
|
const path = c.path;
|
||||||
const hash = await c.getHash();
|
const hash = await c.getHash();
|
||||||
this.cl.set(path,c);
|
this.cl.set(path, c);
|
||||||
this.hl.set(hash,c);
|
this.hl.set(hash, c);
|
||||||
}
|
}
|
||||||
/** delete content file */
|
/** delete content file */
|
||||||
async delete(c:ContentFile){
|
async delete(c: ContentFile) {
|
||||||
const hash = await c.getHash();
|
const hash = await c.getHash();
|
||||||
let r = true;
|
let r = true;
|
||||||
r = this.cl.delete(c.path) && r;
|
r = this.cl.delete(c.path) && r;
|
||||||
r = this.hl.delete(hash) && r;
|
r = this.hl.delete(hash) && r;
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
async deleteByPath(p:string){
|
async deleteByPath(p: string) {
|
||||||
const o = this.getByPath(p);
|
const o = this.getByPath(p);
|
||||||
if(o === undefined) return false;
|
if (o === undefined) return false;
|
||||||
return await this.delete(o);
|
return await this.delete(o);
|
||||||
}
|
}
|
||||||
deleteByHash(s:string){
|
deleteByHash(s: string) {
|
||||||
const o = this.getByHash(s);
|
const o = this.getByHash(s);
|
||||||
if(o === undefined) return false;
|
if (o === undefined) return false;
|
||||||
let r = true;
|
let r = true;
|
||||||
r = this.cl.delete(o.path) && r;
|
r = this.cl.delete(o.path) && r;
|
||||||
r = this.hl.delete(s) && r;
|
r = this.hl.delete(s) && r;
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
clear(){
|
clear() {
|
||||||
this.cl.clear();
|
this.cl.clear();
|
||||||
this.hl.clear();
|
this.hl.clear();
|
||||||
}
|
}
|
||||||
getAll(){
|
getAll() {
|
||||||
return [...this.cl.values()];
|
return [...this.cl.values()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import { DocumentAccessor } from '../model/doc';
|
import asyncPool from "tiny-async-pool";
|
||||||
import {ContentDiffHandler} from './content_handler';
|
import { DocumentAccessor } from "../model/doc";
|
||||||
import { IDiffWatcher } from './watcher';
|
import { ContentDiffHandler } from "./content_handler";
|
||||||
import asyncPool from 'tiny-async-pool';
|
import { IDiffWatcher } from "./watcher";
|
||||||
|
|
||||||
export class DiffManager{
|
export class DiffManager {
|
||||||
watching: {[content_type:string]:ContentDiffHandler};
|
watching: { [content_type: string]: ContentDiffHandler };
|
||||||
doc_cntr: DocumentAccessor;
|
doc_cntr: DocumentAccessor;
|
||||||
constructor(contorller: DocumentAccessor){
|
constructor(contorller: DocumentAccessor) {
|
||||||
this.watching = {};
|
this.watching = {};
|
||||||
this.doc_cntr = contorller;
|
this.doc_cntr = contorller;
|
||||||
}
|
}
|
||||||
async register(content_type:string,watcher:IDiffWatcher){
|
async register(content_type: string, watcher: IDiffWatcher) {
|
||||||
if(this.watching[content_type] === undefined){
|
if (this.watching[content_type] === undefined) {
|
||||||
this.watching[content_type] = new ContentDiffHandler(this.doc_cntr,content_type);
|
this.watching[content_type] = new ContentDiffHandler(this.doc_cntr, content_type);
|
||||||
}
|
}
|
||||||
this.watching[content_type].register(watcher);
|
this.watching[content_type].register(watcher);
|
||||||
await watcher.setup(this.doc_cntr);
|
await watcher.setup(this.doc_cntr);
|
||||||
}
|
}
|
||||||
async commit(type:string,path:string){
|
async commit(type: string, path: string) {
|
||||||
const list = this.watching[type].waiting_list;
|
const list = this.watching[type].waiting_list;
|
||||||
const c = list.getByPath(path);
|
const c = list.getByPath(path);
|
||||||
if(c===undefined){
|
if (c === undefined) {
|
||||||
throw new Error("path is not exist");
|
throw new Error("path is not exist");
|
||||||
}
|
}
|
||||||
await list.delete(c);
|
await list.delete(c);
|
||||||
@ -28,18 +28,18 @@ export class DiffManager{
|
|||||||
const id = await this.doc_cntr.add(body);
|
const id = await this.doc_cntr.add(body);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
async commitAll(type:string){
|
async commitAll(type: string) {
|
||||||
const list = this.watching[type].waiting_list;
|
const list = this.watching[type].waiting_list;
|
||||||
const contentFiles = list.getAll();
|
const contentFiles = list.getAll();
|
||||||
list.clear();
|
list.clear();
|
||||||
const bodies = await asyncPool(30,contentFiles,async (x)=>await x.createDocumentBody());
|
const bodies = await asyncPool(30, contentFiles, async (x) => await x.createDocumentBody());
|
||||||
const ids = await this.doc_cntr.addList(bodies);
|
const ids = await this.doc_cntr.addList(bodies);
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
getAdded(){
|
getAdded() {
|
||||||
return Object.keys(this.watching).map(x=>({
|
return Object.keys(this.watching).map(x => ({
|
||||||
type:x,
|
type: x,
|
||||||
value:this.watching[x].waiting_list.getAll(),
|
value: this.watching[x].waiting_list.getAll(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export * from './router';
|
export * from "./diff";
|
||||||
export * from './diff';
|
export * from "./router";
|
||||||
|
@ -1,70 +1,70 @@
|
|||||||
import Koa from 'koa';
|
import Koa from "koa";
|
||||||
import Router from 'koa-router';
|
import Router from "koa-router";
|
||||||
import { ContentFile } from '../content/mod';
|
import { ContentFile } from "../content/mod";
|
||||||
import { sendError } from '../route/error_handler';
|
import { AdminOnlyMiddleware } from "../permission/permission";
|
||||||
import {DiffManager} from './diff';
|
import { sendError } from "../route/error_handler";
|
||||||
import {AdminOnlyMiddleware} from '../permission/permission';
|
import { 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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAdded = (diffmgr:DiffManager)=> (ctx:Koa.Context,next:Koa.Next)=>{
|
export const getAdded = (diffmgr: DiffManager) => (ctx: Koa.Context, next: Koa.Next) => {
|
||||||
const ret = diffmgr.getAdded();
|
const ret = diffmgr.getAdded();
|
||||||
ctx.body = ret.map(x=>({
|
ctx.body = ret.map(x => ({
|
||||||
type:x.type,
|
type: x.type,
|
||||||
value:x.value.map(x=>({path:x.path,type:x.type})),
|
value: x.value.map(x => ({ path: x.path, type: x.type })),
|
||||||
}));
|
}));
|
||||||
ctx.type = 'json';
|
ctx.type = "json";
|
||||||
}
|
};
|
||||||
|
|
||||||
type PostAddedBody = {
|
type PostAddedBody = {
|
||||||
type:string,
|
type: string;
|
||||||
path:string,
|
path: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
function checkPostAddedBody(body: any): body is PostAddedBody{
|
function checkPostAddedBody(body: any): body is PostAddedBody {
|
||||||
if(body instanceof Array){
|
if (body instanceof Array) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postAdded = (diffmgr:DiffManager) => async (ctx:Router.IRouterContext,next:Koa.Next)=>{
|
export const postAdded = (diffmgr: DiffManager) => async (ctx: Router.IRouterContext, next: Koa.Next) => {
|
||||||
const reqbody = ctx.request.body;
|
const reqbody = ctx.request.body;
|
||||||
if(!checkPostAddedBody(reqbody)){
|
if (!checkPostAddedBody(reqbody)) {
|
||||||
sendError(400,"format exception");
|
sendError(400, "format exception");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const allWork = reqbody.map(op=>diffmgr.commit(op.type,op.path));
|
const allWork = reqbody.map(op => diffmgr.commit(op.type, op.path));
|
||||||
const results = await Promise.all(allWork);
|
const results = await Promise.all(allWork);
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
ok:true,
|
ok: true,
|
||||||
docs:results,
|
docs: results,
|
||||||
}
|
};
|
||||||
ctx.type = 'json';
|
ctx.type = "json";
|
||||||
}
|
};
|
||||||
export const postAddedAll = (diffmgr: DiffManager) => async (ctx:Router.IRouterContext,next:Koa.Next) => {
|
export const postAddedAll = (diffmgr: DiffManager) => async (ctx: Router.IRouterContext, next: Koa.Next) => {
|
||||||
if (!ctx.is('json')){
|
if (!ctx.is("json")) {
|
||||||
sendError(400,"format exception");
|
sendError(400, "format exception");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reqbody = ctx.request.body as Record<string,unknown>;
|
const reqbody = ctx.request.body as Record<string, unknown>;
|
||||||
if(!("type" in reqbody)){
|
if (!("type" in reqbody)) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
await diffmgr.commitAll(t);
|
await diffmgr.commitAll(t);
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
ok:true
|
ok: true,
|
||||||
};
|
};
|
||||||
ctx.type = 'json';
|
ctx.type = "json";
|
||||||
}
|
};
|
||||||
/*
|
/*
|
||||||
export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContext,next:Koa.Next)=>{
|
export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContext,next:Koa.Next)=>{
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
@ -74,10 +74,10 @@ export const getNotWatched = (diffmgr : DiffManager)=> (ctx:Router.IRouterContex
|
|||||||
ctx.type = 'json';
|
ctx.type = 'json';
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
export function createDiffRouter(diffmgr: DiffManager){
|
export function createDiffRouter(diffmgr: DiffManager) {
|
||||||
const ret = new Router();
|
const ret = new Router();
|
||||||
ret.get("/list",AdminOnlyMiddleware,getAdded(diffmgr));
|
ret.get("/list", AdminOnlyMiddleware, getAdded(diffmgr));
|
||||||
ret.post("/commit",AdminOnlyMiddleware,postAdded(diffmgr));
|
ret.post("/commit", AdminOnlyMiddleware, postAdded(diffmgr));
|
||||||
ret.post("/commitall",AdminOnlyMiddleware,postAddedAll(diffmgr));
|
ret.post("/commitall", AdminOnlyMiddleware, postAddedAll(diffmgr));
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
@ -1,25 +1,25 @@
|
|||||||
import { FSWatcher, watch } from 'fs';
|
import event from "events";
|
||||||
import { promises } from 'fs';
|
import { FSWatcher, watch } from "fs";
|
||||||
import event from 'events';
|
import { promises } from "fs";
|
||||||
import { join } from 'path';
|
import { join } from "path";
|
||||||
import { DocumentAccessor } from '../model/doc';
|
import { DocumentAccessor } from "../model/doc";
|
||||||
|
|
||||||
const readdir = promises.readdir;
|
const readdir = promises.readdir;
|
||||||
|
|
||||||
export interface DiffWatcherEvent{
|
export interface DiffWatcherEvent {
|
||||||
'create':(path:string)=>void,
|
"create": (path: string) => void;
|
||||||
'delete':(path:string)=>void,
|
"delete": (path: string) => void;
|
||||||
'change':(prev_path:string,cur_path:string)=>void,
|
"change": (prev_path: string, cur_path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDiffWatcher extends event.EventEmitter {
|
export interface IDiffWatcher extends event.EventEmitter {
|
||||||
on<U extends keyof DiffWatcherEvent>(event:U,listener:DiffWatcherEvent[U]): this;
|
on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this;
|
||||||
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean;
|
emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean;
|
||||||
setup(cntr:DocumentAccessor):Promise<void>;
|
setup(cntr: DocumentAccessor): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function linkWatcher(fromWatcher :IDiffWatcher, toWatcher: IDiffWatcher){
|
export function linkWatcher(fromWatcher: IDiffWatcher, toWatcher: IDiffWatcher) {
|
||||||
fromWatcher.on("create",p=>toWatcher.emit("create",p));
|
fromWatcher.on("create", p => toWatcher.emit("create", p));
|
||||||
fromWatcher.on("delete",p=>toWatcher.emit("delete",p));
|
fromWatcher.on("delete", p => toWatcher.emit("delete", p));
|
||||||
fromWatcher.on("change",(p,c)=>toWatcher.emit("change",p,c));
|
fromWatcher.on("change", (p, c) => toWatcher.emit("change", p, c));
|
||||||
}
|
}
|
@ -1 +1,12 @@
|
|||||||
{"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#/definitions/ComicConfig","definitions":{"ComicConfig":{"type":"object","properties":{"watch":{"type":"array","items":{"type":"string"}},"$schema":{"type":"string"}},"required":["watch"],"additionalProperties":false}}}
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$ref": "#/definitions/ComicConfig",
|
||||||
|
"definitions": {
|
||||||
|
"ComicConfig": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": { "watch": { "type": "array", "items": { "type": "string" } }, "$schema": { "type": "string" } },
|
||||||
|
"required": ["watch"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import {ConfigManager} from '../../util/configRW';
|
import { ConfigManager } from "../../util/configRW";
|
||||||
import ComicSchema from "./ComicConfig.schema.json"
|
import ComicSchema from "./ComicConfig.schema.json";
|
||||||
export interface ComicConfig{
|
export interface ComicConfig {
|
||||||
watch:string[]
|
watch: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ComicConfig = new ConfigManager<ComicConfig>("comic_config.json",{watch:[]},ComicSchema);
|
export const ComicConfig = new ConfigManager<ComicConfig>("comic_config.json", { watch: [] }, ComicSchema);
|
||||||
|
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import {IDiffWatcher, DiffWatcherEvent} from '../watcher';
|
import { EventEmitter } from "events";
|
||||||
import {EventEmitter} from 'events';
|
import { DocumentAccessor } from "../../model/doc";
|
||||||
import { DocumentAccessor } from '../../model/doc';
|
import { DiffWatcherEvent, IDiffWatcher } from "../watcher";
|
||||||
import { WatcherFilter } from './watcher_filter';
|
import { ComicConfig } from "./ComicConfig";
|
||||||
import { RecursiveWatcher } from './recursive_watcher';
|
import { WatcherCompositer } from "./compositer";
|
||||||
import { ComicConfig } from './ComicConfig';
|
import { RecursiveWatcher } from "./recursive_watcher";
|
||||||
import {WatcherCompositer} from './compositer'
|
import { WatcherFilter } from "./watcher_filter";
|
||||||
|
|
||||||
|
const createComicWatcherBase = (path: string) => {
|
||||||
const createComicWatcherBase = (path:string)=> {
|
return new WatcherFilter(new RecursiveWatcher(path), (x) => x.endsWith(".zip"));
|
||||||
return new WatcherFilter(new RecursiveWatcher(path),(x)=>x.endsWith(".zip"));
|
};
|
||||||
}
|
export const createComicWatcher = () => {
|
||||||
export const createComicWatcher = ()=>{
|
|
||||||
const file = ComicConfig.get_config_file();
|
const file = ComicConfig.get_config_file();
|
||||||
console.log(`register comic ${file.watch.join(",")}`)
|
console.log(`register comic ${file.watch.join(",")}`);
|
||||||
return new WatcherCompositer(file.watch.map(path=>createComicWatcherBase(path)));
|
return new WatcherCompositer(file.watch.map(path => createComicWatcherBase(path)));
|
||||||
}
|
};
|
||||||
|
@ -1,45 +1,44 @@
|
|||||||
import event from 'events';
|
import event from "events";
|
||||||
import {FSWatcher,watch,promises} from 'fs';
|
import { FSWatcher, promises, watch } from "fs";
|
||||||
import {IDiffWatcher, DiffWatcherEvent} from '../watcher';
|
import { join } from "path";
|
||||||
import {join} from 'path';
|
import { DocumentAccessor } from "../../model/doc";
|
||||||
import { DocumentAccessor } from '../../model/doc';
|
import { DiffWatcherEvent, IDiffWatcher } from "../watcher";
|
||||||
import { setupHelp } from './util';
|
import { setupHelp } from "./util";
|
||||||
|
|
||||||
const {readdir} = promises;
|
const { readdir } = promises;
|
||||||
|
|
||||||
export class CommonDiffWatcher extends event.EventEmitter implements IDiffWatcher{
|
export class CommonDiffWatcher extends event.EventEmitter implements IDiffWatcher {
|
||||||
on<U extends keyof DiffWatcherEvent>(event:U,listener:DiffWatcherEvent[U]): this{
|
on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this {
|
||||||
return super.on(event,listener);
|
return super.on(event, listener);
|
||||||
}
|
}
|
||||||
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean{
|
emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean {
|
||||||
return super.emit(event,...arg);
|
return super.emit(event, ...arg);
|
||||||
}
|
}
|
||||||
private _path:string;
|
private _path: string;
|
||||||
private _watcher: FSWatcher;
|
private _watcher: FSWatcher;
|
||||||
|
|
||||||
constructor(path:string){
|
constructor(path: string) {
|
||||||
super();
|
super();
|
||||||
this._path = path;
|
this._path = path;
|
||||||
this._watcher = watch(this._path,{persistent: true, recursive:false},async (eventType,filename)=>{
|
this._watcher = watch(this._path, { persistent: true, recursive: false }, async (eventType, filename) => {
|
||||||
if(eventType === "rename"){
|
if (eventType === "rename") {
|
||||||
const cur = await readdir(this._path);
|
const cur = await readdir(this._path);
|
||||||
//add
|
// add
|
||||||
if(cur.includes(filename)){
|
if (cur.includes(filename)) {
|
||||||
this.emit('create',join(this.path,filename));
|
this.emit("create", join(this.path, filename));
|
||||||
}
|
} else {
|
||||||
else{
|
this.emit("delete", join(this.path, filename));
|
||||||
this.emit('delete',join(this.path,filename))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async setup(cntr: DocumentAccessor): Promise<void> {
|
async setup(cntr: DocumentAccessor): Promise<void> {
|
||||||
await setupHelp(this,this.path,cntr);
|
await setupHelp(this, this.path, cntr);
|
||||||
}
|
}
|
||||||
public get path(){
|
public get path() {
|
||||||
return this._path;
|
return this._path;
|
||||||
}
|
}
|
||||||
watchClose(){
|
watchClose() {
|
||||||
this._watcher.close()
|
this._watcher.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,23 +2,22 @@ import { EventEmitter } from "events";
|
|||||||
import { DocumentAccessor } from "../../model/doc";
|
import { DocumentAccessor } from "../../model/doc";
|
||||||
import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher";
|
import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher";
|
||||||
|
|
||||||
|
export class WatcherCompositer extends EventEmitter implements IDiffWatcher {
|
||||||
export class WatcherCompositer extends EventEmitter implements IDiffWatcher{
|
refWatchers: IDiffWatcher[];
|
||||||
refWatchers : IDiffWatcher[];
|
on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this {
|
||||||
on<U extends keyof DiffWatcherEvent>(event:U,listener:DiffWatcherEvent[U]): this{
|
return super.on(event, listener);
|
||||||
return super.on(event,listener);
|
|
||||||
}
|
}
|
||||||
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean{
|
emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean {
|
||||||
return super.emit(event,...arg);
|
return super.emit(event, ...arg);
|
||||||
}
|
}
|
||||||
constructor(refWatchers:IDiffWatcher[]){
|
constructor(refWatchers: IDiffWatcher[]) {
|
||||||
super();
|
super();
|
||||||
this.refWatchers = refWatchers;
|
this.refWatchers = refWatchers;
|
||||||
for(const refWatcher of this.refWatchers){
|
for (const refWatcher of this.refWatchers) {
|
||||||
linkWatcher(refWatcher,this);
|
linkWatcher(refWatcher, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async setup(cntr: DocumentAccessor): Promise<void> {
|
async setup(cntr: DocumentAccessor): Promise<void> {
|
||||||
await Promise.all(this.refWatchers.map(x=>x.setup(cntr)));
|
await Promise.all(this.refWatchers.map(x => x.setup(cntr)));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,61 +1,61 @@
|
|||||||
import {watch, FSWatcher} from 'chokidar';
|
import { FSWatcher, watch } from "chokidar";
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from "events";
|
||||||
import { join } from 'path';
|
import { join } from "path";
|
||||||
import { DocumentAccessor } from '../../model/doc';
|
import { DocumentAccessor } from "../../model/doc";
|
||||||
import { DiffWatcherEvent, IDiffWatcher } from '../watcher';
|
import { DiffWatcherEvent, IDiffWatcher } from "../watcher";
|
||||||
import { setupHelp, setupRecursive } from './util';
|
import { setupHelp, setupRecursive } from "./util";
|
||||||
|
|
||||||
type RecursiveWatcherOption={
|
type RecursiveWatcherOption = {
|
||||||
/** @default true */
|
/** @default true */
|
||||||
watchFile?:boolean,
|
watchFile?: boolean;
|
||||||
/** @default false */
|
/** @default false */
|
||||||
watchDir?:boolean,
|
watchDir?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export class RecursiveWatcher extends EventEmitter implements IDiffWatcher {
|
export class RecursiveWatcher extends EventEmitter implements IDiffWatcher {
|
||||||
on<U extends keyof DiffWatcherEvent>(event:U,listener:DiffWatcherEvent[U]): this{
|
on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this {
|
||||||
return super.on(event,listener);
|
return super.on(event, listener);
|
||||||
}
|
}
|
||||||
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean{
|
emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean {
|
||||||
return super.emit(event,...arg);
|
return super.emit(event, ...arg);
|
||||||
}
|
}
|
||||||
readonly path: string;
|
readonly path: string;
|
||||||
private watcher: FSWatcher
|
private watcher: FSWatcher;
|
||||||
|
|
||||||
constructor(path:string, option:RecursiveWatcherOption = {
|
constructor(path: string, option: RecursiveWatcherOption = {
|
||||||
watchDir:false,
|
watchDir: false,
|
||||||
watchFile:true,
|
watchFile: true,
|
||||||
}){
|
}) {
|
||||||
super();
|
super();
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.watcher = watch(path,{
|
this.watcher = watch(path, {
|
||||||
persistent:true,
|
persistent: true,
|
||||||
ignoreInitial:true,
|
ignoreInitial: true,
|
||||||
depth:100,
|
depth: 100,
|
||||||
});
|
});
|
||||||
option.watchFile ??= true;
|
option.watchFile ??= true;
|
||||||
if(option.watchFile){
|
if (option.watchFile) {
|
||||||
this.watcher.on("add",path=>{
|
this.watcher.on("add", path => {
|
||||||
const cpath = path;
|
const cpath = path;
|
||||||
//console.log("add ", cpath);
|
// console.log("add ", cpath);
|
||||||
this.emit("create",cpath);
|
this.emit("create", cpath);
|
||||||
}).on("unlink",path=>{
|
}).on("unlink", path => {
|
||||||
const cpath = path;
|
const cpath = path;
|
||||||
//console.log("unlink ", cpath);
|
// console.log("unlink ", cpath);
|
||||||
this.emit("delete",cpath);
|
this.emit("delete", cpath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(option.watchDir){
|
if (option.watchDir) {
|
||||||
this.watcher.on("addDir",path=>{
|
this.watcher.on("addDir", path => {
|
||||||
const cpath = path;
|
const cpath = path;
|
||||||
this.emit("create",cpath);
|
this.emit("create", cpath);
|
||||||
}).on("unlinkDir",path=>{
|
}).on("unlinkDir", path => {
|
||||||
const cpath = path;
|
const cpath = path;
|
||||||
this.emit("delete",cpath);
|
this.emit("delete", cpath);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async setup(cntr: DocumentAccessor): Promise<void> {
|
async setup(cntr: DocumentAccessor): Promise<void> {
|
||||||
await setupRecursive(this,this.path,cntr);
|
await setupRecursive(this, this.path, cntr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,36 @@
|
|||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { promises } from "fs";
|
import { promises } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
const {readdir} = promises;
|
const { readdir } = promises;
|
||||||
import { DocumentAccessor } from "../../model/doc";
|
import { DocumentAccessor } from "../../model/doc";
|
||||||
import { IDiffWatcher } from "../watcher";
|
import { 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));
|
let added = cur.filter(x => !initial_filenames.includes(x));
|
||||||
let deleted = initial_filenames.filter(x=>!cur.includes(x));
|
let 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);
|
||||||
}
|
}
|
||||||
for (const it of deleted){
|
for (const it of deleted) {
|
||||||
const cpath = join(basepath,it);
|
const cpath = join(basepath, it);
|
||||||
watcher.emit('delete',cpath);
|
watcher.emit("delete", cpath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function setupHelp(watcher:IDiffWatcher,basepath:string,cntr:DocumentAccessor){
|
export async function setupHelp(watcher: IDiffWatcher, basepath: string, cntr: DocumentAccessor) {
|
||||||
const initial_document = await cntr.findByPath(basepath);
|
const initial_document = await cntr.findByPath(basepath);
|
||||||
const initial_filenames = initial_document.map(x=>x.filename);
|
const initial_filenames = initial_document.map(x => x.filename);
|
||||||
const cur = await readdir(basepath);
|
const cur = await readdir(basepath);
|
||||||
setupCommon(watcher,basepath,initial_filenames,cur);
|
setupCommon(watcher, basepath, initial_filenames, cur);
|
||||||
}
|
}
|
||||||
export async function setupRecursive(watcher:IDiffWatcher,basepath:string,cntr:DocumentAccessor){
|
export async function setupRecursive(watcher: IDiffWatcher, basepath: string, cntr: DocumentAccessor) {
|
||||||
const initial_document = await cntr.findByPath(basepath);
|
const initial_document = await cntr.findByPath(basepath);
|
||||||
const initial_filenames = initial_document.map(x=>x.filename);
|
const initial_filenames = initial_document.map(x => x.filename);
|
||||||
const cur = await readdir(basepath,{withFileTypes:true});
|
const cur = await readdir(basepath, { withFileTypes: true });
|
||||||
setupCommon(watcher,basepath,initial_filenames,cur.map(x=>x.name));
|
setupCommon(watcher, basepath, initial_filenames, cur.map(x => x.name));
|
||||||
await Promise.all([cur.filter(x=>x.isDirectory())
|
await Promise.all([
|
||||||
.map(x=>setupHelp(watcher,join(basepath,x.name),cntr))]);
|
cur.filter(x => x.isDirectory())
|
||||||
|
.map(x => setupHelp(watcher, join(basepath, x.name), cntr)),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,11 @@ import { EventEmitter } from "events";
|
|||||||
import { DocumentAccessor } from "../../model/doc";
|
import { DocumentAccessor } from "../../model/doc";
|
||||||
import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher";
|
import { DiffWatcherEvent, IDiffWatcher, linkWatcher } from "../watcher";
|
||||||
|
|
||||||
|
export class WatcherFilter extends EventEmitter implements IDiffWatcher {
|
||||||
export class WatcherFilter extends EventEmitter implements IDiffWatcher{
|
refWatcher: IDiffWatcher;
|
||||||
refWatcher : IDiffWatcher;
|
filter: (filename: string) => boolean;
|
||||||
filter : (filename:string)=>boolean;;
|
on<U extends keyof DiffWatcherEvent>(event: U, listener: DiffWatcherEvent[U]): this {
|
||||||
on<U extends keyof DiffWatcherEvent>(event:U,listener:DiffWatcherEvent[U]): this{
|
return super.on(event, listener);
|
||||||
return super.on(event,listener);
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* emit event
|
* emit event
|
||||||
@ -15,37 +14,33 @@ export class WatcherFilter extends EventEmitter implements IDiffWatcher{
|
|||||||
* @param arg
|
* @param arg
|
||||||
* @returns `true` if the event had listeners, `false` otherwise.
|
* @returns `true` if the event had listeners, `false` otherwise.
|
||||||
*/
|
*/
|
||||||
emit<U extends keyof DiffWatcherEvent>(event:U,...arg:Parameters<DiffWatcherEvent[U]>): boolean{
|
emit<U extends keyof DiffWatcherEvent>(event: U, ...arg: Parameters<DiffWatcherEvent[U]>): boolean {
|
||||||
if(event === "change"){
|
if (event === "change") {
|
||||||
const prev = arg[0];
|
const prev = arg[0];
|
||||||
const cur = arg[1] as string;
|
const cur = arg[1] as string;
|
||||||
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{
|
} else {
|
||||||
return super.emit("delete",cur);
|
if (this.filter(cur)) {
|
||||||
}
|
return super.emit("create", cur);
|
||||||
}
|
|
||||||
else{
|
|
||||||
if(this.filter(cur)){
|
|
||||||
return super.emit("create",cur);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
} else if (!this.filter(arg[0])) {
|
||||||
else if(!this.filter(arg[0])){
|
|
||||||
return false;
|
return false;
|
||||||
}
|
} else return super.emit(event, ...arg);
|
||||||
else return super.emit(event,...arg);
|
|
||||||
}
|
}
|
||||||
constructor(refWatcher:IDiffWatcher, filter:(filename:string)=>boolean){
|
constructor(refWatcher: IDiffWatcher, filter: (filename: string) => boolean) {
|
||||||
super();
|
super();
|
||||||
this.refWatcher = refWatcher;
|
this.refWatcher = refWatcher;
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
linkWatcher(refWatcher,this);
|
linkWatcher(refWatcher, this);
|
||||||
}
|
}
|
||||||
setup(cntr:DocumentAccessor): Promise<void> {
|
setup(cntr: DocumentAccessor): Promise<void> {
|
||||||
return this.refWatcher.setup(cntr);
|
return this.refWatcher.setup(cntr);
|
||||||
}
|
}
|
||||||
}
|
}
|
386
src/login.ts
386
src/login.ts
@ -1,289 +1,285 @@
|
|||||||
|
import { request } from "http";
|
||||||
import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken";
|
import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken";
|
||||||
|
import Knex from "knex";
|
||||||
import Koa from "koa";
|
import Koa from "koa";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import { sendError } from "./route/error_handler";
|
|
||||||
import Knex from "knex";
|
|
||||||
import { createKnexUserController } from "./db/mod";
|
import { createKnexUserController } from "./db/mod";
|
||||||
import { request } from "http";
|
|
||||||
import { get_setting } from "./SettingConfig";
|
|
||||||
import { IUser, UserAccessor } from "./model/mod";
|
import { IUser, UserAccessor } from "./model/mod";
|
||||||
|
import { sendError } from "./route/error_handler";
|
||||||
|
import { get_setting } from "./SettingConfig";
|
||||||
|
|
||||||
type PayloadInfo = {
|
type PayloadInfo = {
|
||||||
username: string;
|
username: string;
|
||||||
permission: string[];
|
permission: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserState = {
|
export type UserState = {
|
||||||
user: PayloadInfo;
|
user: PayloadInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 &&
|
return "username" in obj && "permission" in obj
|
||||||
(obj as { permission: unknown }).permission instanceof Array;
|
&& (obj as { permission: unknown }).permission instanceof Array;
|
||||||
};
|
};
|
||||||
type RefreshPayloadInfo = { username: string };
|
type RefreshPayloadInfo = { username: string };
|
||||||
const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => {
|
const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => {
|
||||||
if (typeof obj === "string") return false;
|
if (typeof obj === "string") return false;
|
||||||
return "username" in obj &&
|
return "username" in obj
|
||||||
typeof (obj as { username: unknown }).username === "string";
|
&& typeof (obj as { username: unknown }).username === "string";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accessTokenName = "access_token";
|
export const accessTokenName = "access_token";
|
||||||
export const refreshTokenName = "refresh_token";
|
export const refreshTokenName = "refresh_token";
|
||||||
const accessExpiredTime = 60 * 60; //1 hour
|
const accessExpiredTime = 60 * 60; // 1 hour
|
||||||
const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 day;
|
const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 day;
|
||||||
|
|
||||||
export const getAdminAccessTokenValue = () => {
|
export const getAdminAccessTokenValue = () => {
|
||||||
const { jwt_secretkey } = get_setting();
|
const { jwt_secretkey } = get_setting();
|
||||||
return publishAccessToken(jwt_secretkey, "admin", [], accessExpiredTime);
|
return publishAccessToken(jwt_secretkey, "admin", [], accessExpiredTime);
|
||||||
};
|
};
|
||||||
export const getAdminRefreshTokenValue = () => {
|
export const getAdminRefreshTokenValue = () => {
|
||||||
const { jwt_secretkey } = get_setting();
|
const { jwt_secretkey } = get_setting();
|
||||||
return publishRefreshToken(jwt_secretkey, "admin", refreshExpiredTime);
|
return publishRefreshToken(jwt_secretkey, "admin", refreshExpiredTime);
|
||||||
};
|
};
|
||||||
const publishAccessToken = (
|
const publishAccessToken = (
|
||||||
secretKey: string,
|
secretKey: string,
|
||||||
username: string,
|
username: string,
|
||||||
permission: string[],
|
permission: string[],
|
||||||
expiredtime: number,
|
expiredtime: number,
|
||||||
) => {
|
) => {
|
||||||
const payload = sign(
|
const payload = sign(
|
||||||
{
|
{
|
||||||
username: username,
|
username: username,
|
||||||
permission: permission,
|
permission: permission,
|
||||||
},
|
},
|
||||||
secretKey,
|
secretKey,
|
||||||
{ expiresIn: expiredtime },
|
{ expiresIn: expiredtime },
|
||||||
);
|
);
|
||||||
return payload;
|
return payload;
|
||||||
};
|
};
|
||||||
const publishRefreshToken = (
|
const publishRefreshToken = (
|
||||||
secretKey: string,
|
secretKey: string,
|
||||||
username: string,
|
username: string,
|
||||||
expiredtime: number,
|
expiredtime: number,
|
||||||
) => {
|
) => {
|
||||||
const payload = sign(
|
const payload = sign(
|
||||||
{ username: username },
|
{ username: username },
|
||||||
secretKey,
|
secretKey,
|
||||||
{ expiresIn: expiredtime },
|
{ expiresIn: expiredtime },
|
||||||
);
|
);
|
||||||
return payload;
|
return payload;
|
||||||
};
|
};
|
||||||
function setToken(
|
function setToken(
|
||||||
ctx: Koa.Context,
|
ctx: Koa.Context,
|
||||||
token_name: string,
|
token_name: string,
|
||||||
token_payload: string | null,
|
token_payload: string | null,
|
||||||
expiredtime: number,
|
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, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: setting.secure,
|
secure: setting.secure,
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
expires: new Date(Date.now() + expiredtime * 1000),
|
expires: new Date(Date.now() + expiredtime * 1000),
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
export const createLoginMiddleware = (userController: UserAccessor) =>
|
export const createLoginMiddleware = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => {
|
||||||
async (ctx: Koa.Context, _next: Koa.Next) => {
|
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
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(
|
return sendError(
|
||||||
400,
|
400,
|
||||||
"invalid form : username or password is not found in query.",
|
"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(
|
return sendError(
|
||||||
400,
|
400,
|
||||||
"invalid form : username or password is not string",
|
"invalid form : username or password is not string",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//if admin login is forbidden?
|
// if admin login is forbidden?
|
||||||
if (username === "admin" && setting.forbid_remote_admin_login) {
|
if (username === "admin" && setting.forbid_remote_admin_login) {
|
||||||
return sendError(403, "forbidden remote admin login");
|
return sendError(403, "forbidden remote admin login");
|
||||||
}
|
}
|
||||||
const user = await userController.findUser(username);
|
const user = await userController.findUser(username);
|
||||||
//username not exist
|
// username not exist
|
||||||
if (user === undefined) return sendError(401, "not authorized");
|
if (user === undefined) return sendError(401, "not authorized");
|
||||||
//password not matched
|
// password not matched
|
||||||
if (!user.password.check_password(password)) {
|
if (!user.password.check_password(password)) {
|
||||||
return sendError(401, "not authorized");
|
return sendError(401, "not authorized");
|
||||||
}
|
}
|
||||||
//create token
|
// create token
|
||||||
const userPermission = await user.get_permissions();
|
const userPermission = await user.get_permissions();
|
||||||
const payload = publishAccessToken(
|
const payload = publishAccessToken(
|
||||||
secretKey,
|
secretKey,
|
||||||
user.username,
|
user.username,
|
||||||
userPermission,
|
userPermission,
|
||||||
accessExpiredTime,
|
accessExpiredTime,
|
||||||
);
|
);
|
||||||
const payload2 = publishRefreshToken(
|
const payload2 = publishRefreshToken(
|
||||||
secretKey,
|
secretKey,
|
||||||
user.username,
|
user.username,
|
||||||
refreshExpiredTime,
|
refreshExpiredTime,
|
||||||
);
|
);
|
||||||
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
||||||
setToken(ctx, refreshTokenName, payload2, refreshExpiredTime);
|
setToken(ctx, refreshTokenName, payload2, refreshExpiredTime);
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
permission: userPermission,
|
permission: userPermission,
|
||||||
accessExpired: (Math.floor(Date.now() / 1000) + accessExpiredTime),
|
accessExpired: (Math.floor(Date.now() / 1000) + accessExpiredTime),
|
||||||
};
|
};
|
||||||
console.log(`${username} logined`);
|
console.log(`${username} logined`);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
|
export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
|
||||||
const setting = get_setting()
|
|
||||||
ctx.cookies.set(accessTokenName, null);
|
|
||||||
ctx.cookies.set(refreshTokenName, null);
|
|
||||||
ctx.body = {
|
|
||||||
ok: true,
|
|
||||||
username: "",
|
|
||||||
permission: setting.guest
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
export const createUserMiddleWare = (userController: UserAccessor) =>
|
|
||||||
async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
|
|
||||||
const refreshToken = refreshTokenHandler(userController);
|
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
const setGuest = async () => {
|
ctx.cookies.set(accessTokenName, null);
|
||||||
setToken(ctx, accessTokenName, null, 0);
|
ctx.cookies.set(refreshTokenName, null);
|
||||||
setToken(ctx, refreshTokenName, null, 0);
|
ctx.body = {
|
||||||
ctx.state["user"] = { username: "", permission: setting.guest };
|
ok: true,
|
||||||
return await next();
|
username: "",
|
||||||
|
permission: setting.guest,
|
||||||
};
|
};
|
||||||
return await refreshToken(ctx, setGuest, next);
|
return;
|
||||||
};
|
};
|
||||||
const refreshTokenHandler = (cntr: UserAccessor) =>
|
export const createUserMiddleWare =
|
||||||
async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
|
(userController: UserAccessor) => async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
|
||||||
|
const refreshToken = refreshTokenHandler(userController);
|
||||||
|
const setting = get_setting();
|
||||||
|
const setGuest = async () => {
|
||||||
|
setToken(ctx, accessTokenName, null, 0);
|
||||||
|
setToken(ctx, refreshTokenName, null, 0);
|
||||||
|
ctx.state["user"] = { username: "", permission: setting.guest };
|
||||||
|
return await next();
|
||||||
|
};
|
||||||
|
return await refreshToken(ctx, setGuest, next);
|
||||||
|
};
|
||||||
|
const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
|
||||||
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 {
|
||||||
const o = verify(accessPayload, secretKey);
|
const o = verify(accessPayload, secretKey);
|
||||||
if (isUserState(o)) {
|
if (isUserState(o)) {
|
||||||
ctx.state.user = o;
|
ctx.state.user = o;
|
||||||
return await next();
|
return await next();
|
||||||
} else {
|
} else {
|
||||||
console.error("invalid token detected");
|
|
||||||
throw new Error("token form invalid");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof TokenExpiredError) {
|
|
||||||
return await checkRefreshAndUpdate();
|
|
||||||
} else 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)) {
|
|
||||||
const user = await cntr.findUser(o.username);
|
|
||||||
if (user === undefined) return await fail(); //already non-existence user
|
|
||||||
const perm = await user.get_permissions();
|
|
||||||
const payload = publishAccessToken(
|
|
||||||
secretKey,
|
|
||||||
user.username,
|
|
||||||
perm,
|
|
||||||
accessExpiredTime,
|
|
||||||
);
|
|
||||||
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
|
||||||
ctx.state.user = { username: o.username, permission: perm };
|
|
||||||
} 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) {
|
|
||||||
if (e instanceof TokenExpiredError) { // refresh token is expired.
|
|
||||||
return await fail();
|
|
||||||
} else throw e;
|
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
return await next();
|
if (e instanceof TokenExpiredError) {
|
||||||
};
|
return await checkRefreshAndUpdate();
|
||||||
};
|
} else throw e;
|
||||||
export const createRefreshTokenMiddleware = (cntr: UserAccessor) =>
|
}
|
||||||
async (ctx: Koa.Context, next: Koa.Next) => {
|
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)) {
|
||||||
|
const user = await cntr.findUser(o.username);
|
||||||
|
if (user === undefined) return await fail(); // already non-existence user
|
||||||
|
const perm = await user.get_permissions();
|
||||||
|
const payload = publishAccessToken(
|
||||||
|
secretKey,
|
||||||
|
user.username,
|
||||||
|
perm,
|
||||||
|
accessExpiredTime,
|
||||||
|
);
|
||||||
|
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
||||||
|
ctx.state.user = { username: o.username, permission: perm };
|
||||||
|
} else {
|
||||||
|
console.error("invalid token detected");
|
||||||
|
throw new Error("token form invalid");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof TokenExpiredError) { // refresh token is expired.
|
||||||
|
return await fail();
|
||||||
|
} else throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const createRefreshTokenMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => {
|
||||||
const handler = refreshTokenHandler(cntr);
|
const handler = refreshTokenHandler(cntr);
|
||||||
await handler(ctx, fail, success);
|
await handler(ctx, fail, success);
|
||||||
async function fail() {
|
async function fail() {
|
||||||
const user = ctx.state.user as PayloadInfo;
|
const user = ctx.state.user as PayloadInfo;
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
refresh: false,
|
refresh: false,
|
||||||
...user,
|
...user,
|
||||||
};
|
};
|
||||||
ctx.type = "json";
|
ctx.type = "json";
|
||||||
};
|
|
||||||
async function success() {
|
|
||||||
const user = ctx.state.user as PayloadInfo;
|
|
||||||
ctx.body = {
|
|
||||||
...user,
|
|
||||||
refresh: true,
|
|
||||||
refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
|
|
||||||
};
|
|
||||||
ctx.type = "json";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export const resetPasswordMiddleware = (cntr: UserAccessor) =>
|
|
||||||
async (ctx: Koa.Context, next: Koa.Next) => {
|
|
||||||
const body = ctx.request.body;
|
|
||||||
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'];
|
async function success() {
|
||||||
const oldpw = body['oldpassword'];
|
const user = ctx.state.user as PayloadInfo;
|
||||||
const newpw = body['newpassword'];
|
ctx.body = {
|
||||||
|
...user,
|
||||||
|
refresh: true,
|
||||||
|
refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
|
||||||
|
};
|
||||||
|
ctx.type = "json";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => {
|
||||||
|
const body = ctx.request.body;
|
||||||
|
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"];
|
||||||
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");
|
||||||
}
|
}
|
||||||
const user = await cntr.findUser(username);
|
const user = await cntr.findUser(username);
|
||||||
if (user === undefined) {
|
if (user === undefined) {
|
||||||
return sendError(403, "not authorized");
|
return sendError(403, "not authorized");
|
||||||
}
|
}
|
||||||
if (!user.password.check_password(oldpw)) {
|
if (!user.password.check_password(oldpw)) {
|
||||||
return sendError(403, "not authorized");
|
return sendError(403, "not authorized");
|
||||||
}
|
}
|
||||||
user.reset_password(newpw);
|
user.reset_password(newpw);
|
||||||
ctx.body = { ok: true }
|
ctx.body = { ok: true };
|
||||||
ctx.type = 'json';
|
ctx.type = "json";
|
||||||
}
|
};
|
||||||
|
|
||||||
export function createLoginRouter(userController: UserAccessor) {
|
export function createLoginRouter(userController: UserAccessor) {
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
router.post('/login', createLoginMiddleware(userController));
|
router.post("/login", createLoginMiddleware(userController));
|
||||||
router.post('/logout', LogoutMiddleware);
|
router.post("/logout", LogoutMiddleware);
|
||||||
router.post('/refresh', createRefreshTokenMiddleware(userController));
|
router.post("/refresh", createRefreshTokenMiddleware(userController));
|
||||||
router.post('/reset', resetPasswordMiddleware(userController));
|
router.post("/reset", resetPasswordMiddleware(userController));
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAdmin = async (cntr: UserAccessor) => {
|
export const getAdmin = async (cntr: UserAccessor) => {
|
||||||
const admin = await cntr.findUser("admin");
|
const admin = await cntr.findUser("admin");
|
||||||
if (admin === undefined) {
|
if (admin === undefined) {
|
||||||
throw new Error("initial process failed!"); //???
|
throw new Error("initial process failed!"); // ???
|
||||||
}
|
}
|
||||||
return admin;
|
return admin;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAdminFirst = (admin: IUser) => {
|
export const isAdminFirst = (admin: IUser) => {
|
||||||
return admin.password.hash === "unchecked" &&
|
return admin.password.hash === "unchecked"
|
||||||
admin.password.salt === "unchecked";
|
&& admin.password.salt === "unchecked";
|
||||||
};
|
};
|
||||||
|
110
src/model/doc.ts
110
src/model/doc.ts
@ -1,129 +1,129 @@
|
|||||||
import {TagAccessor} from './tag';
|
import { JSONMap } from "../types/json";
|
||||||
import {check_type} from '../util/type_check'
|
import { check_type } from "../util/type_check";
|
||||||
import {JSONMap} from '../types/json';
|
import { TagAccessor } from "./tag";
|
||||||
|
|
||||||
export interface DocumentBody{
|
export interface DocumentBody {
|
||||||
title : string,
|
title: string;
|
||||||
content_type : string,
|
content_type: string;
|
||||||
basepath : string,
|
basepath: string;
|
||||||
filename : string,
|
filename: string;
|
||||||
modified_at : number,
|
modified_at: number;
|
||||||
content_hash : string,
|
content_hash: string;
|
||||||
additional : JSONMap,
|
additional: JSONMap;
|
||||||
tags : string[],//eager loading
|
tags: string[]; // eager loading
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetaContentBody = {
|
export const MetaContentBody = {
|
||||||
title : "string",
|
title: "string",
|
||||||
content_type : "string",
|
content_type: "string",
|
||||||
basepath : "string",
|
basepath: "string",
|
||||||
filename : "string",
|
filename: "string",
|
||||||
content_hash : "string",
|
content_hash: "string",
|
||||||
additional : "object",
|
additional: "object",
|
||||||
tags : "string[]",
|
tags: "string[]",
|
||||||
}
|
|
||||||
|
|
||||||
export const isDocBody = (c : any):c is DocumentBody =>{
|
|
||||||
return check_type<DocumentBody>(c,MetaContentBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Document extends DocumentBody{
|
|
||||||
readonly id: number;
|
|
||||||
readonly created_at:number;
|
|
||||||
readonly deleted_at:number|null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isDoc = (c: any):c is Document =>{
|
export const isDocBody = (c: any): c is DocumentBody => {
|
||||||
if('id' in c && typeof c['id'] === "number"){
|
return check_type<DocumentBody>(c, MetaContentBody);
|
||||||
const {id, ...rest} = c;
|
};
|
||||||
|
|
||||||
|
export interface Document extends DocumentBody {
|
||||||
|
readonly id: number;
|
||||||
|
readonly created_at: number;
|
||||||
|
readonly deleted_at: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isDoc = (c: any): c is Document => {
|
||||||
|
if ("id" in c && typeof c["id"] === "number") {
|
||||||
|
const { id, ...rest } = c;
|
||||||
return isDocBody(rest);
|
return isDocBody(rest);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type QueryListOption = {
|
export type QueryListOption = {
|
||||||
/**
|
/**
|
||||||
* search word
|
* search word
|
||||||
*/
|
*/
|
||||||
word?:string,
|
word?: string;
|
||||||
allow_tag?:string[],
|
allow_tag?: string[];
|
||||||
/**
|
/**
|
||||||
* limit of list
|
* limit of list
|
||||||
* @default 20
|
* @default 20
|
||||||
*/
|
*/
|
||||||
limit?:number,
|
limit?: number;
|
||||||
/**
|
/**
|
||||||
* use offset if true, otherwise
|
* use offset if true, otherwise
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
use_offset?:boolean,
|
use_offset?: boolean;
|
||||||
/**
|
/**
|
||||||
* cursor of documents
|
* cursor of documents
|
||||||
*/
|
*/
|
||||||
cursor?:number,
|
cursor?: number;
|
||||||
/**
|
/**
|
||||||
* offset of documents
|
* offset of documents
|
||||||
*/
|
*/
|
||||||
offset?:number,
|
offset?: number;
|
||||||
/**
|
/**
|
||||||
* tag eager loading
|
* tag eager loading
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
eager_loading?:boolean,
|
eager_loading?: boolean;
|
||||||
/**
|
/**
|
||||||
* content type
|
* content type
|
||||||
*/
|
*/
|
||||||
content_type?:string
|
content_type?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface DocumentAccessor{
|
export interface DocumentAccessor {
|
||||||
/**
|
/**
|
||||||
* find list by option
|
* find list by option
|
||||||
* @returns documents list
|
* @returns documents list
|
||||||
*/
|
*/
|
||||||
findList: (option?:QueryListOption)=>Promise<Document[]>,
|
findList: (option?: QueryListOption) => Promise<Document[]>;
|
||||||
/**
|
/**
|
||||||
* @returns document if exist, otherwise undefined
|
* @returns document if exist, otherwise undefined
|
||||||
*/
|
*/
|
||||||
findById: (id:number,tagload?:boolean)=> Promise<Document| undefined>,
|
findById: (id: number, tagload?: boolean) => Promise<Document | undefined>;
|
||||||
/**
|
/**
|
||||||
* find by base path and filename.
|
* find by base path and filename.
|
||||||
* if you call this function with filename, its return array length is 0 or 1.
|
* if you call this function with filename, its return array length is 0 or 1.
|
||||||
*/
|
*/
|
||||||
findByPath:(basepath: string,filename?:string)=>Promise<Document[]>;
|
findByPath: (basepath: string, filename?: string) => Promise<Document[]>;
|
||||||
/**
|
/**
|
||||||
* find deleted content
|
* find deleted content
|
||||||
*/
|
*/
|
||||||
findDeleted:(content_type:string)=>Promise<Document[]>;
|
findDeleted: (content_type: string) => Promise<Document[]>;
|
||||||
/**
|
/**
|
||||||
* search by in document
|
* search by in document
|
||||||
*/
|
*/
|
||||||
search:(search_word:string)=>Promise<Document[]>
|
search: (search_word: string) => Promise<Document[]>;
|
||||||
/**
|
/**
|
||||||
* update document except tag.
|
* update document except tag.
|
||||||
*/
|
*/
|
||||||
update:(c:Partial<Document> & { id:number })=>Promise<boolean>;
|
update: (c: Partial<Document> & { id: number }) => Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* add document
|
* add document
|
||||||
*/
|
*/
|
||||||
add:(c:DocumentBody)=>Promise<number>;
|
add: (c: DocumentBody) => Promise<number>;
|
||||||
/**
|
/**
|
||||||
* add document list
|
* add document list
|
||||||
*/
|
*/
|
||||||
addList:(content_list:DocumentBody[]) => Promise<number[]>;
|
addList: (content_list: DocumentBody[]) => Promise<number[]>;
|
||||||
/**
|
/**
|
||||||
* delete document
|
* delete document
|
||||||
* @returns if it exists, return true.
|
* @returns if it exists, return true.
|
||||||
*/
|
*/
|
||||||
del:(id:number)=>Promise<boolean>;
|
del: (id: number) => Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* @param c Valid Document
|
* @param c Valid Document
|
||||||
* @param tagname tag name to add
|
* @param tagname tag name to add
|
||||||
* @returns if success, return true
|
* @returns if success, return true
|
||||||
*/
|
*/
|
||||||
addTag:(c:Document,tag_name:string)=>Promise<boolean>;
|
addTag: (c: Document, tag_name: string) => Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* @returns if success, return true
|
* @returns if success, return true
|
||||||
*/
|
*/
|
||||||
delTag:(c:Document,tag_name:string)=>Promise<boolean>;
|
delTag: (c: Document, tag_name: string) => Promise<boolean>;
|
||||||
};
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export * from './doc';
|
export * from "./doc";
|
||||||
export * from './tag';
|
export * from "./tag";
|
||||||
export * from './user';
|
export * from "./user";
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
export interface Tag{
|
export interface Tag {
|
||||||
readonly name: string,
|
readonly name: string;
|
||||||
description?: string
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagCount{
|
export interface TagCount {
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
occurs: number;
|
occurs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagAccessor{
|
export interface TagAccessor {
|
||||||
getAllTagList: (onlyname?:boolean)=> Promise<Tag[]>;
|
getAllTagList: (onlyname?: boolean) => 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>;
|
||||||
delTag: (name:string) => Promise<boolean>;
|
delTag: (name: string) => Promise<boolean>;
|
||||||
updateTag: (name:string,tag:string) => Promise<boolean>;
|
updateTag: (name: string, tag: string) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -1,80 +1,84 @@
|
|||||||
import { createHmac, randomBytes } from 'crypto';
|
import { createHmac, randomBytes } from "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");
|
||||||
}
|
}
|
||||||
function createPasswordHashAndSalt(password: string):{salt:string,hash:string}{
|
function createPasswordHashAndSalt(password: string): { salt: string; hash: string } {
|
||||||
const secret = randomBytes(32).toString('hex');
|
const secret = randomBytes(32).toString("hex");
|
||||||
return {
|
return {
|
||||||
salt: secret,
|
salt: secret,
|
||||||
hash: hashForPassword(secret,password)
|
hash: hashForPassword(secret, password),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Password{
|
export class Password {
|
||||||
private _salt:string;
|
private _salt: string;
|
||||||
private _hash:string;
|
private _hash: string;
|
||||||
constructor(pw : string|{salt:string,hash:string}){
|
constructor(pw: string | { salt: string; hash: string }) {
|
||||||
const {salt,hash} = typeof pw === "string" ? createPasswordHashAndSalt(pw) : pw;
|
const { salt, hash } = typeof pw === "string" ? createPasswordHashAndSalt(pw) : pw;
|
||||||
this._hash = hash;
|
this._hash = hash;
|
||||||
this._salt = salt;
|
this._salt = salt;
|
||||||
}
|
}
|
||||||
set_password(password: string){
|
set_password(password: string) {
|
||||||
const {salt,hash} = createPasswordHashAndSalt(password);
|
const { salt, hash } = createPasswordHashAndSalt(password);
|
||||||
this._hash = hash;
|
this._hash = hash;
|
||||||
this._salt = salt;
|
this._salt = salt;
|
||||||
}
|
}
|
||||||
check_password(password: string):boolean{
|
check_password(password: string): boolean {
|
||||||
return this._hash === hashForPassword(this._salt,password);
|
return this._hash === hashForPassword(this._salt, password);
|
||||||
|
}
|
||||||
|
get salt() {
|
||||||
|
return this._salt;
|
||||||
|
}
|
||||||
|
get hash() {
|
||||||
|
return this._hash;
|
||||||
}
|
}
|
||||||
get salt(){return this._salt;}
|
|
||||||
get hash(){return this._hash;}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserCreateInput{
|
export interface UserCreateInput {
|
||||||
username: string,
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUser{
|
export interface IUser {
|
||||||
readonly username : string;
|
readonly username: string;
|
||||||
readonly password : Password;
|
readonly password: Password;
|
||||||
/**
|
/**
|
||||||
* return user's permission list.
|
* return user's permission list.
|
||||||
*/
|
*/
|
||||||
get_permissions():Promise<string[]>;
|
get_permissions(): Promise<string[]>;
|
||||||
/**
|
/**
|
||||||
* add permission
|
* add permission
|
||||||
* @param name permission name to add
|
* @param name permission name to add
|
||||||
* @returns if `name` doesn't exist, return true
|
* @returns if `name` doesn't exist, return true
|
||||||
*/
|
*/
|
||||||
add(name :string):Promise<boolean>;
|
add(name: string): Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* remove permission
|
* remove permission
|
||||||
* @param name permission name to remove
|
* @param name permission name to remove
|
||||||
* @returns if `name` exist, return true
|
* @returns if `name` exist, return true
|
||||||
*/
|
*/
|
||||||
remove(name :string):Promise<boolean>;
|
remove(name: string): Promise<boolean>;
|
||||||
/**
|
/**
|
||||||
* reset password.
|
* reset password.
|
||||||
* @param password password to set
|
* @param password password to set
|
||||||
*/
|
*/
|
||||||
reset_password(password: string):Promise<void>;
|
reset_password(password: string): Promise<void>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface UserAccessor{
|
export interface UserAccessor {
|
||||||
/**
|
/**
|
||||||
* create user
|
* create user
|
||||||
* @returns if user exist, return undefined
|
* @returns if user exist, return undefined
|
||||||
*/
|
*/
|
||||||
createUser: (input :UserCreateInput)=> Promise<IUser|undefined>,
|
createUser: (input: UserCreateInput) => Promise<IUser | undefined>;
|
||||||
/**
|
/**
|
||||||
* find user
|
* find user
|
||||||
*/
|
*/
|
||||||
findUser: (username: string)=> Promise<IUser|undefined>,
|
findUser: (username: string) => Promise<IUser | undefined>;
|
||||||
/**
|
/**
|
||||||
* remove user
|
* remove user
|
||||||
* @returns if user exist, true
|
* @returns if user exist, true
|
||||||
*/
|
*/
|
||||||
delUser: (username: string)=>Promise<boolean>
|
delUser: (username: string) => Promise<boolean>;
|
||||||
};
|
}
|
||||||
|
@ -1,60 +1,58 @@
|
|||||||
import Koa from 'koa';
|
import Koa from "koa";
|
||||||
import { UserState } from '../login';
|
import { UserState } from "../login";
|
||||||
import { sendError } from '../route/error_handler';
|
import { sendError } from "../route/error_handler";
|
||||||
|
|
||||||
|
export enum Permission {
|
||||||
export enum Permission{
|
// ========
|
||||||
//========
|
// not implemented
|
||||||
//not implemented
|
// admin only
|
||||||
//admin only
|
|
||||||
/** remove document */
|
/** remove document */
|
||||||
//removeContent = 'removeContent',
|
// removeContent = 'removeContent',
|
||||||
|
|
||||||
/** upload document */
|
/** upload document */
|
||||||
//uploadContent = 'uploadContent',
|
// uploadContent = 'uploadContent',
|
||||||
|
|
||||||
/** modify document except base path, filename, content_hash. but admin can modify all. */
|
/** modify document except base path, filename, content_hash. but admin can modify all. */
|
||||||
//modifyContent = 'modifyContent',
|
// modifyContent = 'modifyContent',
|
||||||
|
|
||||||
/** add tag into document */
|
/** add tag into document */
|
||||||
//addTagContent = 'addTagContent',
|
// addTagContent = 'addTagContent',
|
||||||
/** remove tag from document */
|
/** remove tag from document */
|
||||||
//removeTagContent = 'removeTagContent',
|
// removeTagContent = 'removeTagContent',
|
||||||
/** ModifyTagInDoc */
|
/** ModifyTagInDoc */
|
||||||
ModifyTag = 'ModifyTag',
|
ModifyTag = "ModifyTag",
|
||||||
|
|
||||||
/** find documents with query */
|
/** find documents with query */
|
||||||
//findAllContent = 'findAllContent',
|
// findAllContent = 'findAllContent',
|
||||||
/** find one document. */
|
/** find one document. */
|
||||||
//findOneContent = 'findOneContent',
|
// findOneContent = 'findOneContent',
|
||||||
/** view content*/
|
/** view content*/
|
||||||
//viewContent = 'viewContent',
|
// viewContent = 'viewContent',
|
||||||
QueryContent = 'QueryContent',
|
QueryContent = "QueryContent",
|
||||||
|
|
||||||
/** modify description about the one tag. */
|
/** modify description about the one tag. */
|
||||||
modifyTagDesc = 'ModifyTagDesc',
|
modifyTagDesc = "ModifyTagDesc",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createPermissionCheckMiddleware = (...permissions:string[]) =>
|
export const createPermissionCheckMiddleware =
|
||||||
async (ctx: Koa.ParameterizedContext<UserState>,next:Koa.Next) => {
|
(...permissions: string[]) => 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();
|
||||||
}
|
|
||||||
const user_permission = user.permission;
|
|
||||||
//if permissions is not subset of user permission
|
|
||||||
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");
|
const user_permission = user.permission;
|
||||||
|
// if permissions is not subset of user permission
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
|
||||||
|
const user = ctx.state["user"];
|
||||||
|
if (user.username !== "admin") {
|
||||||
|
return sendError(403, "admin only");
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
}
|
};
|
||||||
export const AdminOnlyMiddleware = async (ctx: Koa.ParameterizedContext<UserState>,next:Koa.Next)=>{
|
|
||||||
const user = ctx.state['user'];
|
|
||||||
if(user.username !== "admin"){
|
|
||||||
return sendError(403,"admin only");
|
|
||||||
}
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
|
@ -1,55 +1,57 @@
|
|||||||
import { DefaultContext, Middleware, Next, ParameterizedContext } from 'koa';
|
import { DefaultContext, Middleware, Next, ParameterizedContext } from "koa";
|
||||||
import compose from 'koa-compose';
|
import compose from "koa-compose";
|
||||||
import Router, { IParamMiddleware } from 'koa-router';
|
import Router, { IParamMiddleware } from "koa-router";
|
||||||
import { ContentContext } from './context';
|
import ComicRouter from "./comic";
|
||||||
import ComicRouter from './comic';
|
import { ContentContext } from "./context";
|
||||||
import VideoRouter from './video';
|
import VideoRouter from "./video";
|
||||||
|
|
||||||
const table:{[s:string]:Router|undefined} = {
|
const table: { [s: string]: Router | undefined } = {
|
||||||
"comic": new ComicRouter,
|
"comic": new ComicRouter(),
|
||||||
"video": new VideoRouter
|
"video": new VideoRouter(),
|
||||||
}
|
};
|
||||||
const all_middleware = (cont: string|undefined, restarg: string|undefined)=>async (ctx:ParameterizedContext<ContentContext,DefaultContext>,next:Next)=>{
|
const all_middleware =
|
||||||
if(cont == undefined){
|
(cont: string | undefined, restarg: string | undefined) =>
|
||||||
ctx.status = 404;
|
async (ctx: ParameterizedContext<ContentContext, DefaultContext>, next: Next) => {
|
||||||
return;
|
if (cont == undefined) {
|
||||||
}
|
ctx.status = 404;
|
||||||
if(ctx.state.location.type != cont){
|
return;
|
||||||
console.error("not matched")
|
}
|
||||||
ctx.status = 404;
|
if (ctx.state.location.type != cont) {
|
||||||
return;
|
console.error("not matched");
|
||||||
}
|
ctx.status = 404;
|
||||||
const router = table[cont];
|
return;
|
||||||
if(router == undefined){
|
}
|
||||||
ctx.status = 404;
|
const router = table[cont];
|
||||||
return;
|
if (router == undefined) {
|
||||||
}
|
ctx.status = 404;
|
||||||
const rest = "/"+(restarg ?? "");
|
return;
|
||||||
const result = router.match(rest,"GET");
|
}
|
||||||
if(!result.route){
|
const rest = "/" + (restarg ?? "");
|
||||||
return await next();
|
const result = router.match(rest, "GET");
|
||||||
}
|
if (!result.route) {
|
||||||
const chain = result.pathAndMethod.reduce((combination : Middleware<any& DefaultContext,any>[],cur)=>{
|
|
||||||
combination.push(async (ctx,next)=>{
|
|
||||||
const captures = cur.captures(rest);
|
|
||||||
ctx.params = cur.params(rest,captures);
|
|
||||||
ctx.request.params = ctx.params;
|
|
||||||
ctx.routerPath = cur.path;
|
|
||||||
return await next();
|
return await next();
|
||||||
});
|
}
|
||||||
return combination.concat(cur.stack);
|
const chain = result.pathAndMethod.reduce((combination: Middleware<any & DefaultContext, any>[], cur) => {
|
||||||
},[]);
|
combination.push(async (ctx, next) => {
|
||||||
return await compose(chain)(ctx,next);
|
const captures = cur.captures(rest);
|
||||||
};
|
ctx.params = cur.params(rest, captures);
|
||||||
export class AllContentRouter extends Router<ContentContext>{
|
ctx.request.params = ctx.params;
|
||||||
constructor(){
|
ctx.routerPath = cur.path;
|
||||||
|
return await next();
|
||||||
|
});
|
||||||
|
return combination.concat(cur.stack);
|
||||||
|
}, []);
|
||||||
|
return await compose(chain)(ctx, next);
|
||||||
|
};
|
||||||
|
export class AllContentRouter extends Router<ContentContext> {
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { Context, DefaultContext, DefaultState, Next } from "koa";
|
import { Context, DefaultContext, DefaultState, Next } from "koa";
|
||||||
import {
|
|
||||||
createReadableStreamFromZip,
|
|
||||||
entriesByNaturalOrder,
|
|
||||||
readZip,
|
|
||||||
ZipAsync,
|
|
||||||
} from "../util/zipwrap";
|
|
||||||
import { since_last_modified } from "./util";
|
|
||||||
import { ContentContext } from "./context";
|
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
|
import { createReadableStreamFromZip, entriesByNaturalOrder, readZip, ZipAsync } from "../util/zipwrap";
|
||||||
|
import { ContentContext } from "./context";
|
||||||
|
import { since_last_modified } from "./util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* zip stream cache.
|
* zip stream cache.
|
||||||
@ -16,88 +11,86 @@ import Router from "koa-router";
|
|||||||
let ZipStreamCache: { [path: string]: [ZipAsync, number] } = {};
|
let ZipStreamCache: { [path: string]: [ZipAsync, number] } = {};
|
||||||
|
|
||||||
async function acquireZip(path: string) {
|
async function acquireZip(path: string) {
|
||||||
if (!(path in ZipStreamCache)) {
|
if (!(path in ZipStreamCache)) {
|
||||||
const ret = await readZip(path);
|
const ret = await readZip(path);
|
||||||
ZipStreamCache[path] = [ret, 1];
|
ZipStreamCache[path] = [ret, 1];
|
||||||
//console.log(`acquire ${path} 1`);
|
// console.log(`acquire ${path} 1`);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
} else {
|
||||||
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) {
|
||||||
const obj = ZipStreamCache[path];
|
const obj = ZipStreamCache[path];
|
||||||
if (obj === undefined) throw new Error("error! key invalid");
|
if (obj === undefined) throw new Error("error! key invalid");
|
||||||
const [ref, refCount] = obj;
|
const [ref, refCount] = obj;
|
||||||
//console.log(`release ${path} : ${refCount}`);
|
// console.log(`release ${path} : ${refCount}`);
|
||||||
if (refCount === 1) {
|
if (refCount === 1) {
|
||||||
ref.close();
|
ref.close();
|
||||||
delete ZipStreamCache[path];
|
delete ZipStreamCache[path];
|
||||||
}
|
} else {
|
||||||
else{
|
ZipStreamCache[path] = [ref, refCount - 1];
|
||||||
ZipStreamCache[path] = [ref, refCount - 1];
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
let 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);
|
||||||
});
|
|
||||||
if (0 <= page && page < entries.length) {
|
|
||||||
const entry = entries[page];
|
|
||||||
const last_modified = new Date(entry.time);
|
|
||||||
if (since_last_modified(ctx, last_modified)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const read_stream = (await createReadableStreamFromZip(zip, entry));
|
|
||||||
/**Exceptions (ECONNRESET, ECONNABORTED) may be thrown when processing this request
|
|
||||||
* for reasons such as when the browser unexpectedly closes the connection.
|
|
||||||
* Once such an exception is raised, the stream is not properly destroyed,
|
|
||||||
* so there is a problem with the zlib stream being accessed even after the stream is closed.
|
|
||||||
* So it waits for 100 ms and releases it.
|
|
||||||
* Additionaly, there is a risk of memory leak becuase zlib stream is not properly destroyed.
|
|
||||||
* @todo modify function 'stream' in 'node-stream-zip' library to prevent memory leak*/
|
|
||||||
read_stream.once("close", () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
releaseZip(path);
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
|
if (0 <= page && page < entries.length) {
|
||||||
|
const entry = entries[page];
|
||||||
|
const last_modified = new Date(entry.time);
|
||||||
|
if (since_last_modified(ctx, last_modified)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const read_stream = await createReadableStreamFromZip(zip, entry);
|
||||||
|
/** Exceptions (ECONNRESET, ECONNABORTED) may be thrown when processing this request
|
||||||
|
* for reasons such as when the browser unexpectedly closes the connection.
|
||||||
|
* Once such an exception is raised, the stream is not properly destroyed,
|
||||||
|
* so there is a problem with the zlib stream being accessed even after the stream is closed.
|
||||||
|
* So it waits for 100 ms and releases it.
|
||||||
|
* Additionaly, there is a risk of memory leak becuase zlib stream is not properly destroyed.
|
||||||
|
* @todo modify function 'stream' in 'node-stream-zip' library to prevent memory leak */
|
||||||
|
read_stream.once("close", () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
releaseZip(path);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
ctx.body = read_stream;
|
ctx.body = read_stream;
|
||||||
ctx.response.length = entry.size;
|
ctx.response.length = entry.size;
|
||||||
//console.log(`${entry.name}'s ${page}:${entry.size}`);
|
// console.log(`${entry.name}'s ${page}:${entry.size}`);
|
||||||
ctx.response.type = entry.name.split(".").pop() as string;
|
ctx.response.type = entry.name.split(".").pop() as string;
|
||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
ctx.set("Date", new Date().toUTCString());
|
ctx.set("Date", new Date().toUTCString());
|
||||||
ctx.set("Last-Modified", last_modified.toUTCString());
|
ctx.set("Last-Modified", last_modified.toUTCString());
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComicRouter extends Router<ContentContext> {
|
export class ComicRouter extends Router<ContentContext> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.get("/", async (ctx, next) => {
|
this.get("/", async (ctx, next) => {
|
||||||
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) => {
|
||||||
await renderZipImage(ctx, ctx.state.location.path, 0);
|
await renderZipImage(ctx, ctx.state.location.path, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ComicRouter;
|
export default ComicRouter;
|
||||||
|
@ -1,63 +1,68 @@
|
|||||||
import { Context, Next } from 'koa';
|
import { Context, Next } from "koa";
|
||||||
import Router from 'koa-router';
|
import Router from "koa-router";
|
||||||
import {Document, DocumentAccessor, isDocBody} from '../model/doc';
|
import { join } from "path";
|
||||||
import {QueryListOption} from '../model/doc';
|
import { Document, DocumentAccessor, isDocBody } from "../model/doc";
|
||||||
import {ParseQueryNumber, ParseQueryArray, ParseQueryBoolean, ParseQueryArgString} from './util'
|
import { QueryListOption } from "../model/doc";
|
||||||
import {sendError} from './error_handler';
|
import {
|
||||||
import { join } from 'path';
|
AdminOnlyMiddleware as AdminOnly,
|
||||||
import {AllContentRouter} from './all';
|
createPermissionCheckMiddleware as PerCheck,
|
||||||
import {createPermissionCheckMiddleware as PerCheck, Permission as Per, AdminOnlyMiddleware as AdminOnly} from '../permission/permission';
|
Permission as Per,
|
||||||
import {ContentLocation} from './context'
|
} from "../permission/permission";
|
||||||
|
import { AllContentRouter } from "./all";
|
||||||
|
import { 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 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);
|
let 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;
|
||||||
ctx.type = 'json';
|
ctx.type = "json";
|
||||||
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);
|
let 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"];
|
||||||
let query_limit = (ctx.query['limit']);
|
let query_cursor = ctx.query["cursor"];
|
||||||
let query_cursor = (ctx.query['cursor']);
|
let query_word = ctx.query["word"];
|
||||||
let query_word = (ctx.query['word']);
|
let query_content_type = ctx.query["content_type"];
|
||||||
let query_content_type = (ctx.query['content_type']);
|
let query_offset = ctx.query["offset"];
|
||||||
let query_offset = (ctx.query['offset']);
|
let query_use_offset = ctx.query["use_offset"];
|
||||||
let query_use_offset = ctx.query['use_offset'];
|
if (
|
||||||
if(query_limit instanceof Array
|
query_limit instanceof Array
|
||||||
|| query_cursor instanceof Array
|
|| query_cursor instanceof Array
|
||||||
|| query_word instanceof Array
|
|| query_word instanceof Array
|
||||||
|| query_content_type instanceof Array
|
|| query_content_type instanceof Array
|
||||||
|| query_offset instanceof Array
|
|| query_offset instanceof Array
|
||||||
|| query_use_offset instanceof Array){
|
|| query_use_offset instanceof Array
|
||||||
return sendError(400,"paramter can not be array");
|
) {
|
||||||
|
return sendError(400, "paramter can not be array");
|
||||||
}
|
}
|
||||||
const limit = ParseQueryNumber(query_limit);
|
const limit = ParseQueryNumber(query_limit);
|
||||||
const cursor = ParseQueryNumber(query_cursor);
|
const cursor = ParseQueryNumber(query_cursor);
|
||||||
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 (limit === NaN || cursor === NaN || offset === NaN) {
|
||||||
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.");
|
||||||
}
|
}
|
||||||
const option :QueryListOption = {
|
const option: QueryListOption = {
|
||||||
limit: limit,
|
limit: limit,
|
||||||
allow_tag: allow_tag,
|
allow_tag: allow_tag,
|
||||||
word: word,
|
word: word,
|
||||||
@ -69,93 +74,94 @@ const ContentQueryHandler = (controller : DocumentAccessor) => async (ctx: Conte
|
|||||||
};
|
};
|
||||||
let document = await controller.findList(option);
|
let 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.");
|
||||||
}
|
}
|
||||||
if(typeof ctx.request.body !== "object"){
|
if (typeof ctx.request.body !== "object") {
|
||||||
return sendError(400,"update fail. invalid argument: not");
|
return sendError(400, "update fail. invalid argument: not");
|
||||||
}
|
}
|
||||||
const content_desc: Partial<Document> & {id: number} = {
|
const content_desc: Partial<Document> & { id: number } = {
|
||||||
id:num,...ctx.request.body
|
id: num,
|
||||||
|
...ctx.request.body,
|
||||||
};
|
};
|
||||||
const success = await controller.update(content_desc);
|
const success = await controller.update(content_desc);
|
||||||
ctx.body = JSON.stringify(success);
|
ctx.body = JSON.stringify(success);
|
||||||
ctx.type = 'json';
|
ctx.type = "json";
|
||||||
}
|
|
||||||
|
|
||||||
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){
|
|
||||||
return sendError(400,"??? Unreachable");
|
|
||||||
}
|
|
||||||
tag_name = String(tag_name);
|
|
||||||
const c = await controller.findById(num);
|
|
||||||
if(c === undefined){
|
|
||||||
return sendError(404);
|
|
||||||
}
|
|
||||||
const r = await controller.addTag(c,tag_name);
|
|
||||||
ctx.body = JSON.stringify(r);
|
|
||||||
ctx.type = 'json';
|
|
||||||
};
|
};
|
||||||
const DelTagHandler = (controller: DocumentAccessor)=>async (ctx: Context, next: Next)=>{
|
|
||||||
let tag_name = ctx.params['tag'];
|
const AddTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
let tag_name = ctx.params["tag"];
|
||||||
if(typeof tag_name === undefined){
|
const num = Number.parseInt(ctx.params["num"]);
|
||||||
return sendError(400,"?? Unreachable");
|
if (typeof tag_name === undefined) {
|
||||||
|
return sendError(400, "??? Unreachable");
|
||||||
}
|
}
|
||||||
tag_name = String(tag_name);
|
tag_name = String(tag_name);
|
||||||
const c = await controller.findById(num);
|
const c = await controller.findById(num);
|
||||||
if(c === undefined){
|
if (c === undefined) {
|
||||||
return sendError(404);
|
return sendError(404);
|
||||||
}
|
}
|
||||||
const r = await controller.delTag(c,tag_name);
|
const r = await controller.addTag(c, tag_name);
|
||||||
ctx.body = JSON.stringify(r);
|
ctx.body = JSON.stringify(r);
|
||||||
ctx.type = 'json';
|
ctx.type = "json";
|
||||||
}
|
};
|
||||||
const DeleteContentHandler = (controller : DocumentAccessor) => async (ctx: Context, next: Next) => {
|
const DelTagHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const num = Number.parseInt(ctx.params['num']);
|
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);
|
||||||
|
const c = await controller.findById(num);
|
||||||
|
if (c === undefined) {
|
||||||
|
return sendError(404);
|
||||||
|
}
|
||||||
|
const r = await controller.delTag(c, tag_name);
|
||||||
|
ctx.body = JSON.stringify(r);
|
||||||
|
ctx.type = "json";
|
||||||
|
};
|
||||||
|
const DeleteContentHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
|
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);
|
let 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,
|
||||||
} as ContentLocation;
|
} as ContentLocation;
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContentRouter = (controller: DocumentAccessor)=>{
|
export const getContentRouter = (controller: DocumentAccessor) => {
|
||||||
const ret = new Router();
|
const ret = new Router();
|
||||||
ret.get("/search",PerCheck(Per.QueryContent),ContentQueryHandler(controller));
|
ret.get("/search", PerCheck(Per.QueryContent), ContentQueryHandler(controller));
|
||||||
ret.get("/:num(\\d+)",PerCheck(Per.QueryContent),ContentIDHandler(controller));
|
ret.get("/:num(\\d+)", PerCheck(Per.QueryContent), ContentIDHandler(controller));
|
||||||
ret.post("/:num(\\d+)",AdminOnly,UpdateContentHandler(controller));
|
ret.post("/:num(\\d+)", AdminOnly, UpdateContentHandler(controller));
|
||||||
//ret.use("/:num(\\d+)/:content_type");
|
// ret.use("/:num(\\d+)/:content_type");
|
||||||
//ret.post("/",AdminOnly,CreateContentHandler(controller));
|
// ret.post("/",AdminOnly,CreateContentHandler(controller));
|
||||||
ret.get("/:num(\\d+)/tags",PerCheck(Per.QueryContent),ContentTagIDHandler(controller));
|
ret.get("/:num(\\d+)/tags", PerCheck(Per.QueryContent), ContentTagIDHandler(controller));
|
||||||
ret.post("/:num(\\d+)/tags/:tag",PerCheck(Per.ModifyTag),AddTagHandler(controller));
|
ret.post("/:num(\\d+)/tags/:tag", PerCheck(Per.ModifyTag), AddTagHandler(controller));
|
||||||
ret.del("/:num(\\d+)/tags/:tag",PerCheck(Per.ModifyTag),DelTagHandler(controller));
|
ret.del("/:num(\\d+)/tags/:tag", PerCheck(Per.ModifyTag), DelTagHandler(controller));
|
||||||
ret.del("/:num(\\d+)",AdminOnly,DeleteContentHandler(controller));
|
ret.del("/:num(\\d+)", AdminOnly, DeleteContentHandler(controller));
|
||||||
ret.all("/:num(\\d+)/(.*)",PerCheck(Per.QueryContent),ContentHandler(controller));
|
ret.all("/:num(\\d+)/(.*)", PerCheck(Per.QueryContent), ContentHandler(controller));
|
||||||
ret.use("/:num(\\d+)",PerCheck(Per.QueryContent),(new AllContentRouter).routes());
|
ret.use("/:num(\\d+)", PerCheck(Per.QueryContent), (new AllContentRouter()).routes());
|
||||||
return ret;
|
return ret;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default getContentRouter;
|
export default getContentRouter;
|
@ -1,8 +1,8 @@
|
|||||||
export type ContentLocation = {
|
export type ContentLocation = {
|
||||||
path:string,
|
path: string;
|
||||||
type:string,
|
type: string;
|
||||||
additional:object|undefined,
|
additional: object | undefined;
|
||||||
}
|
};
|
||||||
export interface ContentContext{
|
export interface ContentContext {
|
||||||
location:ContentLocation
|
location: ContentLocation;
|
||||||
}
|
}
|
@ -1,50 +1,49 @@
|
|||||||
import {Context, Next} from 'koa';
|
import { Context, Next } from "koa";
|
||||||
|
|
||||||
export interface ErrorFormat {
|
export interface ErrorFormat {
|
||||||
code: number,
|
code: number;
|
||||||
message: string,
|
message: string;
|
||||||
detail?: string
|
detail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ClientRequestError implements Error{
|
class ClientRequestError implements Error {
|
||||||
name: string;
|
name: string;
|
||||||
message: string;
|
message: string;
|
||||||
stack?: string | undefined;
|
stack?: string | undefined;
|
||||||
code: number;
|
code: number;
|
||||||
|
|
||||||
constructor(code : number,message: string){
|
constructor(code: number, message: string) {
|
||||||
this.name = "client request error";
|
this.name = "client request error";
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.code = code;
|
this.code = code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const code_to_message_table:{[key:number]:string|undefined} = {
|
const code_to_message_table: { [key: number]: string | undefined } = {
|
||||||
400:"BadRequest",
|
400: "BadRequest",
|
||||||
404:"NotFound"
|
404: "NotFound",
|
||||||
}
|
};
|
||||||
|
|
||||||
export const error_handler = async (ctx:Context,next: Next)=>{
|
export const error_handler = async (ctx: Context, next: Next) => {
|
||||||
try {
|
try {
|
||||||
await next();
|
await next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if(err instanceof ClientRequestError){
|
if (err instanceof ClientRequestError) {
|
||||||
const body : ErrorFormat= {
|
const body: ErrorFormat = {
|
||||||
code: err.code,
|
code: err.code,
|
||||||
message: code_to_message_table[err.code] ?? "",
|
message: code_to_message_table[err.code] ?? "",
|
||||||
detail: err.message
|
detail: err.message,
|
||||||
}
|
};
|
||||||
ctx.status = err.code;
|
ctx.status = err.code;
|
||||||
ctx.body = body;
|
ctx.body = body;
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const sendError = (code:number,message?:string) =>{
|
export const sendError = (code: number, message?: string) => {
|
||||||
throw new ClientRequestError(code,message ?? "");
|
throw new ClientRequestError(code, message ?? "");
|
||||||
}
|
};
|
||||||
|
|
||||||
export default error_handler;
|
export default error_handler;
|
@ -1,32 +1,29 @@
|
|||||||
import {Context, Next} from "koa";
|
import { Context, Next } from "koa";
|
||||||
import Router,{RouterContext} from "koa-router";
|
import Router, { RouterContext } from "koa-router";
|
||||||
import { TagAccessor } from "../model/tag";
|
import { TagAccessor } from "../model/tag";
|
||||||
import { sendError } from "./error_handler";
|
|
||||||
import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission";
|
import { createPermissionCheckMiddleware as PerCheck, Permission } from "../permission/permission";
|
||||||
|
import { sendError } from "./error_handler";
|
||||||
|
|
||||||
export function getTagRounter(tagController: TagAccessor){
|
export function getTagRounter(tagController: TagAccessor) {
|
||||||
let router = new Router();
|
let router = new Router();
|
||||||
router.get("/",PerCheck(Permission.QueryContent),
|
router.get("/", PerCheck(Permission.QueryContent), async (ctx: Context) => {
|
||||||
async (ctx: Context)=>{
|
if (ctx.query["withCount"]) {
|
||||||
if(ctx.query["withCount"]){
|
const c = await tagController.getAllTagCount();
|
||||||
const c = await tagController.getAllTagCount();
|
|
||||||
ctx.body = c;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const c = await tagController.getAllTagList();
|
|
||||||
ctx.body = c;
|
|
||||||
}
|
|
||||||
ctx.type = "json";
|
|
||||||
});
|
|
||||||
router.get("/:tag_name", PerCheck(Permission.QueryContent),
|
|
||||||
async (ctx: RouterContext)=>{
|
|
||||||
const tag_name = ctx.params["tag_name"];
|
|
||||||
const c = await tagController.getTagByName(tag_name);
|
|
||||||
if (!c){
|
|
||||||
sendError(404, "tags not found");
|
|
||||||
}
|
|
||||||
ctx.body = c;
|
ctx.body = c;
|
||||||
ctx.type = "json";
|
} else {
|
||||||
});
|
const c = await tagController.getAllTagList();
|
||||||
|
ctx.body = c;
|
||||||
|
}
|
||||||
|
ctx.type = "json";
|
||||||
|
});
|
||||||
|
router.get("/:tag_name", PerCheck(Permission.QueryContent), async (ctx: RouterContext) => {
|
||||||
|
const tag_name = ctx.params["tag_name"];
|
||||||
|
const c = await tagController.getTagByName(tag_name);
|
||||||
|
if (!c) {
|
||||||
|
sendError(404, "tags not found");
|
||||||
|
}
|
||||||
|
ctx.body = c;
|
||||||
|
ctx.type = "json";
|
||||||
|
});
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
@ -1,39 +1,37 @@
|
|||||||
|
import { Context } from "koa";
|
||||||
|
|
||||||
import {Context} from 'koa';
|
export function ParseQueryNumber(s: string[] | string | undefined): number | undefined {
|
||||||
|
if (s === undefined) return undefined;
|
||||||
|
else if (typeof s === "object") return undefined;
|
||||||
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);
|
else return Number.parseInt(s);
|
||||||
}
|
}
|
||||||
export function ParseQueryArray(s: string[]|string|undefined){
|
export function ParseQueryArray(s: string[] | string | undefined) {
|
||||||
s = s ?? [];
|
s = s ?? [];
|
||||||
const r = s instanceof Array ? s : [s];
|
const r = s instanceof Array ? s : [s];
|
||||||
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) {
|
||||||
if(typeof s === "object") return undefined;
|
if (typeof s === "object") return undefined;
|
||||||
return s === undefined ? s : decodeURIComponent(s);
|
return s === undefined ? s : decodeURIComponent(s);
|
||||||
}
|
}
|
||||||
export function ParseQueryBoolean(s: string[] |string|undefined): [boolean,boolean|undefined]{
|
export function ParseQueryBoolean(s: string[] | string | undefined): [boolean, boolean | undefined] {
|
||||||
let value:boolean|undefined;
|
let value: boolean | undefined;
|
||||||
|
|
||||||
if(s === "true")
|
if (s === "true") {
|
||||||
value = true;
|
value = true;
|
||||||
else if(s === "false")
|
} else if (s === "false") {
|
||||||
value = false;
|
value = false;
|
||||||
else if(s === undefined)
|
} else if (s === undefined) {
|
||||||
value = undefined;
|
value = undefined;
|
||||||
else return [false,undefined]
|
} else return [false, undefined];
|
||||||
return [true,value]
|
return [true, value];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function since_last_modified(ctx: Context, last_modified: Date): boolean{
|
export function since_last_modified(ctx: Context, last_modified: Date): boolean {
|
||||||
const con = ctx.get("If-Modified-Since");
|
const con = ctx.get("If-Modified-Since");
|
||||||
if(con === "") return false;
|
if (con === "") return false;
|
||||||
const mdate = new Date(con);
|
const mdate = new Date(con);
|
||||||
if(last_modified > mdate) return false;
|
if (last_modified > mdate) return false;
|
||||||
ctx.status = 304;
|
ctx.status = 304;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import {Context } from 'koa';
|
import { createReadStream, promises } from "fs";
|
||||||
import {promises, createReadStream} from "fs";
|
import { Context } from "koa";
|
||||||
import {ContentContext} from './context';
|
import Router from "koa-router";
|
||||||
import Router from 'koa-router';
|
import { 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();
|
||||||
if(ext === undefined) {
|
if (ext === undefined) {
|
||||||
//ctx.status = 404;
|
// ctx.status = 404;
|
||||||
console.error(`${path}:${ext}`)
|
console.error(`${path}:${ext}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.response.type = ext;
|
ctx.response.type = ext;
|
||||||
@ -15,13 +15,13 @@ export async function renderVideo(ctx: Context,path : string){
|
|||||||
const stat = await promises.stat(path);
|
const stat = await promises.stat(path);
|
||||||
let start = 0;
|
let start = 0;
|
||||||
let end = 0;
|
let end = 0;
|
||||||
ctx.set('Last-Modified',(new Date(stat.mtime).toUTCString()));
|
ctx.set("Last-Modified", new Date(stat.mtime).toUTCString());
|
||||||
ctx.set('Date', new Date().toUTCString());
|
ctx.set("Date", new Date().toUTCString());
|
||||||
ctx.set("Accept-Ranges", "bytes");
|
ctx.set("Accept-Ranges", "bytes");
|
||||||
if(range_text === ''){
|
if (range_text === "") {
|
||||||
end = 1024*512;
|
end = 1024 * 512;
|
||||||
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;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -29,39 +29,38 @@ export async function renderVideo(ctx: Context,path : string){
|
|||||||
ctx.length = stat.size;
|
ctx.length = stat.size;
|
||||||
let stream = createReadStream(path);
|
let 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*)/);
|
||||||
if(m === null){
|
if (m === null) {
|
||||||
ctx.status = 416;
|
ctx.status = 416;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
start = parseInt(m[1]);
|
start = parseInt(m[1]);
|
||||||
end = m[2].length > 0 ? parseInt(m[2]) : start + 1024*1024;
|
end = m[2].length > 0 ? 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;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.status = 206;
|
ctx.status = 206;
|
||||||
ctx.length = end - start + 1;
|
ctx.length = end - start + 1;
|
||||||
ctx.response.set("Content-Range",`bytes ${start}-${end}/${stat.size}`);
|
ctx.response.set("Content-Range", `bytes ${start}-${end}/${stat.size}`);
|
||||||
ctx.body = createReadStream(path,{
|
ctx.body = createReadStream(path, {
|
||||||
start:start,
|
start: start,
|
||||||
end:end
|
end: end,
|
||||||
});//inclusive range.
|
}); // inclusive range.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VideoRouter extends Router<ContentContext>{
|
export class VideoRouter extends Router<ContentContext> {
|
||||||
constructor(){
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.get("/", async (ctx,next)=>{
|
this.get("/", async (ctx, next) => {
|
||||||
await renderVideo(ctx,ctx.state.location.path);
|
await renderVideo(ctx, ctx.state.location.path);
|
||||||
|
});
|
||||||
|
this.get("/thumbnail", async (ctx, next) => {
|
||||||
|
await renderVideo(ctx, ctx.state.location.path);
|
||||||
});
|
});
|
||||||
this.get("/thumbnail", async (ctx,next)=>{
|
|
||||||
await renderVideo(ctx,ctx.state.location.path);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
|
export interface PaginationOption {
|
||||||
export interface PaginationOption{
|
cursor: number;
|
||||||
cursor:number;
|
limit: number;
|
||||||
limit:number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIndexer{
|
export interface IIndexer {
|
||||||
indexDoc(word:string,doc_id:number):boolean;
|
indexDoc(word: string, doc_id: number): boolean;
|
||||||
indexDoc(word:string[],doc_id:number):boolean;
|
indexDoc(word: string[], doc_id: number): boolean;
|
||||||
|
|
||||||
getDoc(word:string,option?:PaginationOption):number[];
|
getDoc(word: string, option?: PaginationOption): number[];
|
||||||
getDoc(word:string[],option?:PaginationOption):number[];
|
getDoc(word: string[], option?: PaginationOption): number[];
|
||||||
}
|
}
|
@ -1,9 +1,8 @@
|
|||||||
|
export interface ITokenizer {
|
||||||
export interface ITokenizer{
|
tokenize(s: string): string[];
|
||||||
tokenize(s:string):string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultTokenizer implements ITokenizer{
|
export class DefaultTokenizer implements ITokenizer {
|
||||||
tokenize(s: string): string[] {
|
tokenize(s: string): string[] {
|
||||||
return s.split(" ");
|
return s.split(" ");
|
||||||
}
|
}
|
||||||
|
273
src/server.ts
273
src/server.ts
@ -1,55 +1,55 @@
|
|||||||
import Koa from 'koa';
|
import Koa from "koa";
|
||||||
import Router from 'koa-router';
|
import Router from "koa-router";
|
||||||
|
|
||||||
import {get_setting, SettingConfig} from './SettingConfig';
|
import { connectDB } from "./database";
|
||||||
import {connectDB} from './database';
|
import { createDiffRouter, DiffManager } from "./diff/mod";
|
||||||
import {DiffManager, createDiffRouter} from './diff/mod';
|
import { get_setting, SettingConfig } from "./SettingConfig";
|
||||||
|
|
||||||
import { createReadStream, readFileSync } from 'fs';
|
import { createReadStream, readFileSync } from "fs";
|
||||||
import getContentRouter from './route/contents';
|
import bodyparser from "koa-bodyparser";
|
||||||
import { createKnexDocumentAccessor, createKnexTagController, createKnexUserController } from './db/mod';
|
import { createKnexDocumentAccessor, createKnexTagController, createKnexUserController } from "./db/mod";
|
||||||
import bodyparser from 'koa-bodyparser';
|
import { createLoginRouter, createUserMiddleWare, getAdmin, isAdminFirst } from "./login";
|
||||||
import {error_handler} from './route/error_handler';
|
import getContentRouter from "./route/contents";
|
||||||
import {createUserMiddleWare, createLoginRouter, isAdminFirst, getAdmin} from './login';
|
import { error_handler } from "./route/error_handler";
|
||||||
|
|
||||||
import {createInterface as createReadlineInterface} from 'readline';
|
import { createInterface as createReadlineInterface } from "readline";
|
||||||
import { DocumentAccessor, UserAccessor, TagAccessor } from './model/mod';
|
import { createComicWatcher } from "./diff/watcher/comic_watcher";
|
||||||
import { createComicWatcher } from './diff/watcher/comic_watcher';
|
import { DocumentAccessor, TagAccessor, UserAccessor } from "./model/mod";
|
||||||
import { getTagRounter } from './route/tags';
|
import { getTagRounter } from "./route/tags";
|
||||||
|
|
||||||
|
class ServerApplication {
|
||||||
class ServerApplication{
|
|
||||||
readonly userController: UserAccessor;
|
readonly userController: UserAccessor;
|
||||||
readonly documentController: DocumentAccessor;
|
readonly documentController: DocumentAccessor;
|
||||||
readonly tagController: TagAccessor;
|
readonly tagController: TagAccessor;
|
||||||
readonly diffManger: DiffManager;
|
readonly diffManger: DiffManager;
|
||||||
readonly app: Koa;
|
readonly app: Koa;
|
||||||
private index_html:string;
|
private index_html: string;
|
||||||
private constructor(controller:{
|
private constructor(controller: {
|
||||||
userController: UserAccessor,
|
userController: UserAccessor;
|
||||||
documentController:DocumentAccessor,
|
documentController: DocumentAccessor;
|
||||||
tagController: TagAccessor}){
|
tagController: TagAccessor;
|
||||||
|
}) {
|
||||||
this.userController = controller.userController;
|
this.userController = controller.userController;
|
||||||
this.documentController = controller.documentController;
|
this.documentController = controller.documentController;
|
||||||
this.tagController = controller.tagController;
|
this.tagController = controller.tagController;
|
||||||
|
|
||||||
this.diffManger = new DiffManager(this.documentController);
|
this.diffManger = new DiffManager(this.documentController);
|
||||||
this.app = new Koa();
|
this.app = new Koa();
|
||||||
this.index_html = readFileSync("index.html","utf-8");
|
this.index_html = readFileSync("index.html", "utf-8");
|
||||||
}
|
}
|
||||||
private async setup(){
|
private async setup() {
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
const app = this.app;
|
const app = this.app;
|
||||||
|
|
||||||
if(setting.cli){
|
if (setting.cli) {
|
||||||
const userAdmin = await getAdmin(this.userController);
|
const userAdmin = await getAdmin(this.userController);
|
||||||
if(await isAdminFirst(userAdmin)){
|
if (await isAdminFirst(userAdmin)) {
|
||||||
const rl = createReadlineInterface({
|
const rl = createReadlineInterface({
|
||||||
input:process.stdin,
|
input: process.stdin,
|
||||||
output:process.stdout
|
output: process.stdout,
|
||||||
});
|
});
|
||||||
const pw = await new Promise((res:(data:string)=>void,err)=>{
|
const pw = await new Promise((res: (data: string) => void, err) => {
|
||||||
rl.question("put admin password :",(data)=>{
|
rl.question("put admin password :", (data) => {
|
||||||
res(data);
|
res(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -62,165 +62,166 @@ class ServerApplication{
|
|||||||
app.use(createUserMiddleWare(this.userController));
|
app.use(createUserMiddleWare(this.userController));
|
||||||
|
|
||||||
let diff_router = createDiffRouter(this.diffManger);
|
let 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();
|
let 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", "*");
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use('/api/diff',diff_router.routes());
|
router.use("/api/diff", diff_router.routes());
|
||||||
router.use('/api/diff',diff_router.allowedMethods());
|
router.use("/api/diff", diff_router.allowedMethods());
|
||||||
|
|
||||||
const content_router = getContentRouter(this.documentController);
|
const content_router = getContentRouter(this.documentController);
|
||||||
router.use('/api/doc',content_router.routes());
|
router.use("/api/doc", content_router.routes());
|
||||||
router.use('/api/doc',content_router.allowedMethods());
|
router.use("/api/doc", content_router.allowedMethods());
|
||||||
|
|
||||||
const tags_router = getTagRounter(this.tagController);
|
const tags_router = getTagRounter(this.tagController);
|
||||||
router.use("/api/tags",tags_router.allowedMethods());
|
router.use("/api/tags", tags_router.allowedMethods());
|
||||||
router.use("/api/tags",tags_router.routes());
|
router.use("/api/tags", tags_router.routes());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.serve_with_meta_index(router);
|
this.serve_with_meta_index(router);
|
||||||
this.serve_index(router);
|
this.serve_index(router);
|
||||||
this.serve_static_file(router);
|
this.serve_static_file(router);
|
||||||
|
|
||||||
|
|
||||||
const login_router = createLoginRouter(this.userController);
|
const login_router = createLoginRouter(this.userController);
|
||||||
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`);
|
});
|
||||||
});}
|
}
|
||||||
app.use(router.routes());
|
app.use(router.routes());
|
||||||
app.use(router.allowedMethods());
|
app.use(router.allowedMethods());
|
||||||
console.log("setup done");
|
console.log("setup done");
|
||||||
}
|
}
|
||||||
private serve_index(router:Router){
|
private serve_index(router: Router) {
|
||||||
const serveindex = (url:string)=>{
|
const serveindex = (url: string) => {
|
||||||
router.get(url, (ctx)=>{
|
router.get(url, (ctx) => {
|
||||||
ctx.type = 'html'; ctx.body = this.index_html;
|
ctx.type = "html";
|
||||||
|
ctx.body = this.index_html;
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
ctx.set('x-content-type-options','no-sniff');
|
ctx.set("x-content-type-options", "no-sniff");
|
||||||
if(setting.mode === "development"){
|
if (setting.mode === "development") {
|
||||||
ctx.set('cache-control','no-cache');
|
ctx.set("cache-control", "no-cache");
|
||||||
|
} else {
|
||||||
|
ctx.set("cache-control", "public, max-age=3600");
|
||||||
}
|
}
|
||||||
else{
|
});
|
||||||
ctx.set('cache-control','public, max-age=3600');
|
};
|
||||||
}
|
serveindex("/");
|
||||||
})
|
serveindex("/doc/:rest(.*)");
|
||||||
}
|
serveindex("/search");
|
||||||
serveindex('/');
|
serveindex("/login");
|
||||||
serveindex('/doc/:rest(.*)');
|
serveindex("/profile");
|
||||||
serveindex('/search');
|
serveindex("/difference");
|
||||||
serveindex('/login');
|
serveindex("/setting");
|
||||||
serveindex('/profile');
|
serveindex("/tags");
|
||||||
serveindex('/difference');
|
|
||||||
serveindex('/setting');
|
|
||||||
serveindex('/tags');
|
|
||||||
}
|
}
|
||||||
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);
|
||||||
let meta;
|
let meta;
|
||||||
if(doc === undefined){
|
if (doc === undefined) {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
meta = NotFoundContent();
|
meta = NotFoundContent();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
meta = createOgTagContent(doc.title,doc.tags.join(", "),
|
meta = createOgTagContent(
|
||||||
`https://aeolian.prelude.duckdns.org/api/doc/${docId}/comic/thumbnail`);
|
doc.title,
|
||||||
|
doc.tags.join(", "),
|
||||||
|
`https://aeolian.prelude.duckdns.org/api/doc/${docId}/comic/thumbnail`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const html = makeMetaTagInjectedHTML(this.index_html,meta);
|
const html = makeMetaTagInjectedHTML(this.index_html, meta);
|
||||||
serveHTML(ctx,html);
|
serveHTML(ctx, html);
|
||||||
}
|
};
|
||||||
router.get('/doc/:id(\\d+)',DocMiddleware);
|
router.get("/doc/:id(\\d+)", DocMiddleware);
|
||||||
|
|
||||||
function NotFoundContent(){
|
function NotFoundContent() {
|
||||||
return createOgTagContent("Not Found Doc","Not Found","");
|
return createOgTagContent("Not Found Doc", "Not Found", "");
|
||||||
}
|
}
|
||||||
function makeMetaTagInjectedHTML(html:string,tagContent:string){
|
function makeMetaTagInjectedHTML(html: string, tagContent: string) {
|
||||||
return html.replace("<!--MetaTag-Outlet-->",tagContent);
|
return html.replace("<!--MetaTag-Outlet-->", tagContent);
|
||||||
}
|
}
|
||||||
function serveHTML(ctx: Koa.Context, file: string){
|
function serveHTML(ctx: Koa.Context, file: string) {
|
||||||
ctx.type = 'html'; ctx.body = file;
|
ctx.type = "html";
|
||||||
|
ctx.body = file;
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
ctx.set('x-content-type-options','no-sniff');
|
ctx.set("x-content-type-options", "no-sniff");
|
||||||
if(setting.mode === "development"){
|
if (setting.mode === "development") {
|
||||||
ctx.set('cache-control','no-cache');
|
ctx.set("cache-control", "no-cache");
|
||||||
}
|
} else {
|
||||||
else{
|
ctx.set("cache-control", "public, max-age=3600");
|
||||||
ctx.set('cache-control','public, max-age=3600');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMetaTagContent(key: string, value:string){
|
function createMetaTagContent(key: string, value: string) {
|
||||||
return `<meta property="${key}" content="${value}">`;
|
return `<meta property="${key}" content="${value}">`;
|
||||||
}
|
}
|
||||||
function createOgTagContent(title:string,description: string, image: string){
|
function createOgTagContent(title: string, description: string, image: string) {
|
||||||
return [createMetaTagContent("og:title",title),
|
return [
|
||||||
createMetaTagContent("og:type","website"),
|
createMetaTagContent("og:title", title),
|
||||||
createMetaTagContent("og:description",description),
|
createMetaTagContent("og:type", "website"),
|
||||||
createMetaTagContent("og:image",image),
|
createMetaTagContent("og:description", description),
|
||||||
//createMetaTagContent("og:image:width","480"),
|
createMetaTagContent("og:image", image),
|
||||||
//createMetaTagContent("og:image","480"),
|
// createMetaTagContent("og:image:width","480"),
|
||||||
//createMetaTagContent("og:image:type","image/png"),
|
// createMetaTagContent("og:image","480"),
|
||||||
createMetaTagContent("twitter:card","summary_large_image"),
|
// createMetaTagContent("og:image:type","image/png"),
|
||||||
createMetaTagContent("twitter:title",title),
|
createMetaTagContent("twitter:card", "summary_large_image"),
|
||||||
createMetaTagContent("twitter:description",description),
|
createMetaTagContent("twitter:title", title),
|
||||||
createMetaTagContent("twitter:image",image),
|
createMetaTagContent("twitter:description", description),
|
||||||
].join("\n");
|
createMetaTagContent("twitter:image", image),
|
||||||
|
].join("\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.body = createReadStream(path);
|
ctx.type = type;
|
||||||
ctx.set('x-content-type-options','no-sniff');
|
ctx.body = createReadStream(path);
|
||||||
if(setting.mode === "development"){
|
ctx.set("x-content-type-options", "no-sniff");
|
||||||
ctx.set('cache-control','no-cache');
|
if (setting.mode === "development") {
|
||||||
|
ctx.set("cache-control", "no-cache");
|
||||||
|
} else {
|
||||||
|
ctx.set("cache-control", "public, max-age=3600");
|
||||||
}
|
}
|
||||||
else{
|
});
|
||||||
ctx.set('cache-control','public, max-age=3600');
|
};
|
||||||
}
|
|
||||||
})};
|
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
static_file_server('dist/bundle.css','css');
|
static_file_server("dist/bundle.css", "css");
|
||||||
static_file_server('dist/bundle.js','js');
|
static_file_server("dist/bundle.js", "js");
|
||||||
if(setting.mode === "development"){
|
if (setting.mode === "development") {
|
||||||
static_file_server('dist/bundle.js.map','text');
|
static_file_server("dist/bundle.js.map", "text");
|
||||||
static_file_server('dist/bundle.css.map','text');
|
static_file_server("dist/bundle.css.map", "text");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
start_server(){
|
start_server() {
|
||||||
let setting = get_setting();
|
let 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();
|
let db = await connectDB();
|
||||||
|
|
||||||
const app = new ServerApplication({
|
const app = new ServerApplication({
|
||||||
userController:createKnexUserController(db),
|
userController: createKnexUserController(db),
|
||||||
documentController: createKnexDocumentAccessor(db),
|
documentController: createKnexDocumentAccessor(db),
|
||||||
tagController: createKnexTagController(db),
|
tagController: createKnexTagController(db),
|
||||||
});
|
});
|
||||||
@ -229,8 +230,8 @@ class ServerApplication{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create_server(){
|
export async function create_server() {
|
||||||
return await ServerApplication.createServer();
|
return await ServerApplication.createServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {create_server};
|
export default { create_server };
|
||||||
|
62
src/types/db.d.ts
vendored
62
src/types/db.d.ts
vendored
@ -1,34 +1,34 @@
|
|||||||
import {Knex} from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
declare module "knex" {
|
declare module "knex" {
|
||||||
interface Tables {
|
interface Tables {
|
||||||
tags: {
|
tags: {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
users: {
|
users: {
|
||||||
username: string;
|
username: string;
|
||||||
password_hash: string;
|
password_hash: string;
|
||||||
password_salt: string;
|
password_salt: string;
|
||||||
};
|
};
|
||||||
document: {
|
document: {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
content_type: string;
|
content_type: string;
|
||||||
basepath: string;
|
basepath: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
deleted_at: number|null;
|
deleted_at: number | null;
|
||||||
content_hash: string;
|
content_hash: string;
|
||||||
additional: string|null;
|
additional: string | null;
|
||||||
};
|
};
|
||||||
doc_tag_relation: {
|
doc_tag_relation: {
|
||||||
doc_id: number;
|
doc_id: number;
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
};
|
};
|
||||||
permissions: {
|
permissions: {
|
||||||
username: string;
|
username: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
|
export type JSONPrimitive = null | boolean | number | string;
|
||||||
export type JSONPrimitive = null|boolean|number|string;
|
export interface JSONMap extends Record<string, JSONType> {}
|
||||||
export interface JSONMap extends Record<string, JSONType>{}
|
export interface JSONArray extends Array<JSONType> {}
|
||||||
export interface JSONArray extends Array<JSONType>{};
|
export type JSONType = JSONMap | JSONPrimitive | JSONArray;
|
||||||
export type JSONType = JSONMap|JSONPrimitive|JSONArray;
|
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import {readFileSync, existsSync, writeFileSync, promises as fs} from 'fs';
|
import { existsSync, promises as fs, readFileSync, writeFileSync } from "fs";
|
||||||
import {validate} from 'jsonschema';
|
import { validate } from "jsonschema";
|
||||||
|
|
||||||
export class ConfigManager<T>{
|
export class ConfigManager<T> {
|
||||||
path:string;
|
path: string;
|
||||||
default_config: T;
|
default_config: T;
|
||||||
config: T| null;
|
config: T | null;
|
||||||
schema:object;
|
schema: object;
|
||||||
constructor(path:string,default_config:T,schema:object){
|
constructor(path: string, default_config: T, schema: object) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.default_config = default_config;
|
this.default_config = default_config;
|
||||||
this.config = null;
|
this.config = null;
|
||||||
this.schema = schema;
|
this.schema = schema;
|
||||||
}
|
}
|
||||||
get_config_file(): T{
|
get_config_file(): T {
|
||||||
if(this.config !== null) return this.config;
|
if (this.config !== null) return this.config;
|
||||||
this.config = {...this.read_config_file()};
|
this.config = { ...this.read_config_file() };
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
private emptyToDefault(target:T){
|
private emptyToDefault(target: T) {
|
||||||
let occur = false;
|
let occur = false;
|
||||||
for(const key in this.default_config){
|
for (const key in this.default_config) {
|
||||||
if(key === undefined || key in target){
|
if (key === undefined || key in target) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
target[key] = this.default_config[key];
|
target[key] = this.default_config[key];
|
||||||
@ -28,24 +28,24 @@ export class ConfigManager<T>{
|
|||||||
}
|
}
|
||||||
return occur;
|
return occur;
|
||||||
}
|
}
|
||||||
read_config_file():T{
|
read_config_file(): T {
|
||||||
if(!existsSync(this.path)){
|
if (!existsSync(this.path)) {
|
||||||
writeFileSync(this.path,JSON.stringify(this.default_config));
|
writeFileSync(this.path, JSON.stringify(this.default_config));
|
||||||
return this.default_config;
|
return this.default_config;
|
||||||
}
|
}
|
||||||
const ret = JSON.parse(readFileSync(this.path,{encoding:"utf8"}));
|
const ret = JSON.parse(readFileSync(this.path, { encoding: "utf8" }));
|
||||||
if(this.emptyToDefault(ret)){
|
if (this.emptyToDefault(ret)) {
|
||||||
writeFileSync(this.path,JSON.stringify(ret));
|
writeFileSync(this.path, JSON.stringify(ret));
|
||||||
}
|
}
|
||||||
const result = validate(ret,this.schema);
|
const result = validate(ret, this.schema);
|
||||||
if(!result.valid){
|
if (!result.valid) {
|
||||||
throw new Error(result.toString());
|
throw new Error(result.toString());
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
async write_config_file(new_config:T){
|
async write_config_file(new_config: T) {
|
||||||
this.config = new_config;
|
this.config = new_config;
|
||||||
await fs.writeFile(`${this.path}.temp`,JSON.stringify(new_config));
|
await fs.writeFile(`${this.path}.temp`, JSON.stringify(new_config));
|
||||||
await fs.rename(`${this.path}.temp`,this.path);
|
await fs.rename(`${this.path}.temp`, this.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,16 +1,15 @@
|
|||||||
export function check_type<T>(obj: any,check_proto:Record<string,string|undefined>):obj is T{
|
export function check_type<T>(obj: any, check_proto: Record<string, string | undefined>): obj is T {
|
||||||
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 (!(obj[it] instanceof Array)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
} else if (defined !== typeof obj[it]) {
|
||||||
else if(defined !== typeof obj[it]){
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
}
|
||||||
|
@ -1,31 +1,33 @@
|
|||||||
import { ZipEntry } from 'node-stream-zip';
|
import { ZipEntry } from "node-stream-zip";
|
||||||
|
|
||||||
import {orderBy} from 'natural-orderby';
|
import { ReadStream } from "fs";
|
||||||
import { ReadStream } from 'fs';
|
import { orderBy } from "natural-orderby";
|
||||||
import StreamZip from 'node-stream-zip';
|
import StreamZip from "node-stream-zip";
|
||||||
|
|
||||||
export type ZipAsync = InstanceType<typeof StreamZip.async>;
|
export type ZipAsync = InstanceType<typeof StreamZip.async>;
|
||||||
export async function readZip(path : string): Promise<ZipAsync>{
|
export async function readZip(path: string): Promise<ZipAsync> {
|
||||||
return new StreamZip.async({
|
return new StreamZip.async({
|
||||||
file:path,
|
file: path,
|
||||||
storeEntries: true
|
storeEntries: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
export async function entriesByNaturalOrder(zip: ZipAsync){
|
export async function entriesByNaturalOrder(zip: ZipAsync) {
|
||||||
const entries = await zip.entries();
|
const entries = await zip.entries();
|
||||||
const ret = orderBy(Object.values(entries),v=>v.name);
|
const ret = orderBy(Object.values(entries), v => v.name);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createReadableStreamFromZip(zip:ZipAsync,entry: ZipEntry):Promise<NodeJS.ReadableStream>{
|
export async function createReadableStreamFromZip(zip: ZipAsync, entry: ZipEntry): Promise<NodeJS.ReadableStream> {
|
||||||
return await zip.stream(entry);
|
return await zip.stream(entry);
|
||||||
}
|
}
|
||||||
export async function readAllFromZip(zip:ZipAsync,entry: ZipEntry):Promise<Buffer>{
|
export async function readAllFromZip(zip: ZipAsync, entry: ZipEntry): Promise<Buffer> {
|
||||||
const stream = await createReadableStreamFromZip(zip,entry);
|
const stream = await createReadableStreamFromZip(zip, entry);
|
||||||
const chunks:Uint8Array[] = [];
|
const chunks: Uint8Array[] = [];
|
||||||
return new Promise((resolve,reject)=>{
|
return new Promise((resolve, reject) => {
|
||||||
stream.on('data',(data)=>{chunks.push(data)});
|
stream.on("data", (data) => {
|
||||||
stream.on('error', (err)=>reject(err));
|
chunks.push(data);
|
||||||
stream.on('end',()=>resolve(Buffer.concat(chunks)));
|
});
|
||||||
|
stream.on("error", (err) => reject(err));
|
||||||
|
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -4,17 +4,17 @@
|
|||||||
|
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "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", "ES6"], /* Specify library files to be included in the compilation. */
|
||||||
//"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'. */
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
"outDir": "./build", /* Redirect output structure to the directory. */
|
"outDir": "./build", /* Redirect output structure to the directory. */
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
// "composite": true, /* Enable project compilation */
|
// "composite": true, /* Enable project compilation */
|
||||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
@ -25,13 +25,13 @@
|
|||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
/* Strict Type-Checking Options */
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
"strictNullChecks": true, /* Enable strict null checks. */
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
/* Additional Checks */
|
/* Additional Checks */
|
||||||
@ -41,14 +41,14 @@
|
|||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
@ -64,9 +64,9 @@
|
|||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
/* Advanced Options */
|
/* Advanced Options */
|
||||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
},
|
},
|
||||||
"include": ["./"],
|
"include": ["./"],
|
||||||
"exclude": ["src/client","app","seeds"],
|
"exclude": ["src/client", "app", "seeds"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user