Flatten note tags and store processed media as ordinary resource links
wiki CI and deployment / verify-and-deploy (push) Successful in 13m27s
wiki CI and deployment / verify-and-deploy (push) Successful in 13m27s
This commit is contained in:
@@ -8,7 +8,7 @@ export default async function handler(req:NextApiRequest,res:NextApiResponse){
|
||||
try{
|
||||
const variant=String(req.query.variant||'');
|
||||
if(variant&&!['poster','silent_video','audio_track'].includes(variant))return res.status(404).end();
|
||||
const result=variant?await db().query('SELECT storage_key,mime_type FROM asset_variants WHERE asset_id=$1 AND role=$2',[req.query.id,variant]):await db().query('SELECT storage_key,mime_type FROM assets WHERE id=$1',[req.query.id]);
|
||||
const result=variant?await db().query('SELECT storage_key,mime_type FROM assets WHERE source_asset_id=$1 AND derivation=$2',[req.query.id,variant]):await db().query('SELECT storage_key,mime_type FROM assets WHERE id=$1',[req.query.id]);
|
||||
if(!result.rows[0]?.storage_key)return res.status(404).end();
|
||||
const object=await getObject(result.rows[0].storage_key,req.headers.range);
|
||||
res.status(object.ContentRange?206:200);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type {NextApiRequest,NextApiResponse} from "next";
|
||||
import {snapshotState,queueSnapshots} from "@/lib/snapshots";
|
||||
import {sameOrigin,apiError} from "@/lib/api";
|
||||
export default async function handler(req:NextApiRequest,res:NextApiResponse){
|
||||
res.setHeader('Cache-Control','no-store');
|
||||
if(!['GET','POST'].includes(req.method||''))return res.status(405).end();
|
||||
try{const state=await snapshotState(String(req.query.slug));if(!state)return res.status(404).end();if(req.method==='POST'){sameOrigin(req);const page=req.body?.page;if(page!==undefined&&(!Number.isInteger(page)||page<1||page>8))return res.status(400).json({error:'页码应为 1—8'});await queueSnapshots(state.postId,page);return res.status(202).json({queued:true});}return res.json(state);}catch(e){return apiError(res,e);}
|
||||
}
|
||||
@@ -2,5 +2,5 @@ import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import {db} from '@/lib/db';
|
||||
import {getObject} from '@/lib/storage';
|
||||
import {previewWidth,cachedPreview,loadStaticPreview} from '@/lib/preview';
|
||||
export default async function handler(req:NextApiRequest,res:NextApiResponse){if(!['GET','HEAD'].includes(req.method||''))return res.status(405).end();const width=previewWidth(req.query.width);if(!width||typeof req.query.source!=='string')return res.status(400).end();try{const source=req.query.source;let input:{identity:string;load:()=>Promise<Buffer>};if(source.startsWith('/api/media/')){const match=source.match(/^\/api\/media\/([0-9a-f-]{36})(?:\?variant=(poster))?$/);if(!match)return res.status(400).end();const result=match[2]?await db().query('SELECT storage_key,mime_type FROM asset_variants WHERE asset_id=$1 AND role=$2',[match[1],match[2]]):await db().query('SELECT storage_key,mime_type FROM assets WHERE id=$1',[match[1]]);const row=result.rows[0];if(!row?.storage_key||!row.mime_type?.startsWith('image/'))return res.status(404).end();input={identity:row.storage_key,load:async()=>{const object=await getObject(row.storage_key);if((object.ContentLength||0)>64*1024*1024)throw Error('Image too large');const chunks:Buffer[]=[];let length=0;for await(const chunk of object.Body as AsyncIterable<Uint8Array>){length+=chunk.length;if(length>64*1024*1024)throw Error('Image too large');chunks.push(Buffer.from(chunk));}return Buffer.concat(chunks);}};}else input=await loadStaticPreview(source);const {bytes,etag}=await cachedPreview(input.identity,width,input.load);res.setHeader('ETag',etag);res.setHeader('Cache-Control','public, max-age=3600');res.setHeader('Content-Type','image/webp');res.setHeader('X-Content-Type-Options','nosniff');if(req.headers['if-none-match']===etag)return res.status(304).end();res.setHeader('Content-Length',bytes.length);return req.method==='HEAD'?res.end():res.send(bytes);}catch(e){const message=(e as Error).message;if(message.includes('Invalid source'))return res.status(400).end();if((e as NodeJS.ErrnoException).code==='ENOENT')return res.status(404).end();res.setHeader('Retry-After','5');return res.status(503).end();}}
|
||||
export default async function handler(req:NextApiRequest,res:NextApiResponse){if(!['GET','HEAD'].includes(req.method||''))return res.status(405).end();const width=previewWidth(req.query.width);if(!width||typeof req.query.source!=='string')return res.status(400).end();try{const source=req.query.source;let input:{identity:string;load:()=>Promise<Buffer>};if(source.startsWith('/api/media/')){const match=source.match(/^\/api\/media\/([0-9a-f-]{36})(?:\?variant=(poster))?$/);if(!match)return res.status(400).end();const result=match[2]?await db().query('SELECT storage_key,mime_type FROM assets WHERE source_asset_id=$1 AND derivation=$2',[match[1],match[2]]):await db().query('SELECT storage_key,mime_type FROM assets WHERE id=$1',[match[1]]);const row=result.rows[0];if(!row?.storage_key||!row.mime_type?.startsWith('image/'))return res.status(404).end();input={identity:row.storage_key,load:async()=>{const object=await getObject(row.storage_key);if((object.ContentLength||0)>64*1024*1024)throw Error('Image too large');const chunks:Buffer[]=[];let length=0;for await(const chunk of object.Body as AsyncIterable<Uint8Array>){length+=chunk.length;if(length>64*1024*1024)throw Error('Image too large');chunks.push(Buffer.from(chunk));}return Buffer.concat(chunks);}};}else input=await loadStaticPreview(source);const {bytes,etag}=await cachedPreview(input.identity,width,input.load);res.setHeader('ETag',etag);res.setHeader('Cache-Control','public, max-age=3600');res.setHeader('Content-Type','image/webp');res.setHeader('X-Content-Type-Options','nosniff');if(req.headers['if-none-match']===etag)return res.status(304).end();res.setHeader('Content-Length',bytes.length);return req.method==='HEAD'?res.end():res.send(bytes);}catch(e){const message=(e as Error).message;if(message.includes('Invalid source'))return res.status(400).end();if((e as NodeJS.ErrnoException).code==='ENOENT')return res.status(404).end();res.setHeader('Retry-After','5');return res.status(503).end();}}
|
||||
export const config={api:{responseLimit:false}};
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import type { NextApiRequest,NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { db } from "@/lib/db";
|
||||
import { tags } from "@/lib/library";
|
||||
import { sameOrigin,apiError } from "@/lib/api";
|
||||
import type {NextApiRequest,NextApiResponse} from 'next';
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import {z} from 'zod';
|
||||
import {db} from '@/lib/db';
|
||||
import {tags} from '@/lib/library';
|
||||
import {sameOrigin,apiError} from '@/lib/api';
|
||||
export default async function handler(req:NextApiRequest,res:NextApiResponse){
|
||||
try{
|
||||
if(req.method==='GET')return res.json(await tags());
|
||||
if(req.method!=='POST')return res.status(405).end();
|
||||
sameOrigin(req);
|
||||
const data=z.object({name:z.string().trim().min(1).max(60),slug:z.string().regex(/^[a-z0-9][a-z0-9-]{0,59}$/),parentId:z.uuid().nullable().default(null)}).parse(req.body);
|
||||
const id=randomUUID(); await db().query('INSERT INTO tags(id,name,slug,parent_id) VALUES($1,$2,$3,$4)',[id,data.name,data.slug,data.parentId]);return res.status(201).json({id});
|
||||
const data=z.object({name:z.string().trim().transform(v=>v.replace(/^#+/,'').trim()).pipe(z.string().min(1).max(60).regex(/^[^#\s,,]+$/)),slug:z.string().regex(/^[a-z0-9][a-z0-9-]{0,59}$/).optional()}).parse(req.body);
|
||||
const id=randomUUID();
|
||||
const result=await db().query(`INSERT INTO tags(id,name,slug) VALUES($1,$2,$3)
|
||||
ON CONFLICT(lower(btrim(name))) DO UPDATE SET name=tags.name RETURNING *,slug AS path`,[id,data.name,data.slug||'tag-'+id]);
|
||||
return res.status(result.rows[0].id===id?201:200).json(result.rows[0]);
|
||||
}catch(e){return apiError(res,e);}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user