first commit

Made-with: Cursor
This commit is contained in:
Michilis
2026-04-01 02:46:53 +00:00
commit 76210db03d
126 changed files with 20208 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import sharp from 'sharp';
const STORAGE_PATH = process.env.MEDIA_STORAGE_PATH
? path.resolve(process.env.MEDIA_STORAGE_PATH)
: path.resolve(process.cwd(), '../storage/media');
const CACHE_PATH = path.join(STORAGE_PATH, 'cache');
const CACHE_HEADERS = {
'Cache-Control': 'public, max-age=31536000, immutable',
};
interface MediaMeta {
mimeType: string;
type: 'image' | 'video';
size: number;
}
function readMeta(id: string): MediaMeta | null {
const metaPath = path.join(STORAGE_PATH, `${id}.json`);
try {
const raw = fs.readFileSync(metaPath, 'utf-8');
return JSON.parse(raw);
} catch {
return null;
}
}
function fileExists(filePath: string): boolean {
try {
fs.accessSync(filePath, fs.constants.R_OK);
return true;
} catch {
return false;
}
}
async function handleImageResize(
filePath: string,
width: number,
meta: MediaMeta,
id: string
): Promise<NextResponse> {
fs.mkdirSync(CACHE_PATH, { recursive: true });
const cacheKey = `${id}_w${width}`;
const cachedPath = path.join(CACHE_PATH, cacheKey);
if (fileExists(cachedPath)) {
const cached = fs.readFileSync(cachedPath);
return new NextResponse(new Uint8Array(cached), {
status: 200,
headers: {
'Content-Type': meta.mimeType,
'Content-Length': String(cached.length),
...CACHE_HEADERS,
},
});
}
const buffer = fs.readFileSync(filePath);
const resized = await sharp(buffer)
.resize({ width, withoutEnlargement: true })
.toBuffer();
fs.writeFileSync(cachedPath, resized);
return new NextResponse(new Uint8Array(resized), {
status: 200,
headers: {
'Content-Type': meta.mimeType,
'Content-Length': String(resized.length),
...CACHE_HEADERS,
},
});
}
function handleVideoStream(
filePath: string,
meta: MediaMeta,
rangeHeader: string | null
): NextResponse {
const stat = fs.statSync(filePath);
const fileSize = stat.size;
if (rangeHeader) {
const parts = rangeHeader.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunkSize = end - start + 1;
const stream = fs.createReadStream(filePath, { start, end });
const readable = new ReadableStream({
start(controller) {
stream.on('data', (chunk: string | Buffer) => controller.enqueue(chunk as Buffer));
stream.on('end', () => controller.close());
stream.on('error', (err) => controller.error(err));
},
});
return new NextResponse(readable as any, {
status: 206,
headers: {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': String(chunkSize),
'Content-Type': meta.mimeType,
...CACHE_HEADERS,
},
});
}
const stream = fs.createReadStream(filePath);
const readable = new ReadableStream({
start(controller) {
stream.on('data', (chunk: string | Buffer) => controller.enqueue(chunk as Buffer));
stream.on('end', () => controller.close());
stream.on('error', (err) => controller.error(err));
},
});
return new NextResponse(readable as any, {
status: 200,
headers: {
'Accept-Ranges': 'bytes',
'Content-Length': String(fileSize),
'Content-Type': meta.mimeType,
...CACHE_HEADERS,
},
});
}
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const { id } = params;
const filePath = path.join(STORAGE_PATH, id);
if (!fileExists(filePath)) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
const meta = readMeta(id);
if (!meta) {
return NextResponse.json({ error: 'Metadata not found' }, { status: 404 });
}
const { searchParams } = new URL(request.url);
const widthParam = searchParams.get('w');
if (meta.type === 'image' && widthParam) {
const width = parseInt(widthParam, 10);
if (isNaN(width) || width < 1 || width > 4096) {
return NextResponse.json({ error: 'Invalid width' }, { status: 400 });
}
return handleImageResize(filePath, width, meta, id);
}
if (meta.type === 'video') {
const rangeHeader = request.headers.get('range');
return handleVideoStream(filePath, meta, rangeHeader);
}
// Full image, no resize
const buffer = fs.readFileSync(filePath);
return new NextResponse(new Uint8Array(buffer), {
status: 200,
headers: {
'Content-Type': meta.mimeType,
'Content-Length': String(buffer.length),
...CACHE_HEADERS,
},
});
}