diff --git a/src/content/manager.ts b/src/content/manager.ts deleted file mode 100644 index baecf6d..0000000 --- a/src/content/manager.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 index fe3ec52..68535ab 100644 --- a/src/content/manga.ts +++ b/src/content/manga.ts @@ -1,98 +1,8 @@ - -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; +import {ContentReferrer} from './referrer'; +import {createDefaultClass,registerContentReferrer} from './referrer'; +export class MangaReferrer extends createDefaultClass("manga"){ constructor(path:string){ - this.path = path; - this.desc = undefined; + super(path); } - 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 +}; +registerContentReferrer(MangaReferrer); \ No newline at end of file diff --git a/src/content/referrer.ts b/src/content/referrer.ts new file mode 100644 index 0000000..fada096 --- /dev/null +++ b/src/content/referrer.ts @@ -0,0 +1,44 @@ +import {Context, DefaultState, DefaultContext, Middleware, Next} from 'koa'; +import Router from 'koa-router'; +import {createHash} from 'crypto'; +import {promises} from 'fs' +/** + * content file or directory referrer + */ +export interface ContentReferrer{ + getHash():Promise; + readonly path: string; + readonly type: string; + desc: object|undefined; +} +type ContentReferrerConstructor = (new (path:string,desc?:object) => ContentReferrer)&{content_type:string}; +export const createDefaultClass = (type:string):ContentReferrerConstructor=>{ + let cons = class implements ContentReferrer{ + readonly path: string; + type = type; + static content_type = type; + + desc: object|undefined; + constructor(path:string,desc?:object){ + this.path = path; + this.desc = desc; + } + async getHash():Promise{ + const stat = await promises.stat(this.path); + const hash = createHash("sha512"); + hash.update(stat.mode.toString()); + //if(this.desc !== undefined) + // hash.update(JSON.stringify(this.desc)); + hash.update(stat.size.toString()); + return hash.digest("base64"); + } + }; + return cons; +} +let ContstructorTable:{[k:string]:ContentReferrerConstructor} = {}; +export function registerContentReferrer(s: ContentReferrerConstructor){ + ContstructorTable[s.content_type] = s; +} +export function createContentReferrer(type:string,path:string,desc?:object){ + return new ContstructorTable[type](path,desc); +} \ No newline at end of file diff --git a/src/content/video.ts b/src/content/video.ts index 881b719..8a16429 100644 --- a/src/content/video.ts +++ b/src/content/video.ts @@ -1,79 +1,9 @@ -import {Context, DefaultContext, DefaultState} from 'koa'; -import {promises, createReadStream} from "fs"; -import { ContentContext, ContentManager, ContentReferrer } from './manager'; -import Router from 'koa-router'; +import {ContentReferrer, registerContentReferrer} from './referrer'; +import {createDefaultClass} from './referrer'; -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; +export class VideoReferrer extends createDefaultClass("video"){ constructor(path:string,desc?:object){ - this.path = path; - this.desc = desc; - } - async getHash():Promise{ - return "a"; + super(path,desc); } } - -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 +registerContentReferrer(VideoReferrer); \ No newline at end of file diff --git a/src/route/all.ts b/src/route/all.ts new file mode 100644 index 0000000..dc22c53 --- /dev/null +++ b/src/route/all.ts @@ -0,0 +1,58 @@ +import { DefaultContext, Middleware, Next, ParameterizedContext } from 'koa'; +import compose from 'koa-compose'; +import Router, { IParamMiddleware } from 'koa-router'; +import { ContentContext } from './context'; +import MangaRouter from './manga'; +import VideoRouter from './video'; + +const table:{[s:string]:Router|undefined} = { + "manga": new MangaRouter, + "video": new VideoRouter +} +const all_middleware = (cont: string|undefined, restarg: string|undefined)=>async (ctx:ParameterizedContext,next:Next)=>{ + if(cont == undefined){ + ctx.status = 404; + return; + } + if(ctx.state.content.type != cont){ + ctx.status = 404; + return; + } + const router = table[cont]; + if(router == undefined){ + ctx.status = 404; + return; + } + const rest = "/"+(restarg as string|undefined || ""); + + const result = router.match(rest,"GET"); + console.log(`s : ${result.pathAndMethod}`); + if(!result.route){ + return await next(); + } + const chain = result.pathAndMethod.reduce((combination : Middleware[],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 combination.concat(cur.stack); + },[]); + return await compose(chain)(ctx,next); +}; +export class AllContentRouter extends Router{ + constructor(){ + super(); + this.get('/:content_type',async (ctx,next)=>{ + console.log("no x"); + return await (all_middleware(ctx.params["content_type"],undefined))(ctx,next); + }); + this.get('/:content_type/:rest(.*)', async (ctx,next) => { + console.log("yes x"); + const cont = ctx.params["content_type"] as string; + return await (all_middleware(cont,ctx.params["rest"]))(ctx,next); + }); + } +}; \ No newline at end of file diff --git a/src/route/contents.ts b/src/route/contents.ts index 705cba0..c278b45 100644 --- a/src/route/contents.ts +++ b/src/route/contents.ts @@ -4,6 +4,9 @@ import {ContentAccessor, isContentContent} from './../model/contents'; import {QueryListOption} from './../model/contents'; import {ParseQueryNumber, ParseQueryArray, ParseQueryBoolean} from './util' import {sendError} from './error_handler'; +import { createContentReferrer } from '../content/referrer'; +import { join } from 'path'; +import {AllContentRouter} from './all'; const ContentIDHandler = (controller: ContentAccessor) => async (ctx: Context,next: Next)=>{ const num = Number.parseInt(ctx.params['num']); @@ -102,6 +105,18 @@ const DeleteContentHandler = (controller : ContentAccessor) => async (ctx: Conte ctx.body = {"ret":r}; ctx.type = 'json'; }; +const ContentHandler = (controller : ContentAccessor) => async (ctx:Context, next:Next) => { + const num = Number.parseInt(ctx.params['num']); + let content = await controller.findById(num,true); + if (content == undefined){ + sendError(404,"content does not exist."); + return; + } + const path = join(content.basepath,content.filename); + ctx.state['content'] = createContentReferrer(content.content_type,path,content.additional); + await next(); +}; + export const getContentRouter = (controller: ContentAccessor)=>{ const ret = new Router(); ret.get("/search",ContentQueryHandler(controller)); @@ -112,7 +127,8 @@ export const getContentRouter = (controller: ContentAccessor)=>{ ret.post("/:num(\\d+)/tags/:tag",AddTagHandler(controller)); ret.del("/:num(\\d+)/tags/:tag",DelTagHandler(controller)); ret.del("/:num(\\d+)",DeleteContentHandler(controller)); - //ret.get("/"); + ret.all("/:num(\\d+)/(.*)",ContentHandler(controller)); + ret.use("/:num",(new AllContentRouter).routes()); return ret; } diff --git a/src/route/context.ts b/src/route/context.ts new file mode 100644 index 0000000..f810743 --- /dev/null +++ b/src/route/context.ts @@ -0,0 +1,5 @@ +import {ContentReferrer} from '../content/referrer'; + +export interface ContentContext{ + content:ContentReferrer +} \ No newline at end of file diff --git a/src/route/manga.ts b/src/route/manga.ts new file mode 100644 index 0000000..d342452 --- /dev/null +++ b/src/route/manga.ts @@ -0,0 +1,84 @@ + +import {Context, DefaultContext, DefaultState, Next} from 'koa'; +import StreamZip, { ZipEntry } from 'node-stream-zip'; +import {orderBy} from 'natural-orderby'; +import {since_last_modified} from '../content/util'; +import {ContentContext} from './context'; +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 MangaRouter extends Router{ + constructor(){ + super(); + this.get("/",async (ctx,next)=>{ + await renderZipImage(ctx,ctx.state.content.path,0); + }); + this.get("/:page(\\d+)",async (ctx,next)=>{ + const page = Number.parseInt(ctx.params['page']); + await renderZipImage(ctx,ctx.state.content.path,page); + }); + } +} + +export default MangaRouter; \ No newline at end of file diff --git a/src/route/video.ts b/src/route/video.ts new file mode 100644 index 0000000..8638085 --- /dev/null +++ b/src/route/video.ts @@ -0,0 +1,65 @@ +import {Context } from 'koa'; +import {promises, createReadStream} from "fs"; +import {ContentContext} from './context'; +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 VideoRouter extends Router{ + constructor(){ + super(); + this.get("/", async (ctx,next)=>{ + await renderVideo(ctx,ctx.state.content.path); + }); + } +} + +export default VideoRouter; \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index a21aa96..bc40b31 100644 --- a/src/server.ts +++ b/src/server.ts @@ -10,11 +10,11 @@ import getContentRouter from './route/contents'; import { createKnexContentsAccessor } from './db/contents'; import bodyparser from 'koa-bodyparser'; import {error_handler} from './route/error_handler'; +import {MangaReferrer} from './content/manga'; +import {VideoReferrer} from './content/video'; -import {MangaManager,MangaReferrer} from './content/manga' -import { ContentContext } from './content/manager'; -import { Context } from 'vm'; -import { VideoManager } from './content/video'; +import { ContentContext } from './route/context'; +import { AllContentRouter } from './route/all'; //let Koa = require("koa"); async function main(){ @@ -51,23 +51,17 @@ async function main(){ let content_router = getContentRouter(createKnexContentsAccessor(db)); router.use('/content',content_router.routes()); 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"); + let ctnrouter = new AllContentRouter(); + router.all('/image/(.*)', async (ctx,next)=>{ + ctx.state['content'] = new MangaReferrer("testdata/test_zip.zip"); await next(); }); - 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"); + router.use('/image',ctnrouter.routes()); + router.all('/ss/(.*)', async (ctx,next)=>{ + ctx.state['content'] = new VideoReferrer("testdata/video_test.mp4"); await next(); }); - router.use('/ss.mp4',video_manager.getRouter().routes()); + router.use('/ss',ctnrouter.routes()); let mm_count=0; app.use(async (ctx,next)=>{ console.log(`==========================${mm_count++}`);