| |
| |
|
|
| import { ENV } from './_core/env'; |
|
|
| type StorageConfig = { baseUrl: string; apiKey: string }; |
|
|
| function getStorageConfig(): StorageConfig { |
| const baseUrl = ENV.forgeApiUrl; |
| const apiKey = ENV.forgeApiKey; |
|
|
| if (!baseUrl || !apiKey) { |
| throw new Error( |
| "Storage proxy credentials missing: set BUILT_IN_FORGE_API_URL and BUILT_IN_FORGE_API_KEY" |
| ); |
| } |
|
|
| return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey }; |
| } |
|
|
| function buildUploadUrl(baseUrl: string, relKey: string): URL { |
| const url = new URL("v1/storage/upload", ensureTrailingSlash(baseUrl)); |
| url.searchParams.set("path", normalizeKey(relKey)); |
| return url; |
| } |
|
|
| async function buildDownloadUrl( |
| baseUrl: string, |
| relKey: string, |
| apiKey: string |
| ): Promise<string> { |
| const downloadApiUrl = new URL( |
| "v1/storage/downloadUrl", |
| ensureTrailingSlash(baseUrl) |
| ); |
| downloadApiUrl.searchParams.set("path", normalizeKey(relKey)); |
| const response = await fetch(downloadApiUrl, { |
| method: "GET", |
| headers: buildAuthHeaders(apiKey), |
| }); |
| return (await response.json()).url; |
| } |
|
|
| function ensureTrailingSlash(value: string): string { |
| return value.endsWith("/") ? value : `${value}/`; |
| } |
|
|
| function normalizeKey(relKey: string): string { |
| return relKey.replace(/^\/+/, ""); |
| } |
|
|
| function appendHashSuffix(relKey: string): string { |
| const hash = crypto.randomUUID().replace(/-/g, "").slice(0, 8); |
| const segmentStart = relKey.lastIndexOf("/"); |
| const lastDot = relKey.lastIndexOf("."); |
| if (lastDot === -1 || lastDot <= segmentStart) return `${relKey}_${hash}`; |
| return `${relKey.slice(0, lastDot)}_${hash}${relKey.slice(lastDot)}`; |
| } |
|
|
| function toFormData( |
| data: Buffer | Uint8Array | string, |
| contentType: string, |
| fileName: string |
| ): FormData { |
| const blob = |
| typeof data === "string" |
| ? new Blob([data], { type: contentType }) |
| : new Blob([data as any], { type: contentType }); |
| const form = new FormData(); |
| form.append("file", blob, fileName || "file"); |
| return form; |
| } |
|
|
| function buildAuthHeaders(apiKey: string): HeadersInit { |
| return { Authorization: `Bearer ${apiKey}` }; |
| } |
|
|
| export async function storagePut( |
| relKey: string, |
| data: Buffer | Uint8Array | string, |
| contentType = "application/octet-stream" |
| ): Promise<{ key: string; url: string }> { |
| const { baseUrl, apiKey } = getStorageConfig(); |
| const key = appendHashSuffix(normalizeKey(relKey)); |
| const uploadUrl = buildUploadUrl(baseUrl, key); |
| const formData = toFormData(data, contentType, key.split("/").pop() ?? key); |
| const response = await fetch(uploadUrl, { |
| method: "POST", |
| headers: buildAuthHeaders(apiKey), |
| body: formData, |
| }); |
|
|
| if (!response.ok) { |
| const message = await response.text().catch(() => response.statusText); |
| throw new Error( |
| `Storage upload failed (${response.status} ${response.statusText}): ${message}` |
| ); |
| } |
| const url = (await response.json()).url; |
| return { key, url }; |
| } |
|
|
| export async function storageGet(relKey: string): Promise<{ key: string; url: string; }> { |
| const { baseUrl, apiKey } = getStorageConfig(); |
| const key = normalizeKey(relKey); |
| return { |
| key, |
| url: await buildDownloadUrl(baseUrl, key, apiKey), |
| }; |
| } |
|
|