diff --git a/src/content/manager.ts b/src/content/manager.ts new file mode 100644 index 0000000..baecf6d --- /dev/null +++ b/src/content/manager.ts @@ -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; + path: string; + desc: object|undefined; +} + +export interface ContentContext{ + content:ContentReferrer +} + +export interface ContentManager{ + getRouter():Router + createContent(path:string,option?:any):Promise +} \ No newline at end of file diff --git a/src/content/manga.ts b/src/content/manga.ts new file mode 100644 index 0000000..fe3ec52 --- /dev/null +++ b/src/content/manga.ts @@ -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{ + 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{ + 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{ + return "a"; + } +} + +export class MangaManager implements ContentManager{ + async createContent(path:string):Promise{ + return new MangaReferrer(path); + } + getRouter(){ + const router = new Router(); + 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; + } +} \ No newline at end of file diff --git a/src/content/mod.ts b/src/content/mod.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/content/util.ts b/src/content/util.ts new file mode 100644 index 0000000..c2a444e --- /dev/null +++ b/src/content/util.ts @@ -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; +} diff --git a/src/content/video.ts b/src/content/video.ts new file mode 100644 index 0000000..881b719 --- /dev/null +++ b/src/content/video.ts @@ -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{ + return "a"; + } +} + +export class VideoManager implements ContentManager{ + async createContent(path:string):Promise{ + return new VideoReferrer(path); + } + getRouter(){ + const router = new Router(); + router.get("/",async (ctx,next)=>{ + await renderVideo(ctx,ctx.state.content.path); + }); + return router; + } +} \ No newline at end of file diff --git a/src/db/contents.ts b/src/db/contents.ts index 51b4ad1..3cdb65e 100644 --- a/src/db/contents.ts +++ b/src/db/contents.ts @@ -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); } diff --git a/src/model/contents.ts b/src/model/contents.ts index 34cf15f..960fe5b 100644 --- a/src/model/contents.ts +++ b/src/model/contents.ts @@ -67,9 +67,9 @@ export interface QueryListOption{ */ eager_loading?:boolean, /** - * + * content type */ - content_type?:string, + content_type?:string } export interface ContentAccessor{ diff --git a/src/route/contents.ts b/src/route/contents.ts index 6f6243f..705cba0 100644 --- a/src/route/contents.ts +++ b/src/route/contents.ts @@ -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)); diff --git a/src/route/render.ts b/src/route/render.ts deleted file mode 100644 index 09f7bd3..0000000 --- a/src/route/render.ts +++ /dev/null @@ -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. - } -} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 1dc1ee4..a21aa96 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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++}`); diff --git a/src/util/ziputil.ts b/src/util/ziputil.ts deleted file mode 100644 index 3c54bd9..0000000 --- a/src/util/ziputil.ts +++ /dev/null @@ -1,36 +0,0 @@ -import StreamZip, { ZipEntry } from 'node-stream-zip'; -import {orderBy}from 'natural-orderby'; - -export async function readZip(path : string):Promise{ - 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{ - return new Promise((resolve,reject)=>{ - zip.stream(entry,(err, stream)=>{ - if(stream !== undefined){ - resolve(stream); - } - else{ - reject(err); - } - });} - ); -} \ No newline at end of file