Files
memind/user-image-url.mjs
2026-06-29 22:20:04 +08:00

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);
}