refractor code,
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export function isDatabaseUnavailableError(error: unknown): boolean {
|
||||
if (!error || typeof error !== 'object') return false
|
||||
const candidate = error as { code?: string; message?: string }
|
||||
return candidate.code === 'P1001' || candidate.message?.includes("Can't reach database server") === true
|
||||
}
|
||||
+76
-10
@@ -2,7 +2,13 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const DEFAULT_STORAGE_ROOT = path.resolve(__dirname, '..', '..', 'storage')
|
||||
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 function getStorageRoot(): string {
|
||||
return process.env.FILE_STORAGE_ROOT
|
||||
@@ -10,8 +16,37 @@ export function getStorageRoot(): string {
|
||||
: DEFAULT_STORAGE_ROOT
|
||||
}
|
||||
|
||||
function ensureStorageRoot(): string {
|
||||
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(): string {
|
||||
const storageRoot = assertStorageConfiguration()
|
||||
fs.mkdirSync(storageRoot, { recursive: true })
|
||||
return storageRoot
|
||||
}
|
||||
@@ -20,6 +55,25 @@ 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:3001/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,
|
||||
@@ -38,12 +92,24 @@ export async function uploadImage(
|
||||
return `${getApiBase()}/storage/${folder}/${filename}`
|
||||
}
|
||||
|
||||
export async function deleteImage(imageUrl: string): Promise<void> {
|
||||
const storageRoot = getStorageRoot()
|
||||
const marker = '/storage/'
|
||||
const idx = imageUrl.indexOf(marker)
|
||||
if (idx === -1) return
|
||||
const relative = imageUrl.slice(idx + marker.length)
|
||||
const filePath = path.join(storageRoot, relative)
|
||||
if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
|
||||
export function resolveStoredFilePath(imageUrl: string): string | null {
|
||||
const relative = getStorageRelativePath(imageUrl)
|
||||
if (!relative) return null
|
||||
|
||||
const roots = [assertStorageConfiguration(), ...getLegacyStorageRoots()]
|
||||
for (const root of roots) {
|
||||
const filePath = path.join(root, relative)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user