Files
carmanagement/apps/api/src/lib/storage.ts
T
2026-06-10 00:40:19 -04:00

143 lines
4.5 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import crypto from 'crypto'
const APP_PACKAGE_ROOT = path.resolve(__dirname, '..', '..', '..')
const APP_SOURCE_ROOT = path.join(APP_PACKAGE_ROOT, 'src')
const APP_DIST_ROOT = path.join(APP_PACKAGE_ROOT, 'dist')
const DEFAULT_STORAGE_ROOT = path.join(APP_PACKAGE_ROOT, 'storage')
const LEGACY_STORAGE_ROOTS = [
path.join(APP_SOURCE_ROOT, 'lib', 'storage'),
]
export type StorageVisibility = 'public' | 'private'
export function getStorageRoot(): string {
return process.env.FILE_STORAGE_ROOT
? path.resolve(process.env.FILE_STORAGE_ROOT)
: DEFAULT_STORAGE_ROOT
}
export function getPublicStorageRoot(): string {
return path.join(getStorageRoot(), 'public')
}
export function getPrivateStorageRoot(): string {
return path.join(getStorageRoot(), 'private')
}
export function getLegacyStorageRoots(): string[] {
return LEGACY_STORAGE_ROOTS
}
function isWithinPath(targetPath: string, parentPath: string): boolean {
const relative = path.relative(parentPath, targetPath)
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
}
export function assertStorageConfiguration(): string {
const storageRoot = getStorageRoot()
if (process.env.NODE_ENV === 'production' && !process.env.FILE_STORAGE_ROOT) {
throw new Error('FILE_STORAGE_ROOT must be set in production so uploads are stored on the mounted volume.')
}
if (process.env.NODE_ENV === 'production') {
const forbiddenRoots = [APP_PACKAGE_ROOT, APP_SOURCE_ROOT, APP_DIST_ROOT]
const invalidRoot = forbiddenRoots.find((root) => isWithinPath(storageRoot, root))
if (invalidRoot) {
throw new Error(
`FILE_STORAGE_ROOT must point outside the API app tree in production. Received ${storageRoot}, which is inside ${invalidRoot}.`
)
}
}
return storageRoot
}
function ensureStorageRoot(visibility: StorageVisibility): string {
assertStorageConfiguration()
const root = visibility === 'public' ? getPublicStorageRoot() : getPrivateStorageRoot()
fs.mkdirSync(root, { recursive: true })
return root
}
function inferVisibility(folder: string): StorageVisibility {
const normalized = folder.replace(/\\/g, '/')
if (/\/customers\//.test(`/${normalized}/`)) return 'private'
if (/\/licenses?\//.test(`/${normalized}/`)) return 'private'
if (/\/contracts?\//.test(`/${normalized}/`)) return 'private'
if (/\/documents?\//.test(`/${normalized}/`)) return 'private'
if (/\/inspections?\//.test(`/${normalized}/`)) return 'private'
if (/\/internal\//.test(`/${normalized}/`)) return 'private'
return 'public'
}
function getApiBase(): string {
return (process.env.API_URL ?? 'http://localhost:4000').replace(/\/$/, '')
}
function getDashboardBase(): string {
return (
process.env.DASHBOARD_URL ??
process.env.NEXT_PUBLIC_DASHBOARD_URL ??
'http://localhost:3000/dashboard'
).replace(/\/$/, '')
}
function getStorageRelativePath(imageUrl: string): string | null {
const marker = '/storage/'
const idx = imageUrl.indexOf(marker)
if (idx === -1) return null
return imageUrl.slice(idx + marker.length)
}
export function getProtectedCustomerLicenseImageUrl(customerId: string): string {
return `${getDashboardBase()}/api/v1/customers/${customerId}/license-image`
}
export async function uploadImage(
buffer: Buffer,
folder: string,
publicId?: string,
visibility: StorageVisibility = inferVisibility(folder),
): Promise<string> {
const storageRoot = ensureStorageRoot(visibility)
const folderPath = path.join(storageRoot, folder)
if (!isWithinPath(folderPath, storageRoot)) {
throw new Error('Upload path escapes storage root')
}
fs.mkdirSync(folderPath, { recursive: true })
const filename = publicId
? `${publicId}.jpg`
: `${crypto.randomBytes(16).toString('hex')}.jpg`
fs.writeFileSync(path.join(folderPath, filename), buffer)
return `${getApiBase()}/storage/${folder}/${filename}`
}
export function resolveStoredFilePath(imageUrl: string): string | null {
const relative = getStorageRelativePath(imageUrl)
if (!relative) return null
const roots = [getPublicStorageRoot(), getPrivateStorageRoot(), ...getLegacyStorageRoots(), assertStorageConfiguration()]
for (const root of roots) {
const filePath = path.join(root, relative)
if (!isWithinPath(filePath, root)) continue
if (fs.existsSync(filePath)) {
return filePath
}
}
return null
}
export async function deleteImage(imageUrl: string): Promise<void> {
const filePath = resolveStoredFilePath(imageUrl)
if (filePath && fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
}