api compelete
This commit is contained in:
		
							parent
							
								
									221a3f2748
								
							
						
					
					
						commit
						eaafdbe577
					
				
					 10 changed files with 295 additions and 208 deletions
				
			
		| 
						 | 
				
			
			@ -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<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>
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<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;
 | 
			
		||||
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<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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
registerContentReferrer(MangaReferrer);
 | 
			
		||||
							
								
								
									
										44
									
								
								src/content/referrer.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/content/referrer.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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<string>;
 | 
			
		||||
    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<string>{
 | 
			
		||||
            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);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<string>{
 | 
			
		||||
        return "a";
 | 
			
		||||
        super(path,desc);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
registerContentReferrer(VideoReferrer);
 | 
			
		||||
							
								
								
									
										58
									
								
								src/route/all.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/route/all.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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<ContentContext,DefaultContext>,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<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 combination.concat(cur.stack);
 | 
			
		||||
    },[]);
 | 
			
		||||
    return await compose(chain)(ctx,next);
 | 
			
		||||
};
 | 
			
		||||
export class AllContentRouter extends Router<ContentContext>{
 | 
			
		||||
    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);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										5
									
								
								src/route/context.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/route/context.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
import {ContentReferrer} from '../content/referrer';
 | 
			
		||||
 | 
			
		||||
export interface ContentContext{
 | 
			
		||||
    content:ContentReferrer
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								src/route/manga.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/route/manga.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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<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 MangaRouter extends Router<ContentContext>{
 | 
			
		||||
    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;
 | 
			
		||||
							
								
								
									
										65
									
								
								src/route/video.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/route/video.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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<ContentContext>{
 | 
			
		||||
    constructor(){
 | 
			
		||||
        super();
 | 
			
		||||
        this.get("/", async (ctx,next)=>{
 | 
			
		||||
            await renderVideo(ctx,ctx.state.content.path);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default VideoRouter;
 | 
			
		||||
| 
						 | 
				
			
			@ -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++}`);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue