manager
This commit is contained in:
parent
eb771fb5b7
commit
86114e4eb9
19
src/content/manager.ts
Normal file
19
src/content/manager.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {Context, DefaultState, DefaultContext, Middleware, Next} from 'koa';
|
||||
import Router from 'koa-router';
|
||||
/**
|
||||
* content file or directory referrer
|
||||
*/
|
||||
export interface ContentReferrer{
|
||||
getHash():Promise<string>;
|
||||
path: string;
|
||||
desc: object|undefined;
|
||||
}
|
||||
|
||||
export interface ContentContext{
|
||||
content:ContentReferrer
|
||||
}
|
||||
|
||||
export interface ContentManager{
|
||||
getRouter():Router<ContentContext & DefaultState, DefaultContext>
|
||||
createContent(path:string,option?:any):Promise<ContentReferrer>
|
||||
}
|
98
src/content/manga.ts
Normal file
98
src/content/manga.ts
Normal file
@ -0,0 +1,98 @@
|
||||
|
||||
import {Context, DefaultContext, DefaultState, Next} from 'koa';
|
||||
import StreamZip, { ZipEntry } from 'node-stream-zip';
|
||||
import {orderBy} from 'natural-orderby';
|
||||
import {since_last_modified} from './util';
|
||||
import {ContentReferrer, ContentManager, ContentContext} from './manager'
|
||||
import Router from 'koa-router';
|
||||
|
||||
export async function readZip(path : string):Promise<StreamZip>{
|
||||
return new Promise((resolve,reject)=>{
|
||||
let zip = new StreamZip({
|
||||
file:path,
|
||||
storeEntries: true
|
||||
});
|
||||
zip.on('error',(err)=>{
|
||||
console.error(`read zip file ${path}`);
|
||||
reject(err);
|
||||
});
|
||||
zip.on('ready',()=>{
|
||||
resolve(zip);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
export function entriesByNaturalOrder(zip: StreamZip){
|
||||
const entries = zip.entries();
|
||||
const ret = orderBy(Object.values(entries),v=>v.name);
|
||||
return ret;
|
||||
}
|
||||
export async function createReadStreamFromZip(zip:StreamZip,entry: ZipEntry):Promise<NodeJS.ReadableStream>{
|
||||
return new Promise((resolve,reject)=>{
|
||||
zip.stream(entry,(err, stream)=>{
|
||||
if(stream !== undefined){
|
||||
resolve(stream);
|
||||
}
|
||||
else{
|
||||
reject(err);
|
||||
}
|
||||
});}
|
||||
);
|
||||
}
|
||||
|
||||
async function renderZipImage(ctx: Context,path : string, page:number){
|
||||
const image_ext = ['gif', 'png', 'jpeg', 'bmp', 'webp', 'jpg'];
|
||||
let zip = await readZip(path);
|
||||
const entries = entriesByNaturalOrder(zip).filter(x=>{
|
||||
const ext = x.name.split('.').pop();
|
||||
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 createReadStreamFromZip(zip,entry));
|
||||
read_stream.on('close',()=>zip.close());
|
||||
ctx.body = read_stream;
|
||||
ctx.response.length = entry.size;
|
||||
//console.log(`${entry.name}'s ${page}:${entry.size}`);
|
||||
ctx.response.type = entry.name.split(".").pop() as string;
|
||||
ctx.status = 200;
|
||||
ctx.set('Date', new Date().toUTCString());
|
||||
ctx.set("Last-Modified",last_modified.toUTCString());
|
||||
}
|
||||
else{
|
||||
ctx.status = 404;
|
||||
}
|
||||
}
|
||||
|
||||
export class MangaReferrer implements ContentReferrer{
|
||||
readonly path: string;
|
||||
desc: object|undefined;
|
||||
constructor(path:string){
|
||||
this.path = path;
|
||||
this.desc = undefined;
|
||||
}
|
||||
async getHash():Promise<string>{
|
||||
return "a";
|
||||
}
|
||||
}
|
||||
|
||||
export class MangaManager implements ContentManager{
|
||||
async createContent(path:string):Promise<MangaReferrer>{
|
||||
return new MangaReferrer(path);
|
||||
}
|
||||
getRouter(){
|
||||
const router = new Router<DefaultState&ContentContext,DefaultContext>();
|
||||
router.get("/",async (ctx,next)=>{
|
||||
await renderZipImage(ctx,ctx.state.content.path,0);
|
||||
})
|
||||
router.get("/:page(\\d+)",async (ctx,next)=>{
|
||||
const page = Number.parseInt(ctx.params['page']);
|
||||
await renderZipImage(ctx,ctx.state.content.path,page);
|
||||
});
|
||||
return router;
|
||||
}
|
||||
}
|
0
src/content/mod.ts
Normal file
0
src/content/mod.ts
Normal file
10
src/content/util.ts
Normal file
10
src/content/util.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {Context} from 'koa';
|
||||
|
||||
export function since_last_modified(ctx: Context, last_modified: Date): boolean{
|
||||
const con = ctx.get("If-Modified-Since");
|
||||
if(con === "") return false;
|
||||
const mdate = new Date(con);
|
||||
if(last_modified > mdate) return false;
|
||||
ctx.status = 304;
|
||||
return true;
|
||||
}
|
79
src/content/video.ts
Normal file
79
src/content/video.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import {Context, DefaultContext, DefaultState} from 'koa';
|
||||
import {promises, createReadStream} from "fs";
|
||||
import { ContentContext, ContentManager, ContentReferrer } from './manager';
|
||||
import Router from 'koa-router';
|
||||
|
||||
export async function renderVideo(ctx: Context,path : string){
|
||||
const ext = path.trim().split('.').pop();
|
||||
if(ext === undefined) {
|
||||
//ctx.status = 404;
|
||||
console.error(`${path}:${ext}`)
|
||||
return;
|
||||
}
|
||||
ctx.response.type = ext;
|
||||
const range_text = ctx.request.get("range");
|
||||
const stat = await promises.stat(path);
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
ctx.set('Last-Modified',(new Date(stat.mtime).toUTCString()));
|
||||
ctx.set('Date', new Date().toUTCString());
|
||||
ctx.set("Accept-Ranges", "bytes");
|
||||
if(range_text === ''){
|
||||
end = 1024*512;
|
||||
end = Math.min(end,stat.size-1);
|
||||
if(start > end){
|
||||
ctx.status = 416;
|
||||
return;
|
||||
}
|
||||
ctx.status = 200;
|
||||
ctx.length = stat.size;
|
||||
let stream = createReadStream(path);
|
||||
ctx.body = stream;
|
||||
}
|
||||
else{
|
||||
const m = range_text.match(/^bytes=(\d+)-(\d*)/);
|
||||
if(m === null){
|
||||
ctx.status = 416;
|
||||
return;
|
||||
}
|
||||
start = parseInt(m[1]);
|
||||
end = m[2].length > 0 ? parseInt(m[2]) : start + 1024*1024;
|
||||
end = Math.min(end,stat.size-1);
|
||||
if(start > end){
|
||||
ctx.status = 416;
|
||||
return;
|
||||
}
|
||||
ctx.status = 206;
|
||||
ctx.length = end - start + 1;
|
||||
ctx.response.set("Content-Range",`bytes ${start}-${end}/${stat.size}`);
|
||||
ctx.body = createReadStream(path,{
|
||||
start:start,
|
||||
end:end
|
||||
});//inclusive range.
|
||||
}
|
||||
}
|
||||
|
||||
export class VideoReferrer implements ContentReferrer{
|
||||
readonly path: string;
|
||||
desc: object|undefined;
|
||||
constructor(path:string,desc?:object){
|
||||
this.path = path;
|
||||
this.desc = desc;
|
||||
}
|
||||
async getHash():Promise<string>{
|
||||
return "a";
|
||||
}
|
||||
}
|
||||
|
||||
export class VideoManager implements ContentManager{
|
||||
async createContent(path:string):Promise<VideoReferrer>{
|
||||
return new VideoReferrer(path);
|
||||
}
|
||||
getRouter(){
|
||||
const router = new Router<DefaultState&ContentContext,DefaultContext>();
|
||||
router.get("/",async (ctx,next)=>{
|
||||
await renderVideo(ctx,ctx.state.content.path);
|
||||
});
|
||||
return router;
|
||||
}
|
||||
}
|
@ -60,6 +60,7 @@ class KnexContentsAccessor implements ContentAccessor{
|
||||
const use_offset = option.use_offset || false;
|
||||
const offset = option.offset || 0;
|
||||
const word = option.word;
|
||||
const content_type = option.content_type;
|
||||
const cursor = option.cursor;
|
||||
|
||||
const buildquery = ()=>{
|
||||
@ -76,6 +77,9 @@ class KnexContentsAccessor implements ContentAccessor{
|
||||
if(word !== undefined){
|
||||
query = query.where('title','like',`%${word}%`);
|
||||
}
|
||||
if(content_type !== undefined){
|
||||
query = query.where('content_type','=',content_type);
|
||||
}
|
||||
if(use_offset){
|
||||
query = query.offset(offset);
|
||||
}
|
||||
|
@ -67,9 +67,9 @@ export interface QueryListOption{
|
||||
*/
|
||||
eager_loading?:boolean,
|
||||
/**
|
||||
*
|
||||
* content type
|
||||
*/
|
||||
content_type?:string,
|
||||
content_type?:string
|
||||
}
|
||||
|
||||
export interface ContentAccessor{
|
||||
|
@ -106,7 +106,7 @@ export const getContentRouter = (controller: ContentAccessor)=>{
|
||||
const ret = new Router();
|
||||
ret.get("/search",ContentQueryHandler(controller));
|
||||
ret.get("/:num(\\d+)",ContentIDHandler(controller));
|
||||
//ret.get("/:num(\\d+)/:content_type");
|
||||
ret.use("/:num(\\d+)/:content_type");
|
||||
ret.post("/",CreateContentHandler(controller));
|
||||
ret.get("/:num(\\d+)/tags",ContentTagIDHandler(controller));
|
||||
ret.post("/:num(\\d+)/tags/:tag",AddTagHandler(controller));
|
||||
|
@ -1,99 +0,0 @@
|
||||
import {Context} from 'koa';
|
||||
import {promises, createReadStream} from "fs";
|
||||
import {createReadStreamFromZip, entriesByNaturalOrder, readZip} from "../util/ziputil";
|
||||
|
||||
function since_last_modified(ctx: Context, last_modified: Date): boolean{
|
||||
const con = ctx.get("If-Modified-Since");
|
||||
if(con === "") return false;
|
||||
const mdate = new Date(con);
|
||||
if(last_modified > mdate) return false;
|
||||
ctx.status = 304;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function renderZipImage(ctx: Context,path : string, page:number){
|
||||
const image_ext = ['gif', 'png', 'jpeg', 'bmp', 'webp', 'jpg'];
|
||||
let zip = await readZip(path);
|
||||
const entries = entriesByNaturalOrder(zip).filter(x=>{
|
||||
const ext = x.name.split('.').pop();
|
||||
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 createReadStreamFromZip(zip,entry));
|
||||
read_stream.on('close',()=>zip.close());
|
||||
ctx.body = read_stream;
|
||||
ctx.response.length = entry.size;
|
||||
//console.log(`${entry.name}'s ${page}:${entry.size}`);
|
||||
ctx.response.type = entry.name.split(".").pop() as string;
|
||||
ctx.status = 200;
|
||||
ctx.set('Date', new Date().toUTCString());
|
||||
ctx.set("Last-Modified",last_modified.toUTCString());
|
||||
}
|
||||
else{
|
||||
ctx.status = 404;
|
||||
}
|
||||
}
|
||||
export async function renderImage(ctx: Context,path : string){
|
||||
const ext = path.trim().split('.').pop();
|
||||
if(ext === undefined) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
ctx.response.type = ext;
|
||||
ctx.body = createReadStream(path);
|
||||
}
|
||||
|
||||
export async function renderVideo(ctx: Context,path : string){
|
||||
const ext = path.trim().split('.').pop();
|
||||
if(ext === undefined) {
|
||||
//ctx.status = 404;
|
||||
console.error(`${path}:${ext}`)
|
||||
return;
|
||||
}
|
||||
ctx.response.type = ext;
|
||||
const range_text = ctx.request.get("range");
|
||||
const stat = await promises.stat(path);
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
ctx.set('Last-Modified',(new Date(stat.mtime).toUTCString()));
|
||||
ctx.set('Date', new Date().toUTCString());
|
||||
ctx.set("Accept-Ranges", "bytes");
|
||||
if(range_text === ''){
|
||||
end = 1024*512;
|
||||
end = Math.min(end,stat.size-1);
|
||||
if(start > end){
|
||||
ctx.status = 416;
|
||||
return;
|
||||
}
|
||||
ctx.status = 200;
|
||||
ctx.length = stat.size;
|
||||
let stream = createReadStream(path);
|
||||
ctx.body = stream;
|
||||
}
|
||||
else{
|
||||
const m = range_text.match(/^bytes=(\d+)-(\d*)/);
|
||||
if(m === null){
|
||||
ctx.status = 416;
|
||||
return;
|
||||
}
|
||||
start = parseInt(m[1]);
|
||||
end = m[2].length > 0 ? parseInt(m[2]) : start + 1024*1024;
|
||||
end = Math.min(end,stat.size-1);
|
||||
if(start > end){
|
||||
ctx.status = 416;
|
||||
return;
|
||||
}
|
||||
ctx.status = 206;
|
||||
ctx.length = end - start + 1;
|
||||
ctx.response.set("Content-Range",`bytes ${start}-${end}/${stat.size}`);
|
||||
ctx.body = createReadStream(path,{
|
||||
start:start,
|
||||
end:end
|
||||
});//inclusive range.
|
||||
}
|
||||
}
|
@ -1,15 +1,21 @@
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import Koa, { DefaultState } from 'koa';
|
||||
import Router, { IParamMiddleware, IRouterParamContext } from 'koa-router';
|
||||
|
||||
import {get_setting} from './setting';
|
||||
import {connectDB} from './database';
|
||||
import {Watcher} from './diff'
|
||||
import {renderImage, renderVideo, renderZipImage} from './route/render';
|
||||
|
||||
import { createReadStream, readFileSync } from 'fs';
|
||||
import getContentRouter from './route/contents';
|
||||
import { createKnexContentsAccessor } from './db/contents';
|
||||
import bodyparser from 'koa-bodyparser';
|
||||
import {error_handler} from './route/error_handler';
|
||||
|
||||
import {MangaManager,MangaReferrer} from './content/manga'
|
||||
import { ContentContext } from './content/manager';
|
||||
import { Context } from 'vm';
|
||||
import { VideoManager } from './content/video';
|
||||
|
||||
//let Koa = require("koa");
|
||||
async function main(){
|
||||
let app = new Koa();
|
||||
@ -44,29 +50,24 @@ async function main(){
|
||||
);
|
||||
let content_router = getContentRouter(createKnexContentsAccessor(db));
|
||||
router.use('/content',content_router.routes());
|
||||
router.get('/ss.mp4',async (ctx,next)=>{
|
||||
/*for(let i in ctx.header){
|
||||
if(i !== undefined)
|
||||
console.log(i,ctx.get(i));
|
||||
}*/
|
||||
await renderVideo(ctx,"testdata/video_test.mp4");
|
||||
router.use('/content',content_router.allowedMethods());
|
||||
let manga_manager = new MangaManager();
|
||||
let video_manager = new VideoManager();
|
||||
router.use('/image', async (ctx,next)=>{
|
||||
ctx.state['content'] = await manga_manager.createContent("testdata/test_zip.zip");
|
||||
await next();
|
||||
});
|
||||
router.get('/image/:number',async (ctx,next)=>{
|
||||
let page = ctx.params.number;
|
||||
console.log("page type : "+typeof page)
|
||||
await renderZipImage(ctx,"testdata/test_zip.zip",page);
|
||||
ctx.set("cache-control","max-age=3600");
|
||||
let rr = manga_manager.getRouter();
|
||||
rr.prefix("/image");
|
||||
router.use('/image',(ctx,next)=>{
|
||||
console.log("asdf");
|
||||
rr.routes()(ctx,next);
|
||||
});
|
||||
router.use('/ss.mp4', async (ctx,next)=>{
|
||||
ctx.state['content'] = await video_manager.createContent("testdata/video_test.mp4");
|
||||
await next();
|
||||
});
|
||||
router.get('/aaaa/:number(\\d+)',async (ctx,next)=>{
|
||||
let page = ctx.params.number;
|
||||
console.log("matched");
|
||||
await renderZipImage(ctx,"testdata/test_zip.zip",page);
|
||||
ctx.set("cache-control","max-age=3600");
|
||||
await next();
|
||||
});
|
||||
|
||||
router.use('/ss.mp4',video_manager.getRouter().routes());
|
||||
let mm_count=0;
|
||||
app.use(async (ctx,next)=>{
|
||||
console.log(`==========================${mm_count++}`);
|
||||
|
@ -1,36 +0,0 @@
|
||||
import StreamZip, { ZipEntry } from 'node-stream-zip';
|
||||
import {orderBy}from 'natural-orderby';
|
||||
|
||||
export async function readZip(path : string):Promise<StreamZip>{
|
||||
return new Promise((resolve,reject)=>{
|
||||
let zip = new StreamZip({
|
||||
file:path,
|
||||
storeEntries: true
|
||||
});
|
||||
zip.on('error',(err)=>{
|
||||
console.error(`read zip file ${path}`);
|
||||
reject(err);
|
||||
});
|
||||
zip.on('ready',()=>{
|
||||
resolve(zip);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
export function entriesByNaturalOrder(zip: StreamZip){
|
||||
const entries = zip.entries();
|
||||
const ret = orderBy(Object.values(entries),v=>v.name);
|
||||
return ret;
|
||||
}
|
||||
export async function createReadStreamFromZip(zip:StreamZip,entry: ZipEntry):Promise<NodeJS.ReadableStream>{
|
||||
return new Promise((resolve,reject)=>{
|
||||
zip.stream(entry,(err, stream)=>{
|
||||
if(stream !== undefined){
|
||||
resolve(stream);
|
||||
}
|
||||
else{
|
||||
reject(err);
|
||||
}
|
||||
});}
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user