build 1
This commit is contained in:
91
backend/src/services/image.ts
Normal file
91
backend/src/services/image.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import sharp from 'sharp';
|
||||
import fs from 'fs';
|
||||
import { absolutePath, ensureDir } from './storage.js';
|
||||
|
||||
export interface ImageMeta {
|
||||
width: number;
|
||||
height: number;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export async function extractMeta(filePath: string): Promise<ImageMeta> {
|
||||
const abs = absolutePath(filePath);
|
||||
const meta = await sharp(abs).metadata();
|
||||
const stat = fs.statSync(abs);
|
||||
|
||||
const mimeMap: Record<string, string> = {
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
};
|
||||
|
||||
return {
|
||||
width: meta.width ?? 0,
|
||||
height: meta.height ?? 0,
|
||||
mimeType: mimeMap[meta.format ?? ''] ?? 'image/jpeg',
|
||||
size: stat.size,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveBuffer(buffer: Buffer, destRelPath: string): Promise<void> {
|
||||
ensureDir(destRelPath);
|
||||
const abs = absolutePath(destRelPath);
|
||||
fs.writeFileSync(abs, buffer);
|
||||
}
|
||||
|
||||
export interface ResizeOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
quality?: number;
|
||||
}
|
||||
|
||||
export async function resizeImage(
|
||||
srcRelPath: string,
|
||||
destRelPath: string,
|
||||
options: ResizeOptions
|
||||
): Promise<ImageMeta> {
|
||||
const srcAbs = absolutePath(srcRelPath);
|
||||
ensureDir(destRelPath);
|
||||
const destAbs = absolutePath(destRelPath);
|
||||
|
||||
const src = await sharp(srcAbs).metadata();
|
||||
const isGif = src.format === 'gif';
|
||||
|
||||
let pipeline = sharp(srcAbs, { animated: isGif });
|
||||
|
||||
if (options.width || options.height) {
|
||||
pipeline = pipeline.resize({
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isGif && options.quality) {
|
||||
if (src.format === 'jpeg') pipeline = pipeline.jpeg({ quality: options.quality });
|
||||
else if (src.format === 'png') pipeline = pipeline.png({ quality: options.quality });
|
||||
else if (src.format === 'webp') pipeline = pipeline.webp({ quality: options.quality });
|
||||
}
|
||||
|
||||
await pipeline.toFile(destAbs);
|
||||
|
||||
const resultMeta = await sharp(destAbs).metadata();
|
||||
const stat = fs.statSync(destAbs);
|
||||
|
||||
const mimeMap: Record<string, string> = {
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
};
|
||||
|
||||
return {
|
||||
width: resultMeta.width ?? 0,
|
||||
height: resultMeta.height ?? 0,
|
||||
mimeType: mimeMap[resultMeta.format ?? ''] ?? 'image/jpeg',
|
||||
size: stat.size,
|
||||
};
|
||||
}
|
||||
49
backend/src/services/storage.ts
Normal file
49
backend/src/services/storage.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR ?? '/data';
|
||||
export const IMAGES_DIR = path.join(DATA_DIR, 'images');
|
||||
|
||||
export function ensureImagesDir(): void {
|
||||
fs.mkdirSync(IMAGES_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
export function getMonthDir(): string {
|
||||
const now = new Date();
|
||||
const yyyy = now.getFullYear();
|
||||
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
||||
return `${yyyy}-${mm}`;
|
||||
}
|
||||
|
||||
export function buildFilePath(id: string, ext: string, label?: string): string {
|
||||
const monthDir = getMonthDir();
|
||||
const suffix = label ? `-${label}` : '';
|
||||
const filename = `${id}${suffix}.${ext}`;
|
||||
return path.join(monthDir, filename);
|
||||
}
|
||||
|
||||
export function absolutePath(relativePath: string): string {
|
||||
return path.join(IMAGES_DIR, relativePath);
|
||||
}
|
||||
|
||||
export function ensureDir(relativePath: string): void {
|
||||
const dir = path.dirname(absolutePath(relativePath));
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
export function deleteFile(relativePath: string): void {
|
||||
const abs = absolutePath(relativePath);
|
||||
if (fs.existsSync(abs)) {
|
||||
fs.unlinkSync(abs);
|
||||
}
|
||||
}
|
||||
|
||||
export function getExtension(mimeType: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/webp': 'webp',
|
||||
};
|
||||
return map[mimeType] ?? 'jpg';
|
||||
}
|
||||
Reference in New Issue
Block a user