100 lines
3.5 KiB
JavaScript
100 lines
3.5 KiB
JavaScript
import path from 'node:path';
|
|
import { createImgproxySigner } from './imgproxy-signer.mjs';
|
|
|
|
export const PUBLIC_IMAGES_DIR = 'images';
|
|
|
|
let cachedSigner = null;
|
|
let signerInitAttempted = false;
|
|
|
|
export function resolveImgproxyBaseUrl(env = process.env) {
|
|
return (env.IMGPROXY_BASE_URL ?? 'http://localhost:20081').replace(/\/$/, '');
|
|
}
|
|
|
|
export function getImgproxySigner(env = process.env) {
|
|
if (signerInitAttempted) return cachedSigner;
|
|
signerInitAttempted = true;
|
|
const key = env.IMGPROXY_SIGNING_KEY?.trim();
|
|
const salt = env.IMGPROXY_SIGNING_SALT?.trim();
|
|
if (!key || !salt) return null;
|
|
try {
|
|
cachedSigner = createImgproxySigner(key, salt);
|
|
} catch {
|
|
cachedSigner = null;
|
|
}
|
|
return cachedSigner;
|
|
}
|
|
|
|
export function formatImageDateFolder(date = new Date()) {
|
|
const value = date instanceof Date ? date : new Date(date);
|
|
const year = value.getFullYear();
|
|
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
const day = String(value.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function extensionForMime(mimeType, fallbackFilename = '') {
|
|
const ext = path.extname(String(fallbackFilename)).toLowerCase();
|
|
if (ext && /^\.[a-z0-9]{1,8}$/.test(ext)) return ext === '.jpeg' ? '.jpg' : ext;
|
|
if (mimeType === 'image/jpeg') return '.jpg';
|
|
if (mimeType === 'image/png') return '.png';
|
|
if (mimeType === 'image/webp') return '.webp';
|
|
if (mimeType === 'image/gif') return '.gif';
|
|
return '.img';
|
|
}
|
|
|
|
function sanitizeImageBasename(filename, assetId) {
|
|
const raw = path.basename(String(filename ?? ''), path.extname(String(filename ?? ''))).trim();
|
|
const normalized = raw
|
|
.normalize('NFKC')
|
|
.replace(/[^\w\u4e00-\u9fff.-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
.slice(0, 80);
|
|
return normalized || assetId.slice(0, 8);
|
|
}
|
|
|
|
export function buildPublicImagePaths({
|
|
userId,
|
|
assetId,
|
|
mimeType,
|
|
originalFilename,
|
|
date = new Date(),
|
|
uniqueSuffix = null,
|
|
}) {
|
|
const dateFolder = formatImageDateFolder(date);
|
|
const extension = extensionForMime(mimeType, originalFilename);
|
|
const basename = sanitizeImageBasename(originalFilename, assetId);
|
|
const suffix = uniqueSuffix ? `-${uniqueSuffix}` : '';
|
|
const filename = `${basename}${suffix}${extension}`;
|
|
const storageKey = path.posix.join('users', userId, PUBLIC_IMAGES_DIR, dateFolder, filename);
|
|
const workspaceRelativePath = path.posix.join('public', PUBLIC_IMAGES_DIR, dateFolder, filename);
|
|
return { storageKey, workspaceRelativePath, filename, dateFolder };
|
|
}
|
|
|
|
export function buildImgproxyDisplayUrl(storageKey, options = {}) {
|
|
const baseUrl = (options.baseUrl ?? resolveImgproxyBaseUrl()).replace(/\/$/, '');
|
|
const signer = options.signer ?? getImgproxySigner();
|
|
if (signer) {
|
|
return signer.buildUrl(baseUrl, storageKey, options.preset ?? 'display');
|
|
}
|
|
const preset = options.preset ?? 'display';
|
|
if (preset === 'vision') {
|
|
return `${baseUrl}/unsafe/rs:fit:768:768:0/q:60/plain/local:///${storageKey}@jpg`;
|
|
}
|
|
if (preset === 'thumb') {
|
|
return `${baseUrl}/unsafe/rs:fit:256:256:0/q:70/plain/local:///${storageKey}@jpg`;
|
|
}
|
|
return `${baseUrl}/unsafe/rs:fit:1280:1280:0/q:85/plain/local:///${storageKey}@jpg`;
|
|
}
|
|
|
|
export function buildUserImagePublicUrl({ userId, storageKey, mimeType, originalFilename }) {
|
|
void userId;
|
|
void mimeType;
|
|
void originalFilename;
|
|
return buildImgproxyDisplayUrl(storageKey);
|
|
}
|
|
|
|
export function isPublicImageStorageKey(storageKey) {
|
|
const normalized = String(storageKey ?? '').replace(/\\/g, '/');
|
|
return /\/images\/\d{4}-\d{2}-\d{2}\//.test(normalized);
|
|
}
|