storage.ts 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. // Preconfigured storage helpers for Manus WebDev templates
  2. // Uses the Biz-provided storage proxy (Authorization: Bearer <token>)
  3. import { ENV } from './_core/env';
  4. type StorageConfig = { baseUrl: string; apiKey: string };
  5. function getStorageConfig(): StorageConfig {
  6. const baseUrl = ENV.forgeApiUrl;
  7. const apiKey = ENV.forgeApiKey;
  8. if (!baseUrl || !apiKey) {
  9. throw new Error(
  10. "Storage proxy credentials missing: set BUILT_IN_FORGE_API_URL and BUILT_IN_FORGE_API_KEY"
  11. );
  12. }
  13. return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey };
  14. }
  15. function buildUploadUrl(baseUrl: string, relKey: string): URL {
  16. const url = new URL("v1/storage/upload", ensureTrailingSlash(baseUrl));
  17. url.searchParams.set("path", normalizeKey(relKey));
  18. return url;
  19. }
  20. async function buildDownloadUrl(
  21. baseUrl: string,
  22. relKey: string,
  23. apiKey: string
  24. ): Promise<string> {
  25. const downloadApiUrl = new URL(
  26. "v1/storage/downloadUrl",
  27. ensureTrailingSlash(baseUrl)
  28. );
  29. downloadApiUrl.searchParams.set("path", normalizeKey(relKey));
  30. const response = await fetch(downloadApiUrl, {
  31. method: "GET",
  32. headers: buildAuthHeaders(apiKey),
  33. });
  34. return (await response.json()).url;
  35. }
  36. function ensureTrailingSlash(value: string): string {
  37. return value.endsWith("/") ? value : `${value}/`;
  38. }
  39. function normalizeKey(relKey: string): string {
  40. return relKey.replace(/^\/+/, "");
  41. }
  42. function toFormData(
  43. data: Buffer | Uint8Array | string,
  44. contentType: string,
  45. fileName: string
  46. ): FormData {
  47. const blob =
  48. typeof data === "string"
  49. ? new Blob([data], { type: contentType })
  50. : new Blob([data as any], { type: contentType });
  51. const form = new FormData();
  52. form.append("file", blob, fileName || "file");
  53. return form;
  54. }
  55. function buildAuthHeaders(apiKey: string): HeadersInit {
  56. return { Authorization: `Bearer ${apiKey}` };
  57. }
  58. export async function storagePut(
  59. relKey: string,
  60. data: Buffer | Uint8Array | string,
  61. contentType = "application/octet-stream"
  62. ): Promise<{ key: string; url: string }> {
  63. const { baseUrl, apiKey } = getStorageConfig();
  64. const key = normalizeKey(relKey);
  65. const uploadUrl = buildUploadUrl(baseUrl, key);
  66. const formData = toFormData(data, contentType, key.split("/").pop() ?? key);
  67. const response = await fetch(uploadUrl, {
  68. method: "POST",
  69. headers: buildAuthHeaders(apiKey),
  70. body: formData,
  71. });
  72. if (!response.ok) {
  73. const message = await response.text().catch(() => response.statusText);
  74. throw new Error(
  75. `Storage upload failed (${response.status} ${response.statusText}): ${message}`
  76. );
  77. }
  78. const url = (await response.json()).url;
  79. return { key, url };
  80. }
  81. export async function storageGet(relKey: string): Promise<{ key: string; url: string; }> {
  82. const { baseUrl, apiKey } = getStorageConfig();
  83. const key = normalizeKey(relKey);
  84. return {
  85. key,
  86. url: await buildDownloadUrl(baseUrl, key, apiKey),
  87. };
  88. }