@openinary/core 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/LICENSE +661 -0
  2. package/dist/config/deps.d.ts +14 -0
  3. package/dist/config/deps.d.ts.map +1 -0
  4. package/dist/config/deps.js +2 -0
  5. package/dist/config/deps.js.map +1 -0
  6. package/dist/index.d.ts +27 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +28 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/routes/authenticated.d.ts +4 -0
  11. package/dist/routes/authenticated.d.ts.map +1 -0
  12. package/dist/routes/authenticated.js +130 -0
  13. package/dist/routes/authenticated.js.map +1 -0
  14. package/dist/routes/download-folder.d.ts +4 -0
  15. package/dist/routes/download-folder.d.ts.map +1 -0
  16. package/dist/routes/download-folder.js +101 -0
  17. package/dist/routes/download-folder.js.map +1 -0
  18. package/dist/routes/download-zip.d.ts +15 -0
  19. package/dist/routes/download-zip.d.ts.map +1 -0
  20. package/dist/routes/download-zip.js +147 -0
  21. package/dist/routes/download-zip.js.map +1 -0
  22. package/dist/routes/download.d.ts +4 -0
  23. package/dist/routes/download.d.ts.map +1 -0
  24. package/dist/routes/download.js +83 -0
  25. package/dist/routes/download.js.map +1 -0
  26. package/dist/routes/invalidate.d.ts +4 -0
  27. package/dist/routes/invalidate.d.ts.map +1 -0
  28. package/dist/routes/invalidate.js +114 -0
  29. package/dist/routes/invalidate.js.map +1 -0
  30. package/dist/routes/queue-events.d.ts +4 -0
  31. package/dist/routes/queue-events.d.ts.map +1 -0
  32. package/dist/routes/queue-events.js +125 -0
  33. package/dist/routes/queue-events.js.map +1 -0
  34. package/dist/routes/queue.d.ts +4 -0
  35. package/dist/routes/queue.d.ts.map +1 -0
  36. package/dist/routes/queue.js +130 -0
  37. package/dist/routes/queue.js.map +1 -0
  38. package/dist/routes/storage.d.ts +4 -0
  39. package/dist/routes/storage.d.ts.map +1 -0
  40. package/dist/routes/storage.js +694 -0
  41. package/dist/routes/storage.js.map +1 -0
  42. package/dist/routes/transform-helpers.d.ts +62 -0
  43. package/dist/routes/transform-helpers.d.ts.map +1 -0
  44. package/dist/routes/transform-helpers.js +372 -0
  45. package/dist/routes/transform-helpers.js.map +1 -0
  46. package/dist/routes/transform.d.ts +4 -0
  47. package/dist/routes/transform.d.ts.map +1 -0
  48. package/dist/routes/transform.js +65 -0
  49. package/dist/routes/transform.js.map +1 -0
  50. package/dist/routes/video-status.d.ts +4 -0
  51. package/dist/routes/video-status.d.ts.map +1 -0
  52. package/dist/routes/video-status.js +208 -0
  53. package/dist/routes/video-status.js.map +1 -0
  54. package/dist/services/transform.service.d.ts +71 -0
  55. package/dist/services/transform.service.d.ts.map +1 -0
  56. package/dist/services/transform.service.js +362 -0
  57. package/dist/services/transform.service.js.map +1 -0
  58. package/dist/types.d.ts +82 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/utils/asset-deletion.d.ts +16 -0
  63. package/dist/utils/asset-deletion.d.ts.map +1 -0
  64. package/dist/utils/asset-deletion.js +131 -0
  65. package/dist/utils/asset-deletion.js.map +1 -0
  66. package/dist/utils/cache.d.ts +44 -0
  67. package/dist/utils/cache.d.ts.map +1 -0
  68. package/dist/utils/cache.js +248 -0
  69. package/dist/utils/cache.js.map +1 -0
  70. package/dist/utils/get-unique-file-path.d.ts +8 -0
  71. package/dist/utils/get-unique-file-path.d.ts.map +1 -0
  72. package/dist/utils/get-unique-file-path.js +26 -0
  73. package/dist/utils/get-unique-file-path.js.map +1 -0
  74. package/dist/utils/image/aspect-ratio.d.ts +7 -0
  75. package/dist/utils/image/aspect-ratio.d.ts.map +1 -0
  76. package/dist/utils/image/aspect-ratio.js +40 -0
  77. package/dist/utils/image/aspect-ratio.js.map +1 -0
  78. package/dist/utils/image/background.d.ts +6 -0
  79. package/dist/utils/image/background.d.ts.map +1 -0
  80. package/dist/utils/image/background.js +22 -0
  81. package/dist/utils/image/background.js.map +1 -0
  82. package/dist/utils/image/compression.d.ts +44 -0
  83. package/dist/utils/image/compression.d.ts.map +1 -0
  84. package/dist/utils/image/compression.js +345 -0
  85. package/dist/utils/image/compression.js.map +1 -0
  86. package/dist/utils/image/gravity.d.ts +6 -0
  87. package/dist/utils/image/gravity.d.ts.map +1 -0
  88. package/dist/utils/image/gravity.js +24 -0
  89. package/dist/utils/image/gravity.js.map +1 -0
  90. package/dist/utils/image/index.d.ts +8 -0
  91. package/dist/utils/image/index.d.ts.map +1 -0
  92. package/dist/utils/image/index.js +77 -0
  93. package/dist/utils/image/index.js.map +1 -0
  94. package/dist/utils/image/param-registry.d.ts +63 -0
  95. package/dist/utils/image/param-registry.d.ts.map +1 -0
  96. package/dist/utils/image/param-registry.js +196 -0
  97. package/dist/utils/image/param-registry.js.map +1 -0
  98. package/dist/utils/image/quality.d.ts +8 -0
  99. package/dist/utils/image/quality.d.ts.map +1 -0
  100. package/dist/utils/image/quality.js +9 -0
  101. package/dist/utils/image/quality.js.map +1 -0
  102. package/dist/utils/image/resize.d.ts +7 -0
  103. package/dist/utils/image/resize.d.ts.map +1 -0
  104. package/dist/utils/image/resize.js +62 -0
  105. package/dist/utils/image/resize.js.map +1 -0
  106. package/dist/utils/image/rotation.d.ts +6 -0
  107. package/dist/utils/image/rotation.d.ts.map +1 -0
  108. package/dist/utils/image/rotation.js +20 -0
  109. package/dist/utils/image/rotation.js.map +1 -0
  110. package/dist/utils/image/round-corners.d.ts +12 -0
  111. package/dist/utils/image/round-corners.d.ts.map +1 -0
  112. package/dist/utils/image/round-corners.js +102 -0
  113. package/dist/utils/image/round-corners.js.map +1 -0
  114. package/dist/utils/image/types.d.ts +2 -0
  115. package/dist/utils/image/types.d.ts.map +1 -0
  116. package/dist/utils/image/types.js +2 -0
  117. package/dist/utils/image/types.js.map +1 -0
  118. package/dist/utils/logger.d.ts +21 -0
  119. package/dist/utils/logger.d.ts.map +1 -0
  120. package/dist/utils/logger.js +82 -0
  121. package/dist/utils/logger.js.map +1 -0
  122. package/dist/utils/parser.d.ts +8 -0
  123. package/dist/utils/parser.d.ts.map +1 -0
  124. package/dist/utils/parser.js +226 -0
  125. package/dist/utils/parser.js.map +1 -0
  126. package/dist/utils/signature.d.ts +35 -0
  127. package/dist/utils/signature.d.ts.map +1 -0
  128. package/dist/utils/signature.js +100 -0
  129. package/dist/utils/signature.js.map +1 -0
  130. package/dist/utils/storage/cache.d.ts +41 -0
  131. package/dist/utils/storage/cache.d.ts.map +1 -0
  132. package/dist/utils/storage/cache.js +90 -0
  133. package/dist/utils/storage/cache.js.map +1 -0
  134. package/dist/utils/storage/cloud-storage.d.ts +188 -0
  135. package/dist/utils/storage/cloud-storage.d.ts.map +1 -0
  136. package/dist/utils/storage/cloud-storage.js +514 -0
  137. package/dist/utils/storage/cloud-storage.js.map +1 -0
  138. package/dist/utils/storage/factory.d.ts +10 -0
  139. package/dist/utils/storage/factory.d.ts.map +1 -0
  140. package/dist/utils/storage/factory.js +17 -0
  141. package/dist/utils/storage/factory.js.map +1 -0
  142. package/dist/utils/storage/index.d.ts +7 -0
  143. package/dist/utils/storage/index.d.ts.map +1 -0
  144. package/dist/utils/storage/index.js +8 -0
  145. package/dist/utils/storage/index.js.map +1 -0
  146. package/dist/utils/storage/key-generator.d.ts +11 -0
  147. package/dist/utils/storage/key-generator.d.ts.map +1 -0
  148. package/dist/utils/storage/key-generator.js +23 -0
  149. package/dist/utils/storage/key-generator.js.map +1 -0
  150. package/dist/utils/storage/listing-cache.d.ts +14 -0
  151. package/dist/utils/storage/listing-cache.d.ts.map +1 -0
  152. package/dist/utils/storage/listing-cache.js +29 -0
  153. package/dist/utils/storage/listing-cache.js.map +1 -0
  154. package/dist/utils/storage/s3-client.d.ts +97 -0
  155. package/dist/utils/storage/s3-client.d.ts.map +1 -0
  156. package/dist/utils/storage/s3-client.js +297 -0
  157. package/dist/utils/storage/s3-client.js.map +1 -0
  158. package/dist/utils/storage/stats-tracker.d.ts +45 -0
  159. package/dist/utils/storage/stats-tracker.d.ts.map +1 -0
  160. package/dist/utils/storage/stats-tracker.js +97 -0
  161. package/dist/utils/storage/stats-tracker.js.map +1 -0
  162. package/dist/utils/storage/types.d.ts +2 -0
  163. package/dist/utils/storage/types.d.ts.map +1 -0
  164. package/dist/utils/storage/types.js +2 -0
  165. package/dist/utils/storage/types.js.map +1 -0
  166. package/dist/utils/storage-level.d.ts +66 -0
  167. package/dist/utils/storage-level.d.ts.map +1 -0
  168. package/dist/utils/storage-level.js +106 -0
  169. package/dist/utils/storage-level.js.map +1 -0
  170. package/dist/utils/storage-tree.d.ts +30 -0
  171. package/dist/utils/storage-tree.d.ts.map +1 -0
  172. package/dist/utils/storage-tree.js +81 -0
  173. package/dist/utils/storage-tree.js.map +1 -0
  174. package/dist/utils/upload-signature.d.ts +25 -0
  175. package/dist/utils/upload-signature.d.ts.map +1 -0
  176. package/dist/utils/upload-signature.js +56 -0
  177. package/dist/utils/upload-signature.js.map +1 -0
  178. package/dist/utils/video/auto-downscale.d.ts +12 -0
  179. package/dist/utils/video/auto-downscale.d.ts.map +1 -0
  180. package/dist/utils/video/auto-downscale.js +29 -0
  181. package/dist/utils/video/auto-downscale.js.map +1 -0
  182. package/dist/utils/video/command-builder.d.ts +22 -0
  183. package/dist/utils/video/command-builder.d.ts.map +1 -0
  184. package/dist/utils/video/command-builder.js +79 -0
  185. package/dist/utils/video/command-builder.js.map +1 -0
  186. package/dist/utils/video/config.d.ts +51 -0
  187. package/dist/utils/video/config.d.ts.map +1 -0
  188. package/dist/utils/video/config.js +170 -0
  189. package/dist/utils/video/config.js.map +1 -0
  190. package/dist/utils/video/format.d.ts +22 -0
  191. package/dist/utils/video/format.d.ts.map +1 -0
  192. package/dist/utils/video/format.js +64 -0
  193. package/dist/utils/video/format.js.map +1 -0
  194. package/dist/utils/video/index.d.ts +25 -0
  195. package/dist/utils/video/index.d.ts.map +1 -0
  196. package/dist/utils/video/index.js +104 -0
  197. package/dist/utils/video/index.js.map +1 -0
  198. package/dist/utils/video/param-registry.d.ts +64 -0
  199. package/dist/utils/video/param-registry.d.ts.map +1 -0
  200. package/dist/utils/video/param-registry.js +201 -0
  201. package/dist/utils/video/param-registry.js.map +1 -0
  202. package/dist/utils/video/quality.d.ts +12 -0
  203. package/dist/utils/video/quality.d.ts.map +1 -0
  204. package/dist/utils/video/quality.js +45 -0
  205. package/dist/utils/video/quality.js.map +1 -0
  206. package/dist/utils/video/queue-store.d.ts +51 -0
  207. package/dist/utils/video/queue-store.d.ts.map +1 -0
  208. package/dist/utils/video/queue-store.js +2 -0
  209. package/dist/utils/video/queue-store.js.map +1 -0
  210. package/dist/utils/video/resize.d.ts +7 -0
  211. package/dist/utils/video/resize.d.ts.map +1 -0
  212. package/dist/utils/video/resize.js +40 -0
  213. package/dist/utils/video/resize.js.map +1 -0
  214. package/dist/utils/video/sqlite-video-job-store.d.ts +29 -0
  215. package/dist/utils/video/sqlite-video-job-store.d.ts.map +1 -0
  216. package/dist/utils/video/sqlite-video-job-store.js +320 -0
  217. package/dist/utils/video/sqlite-video-job-store.js.map +1 -0
  218. package/dist/utils/video/thumbnail.d.ts +7 -0
  219. package/dist/utils/video/thumbnail.d.ts.map +1 -0
  220. package/dist/utils/video/thumbnail.js +22 -0
  221. package/dist/utils/video/thumbnail.js.map +1 -0
  222. package/dist/utils/video/trim.d.ts +7 -0
  223. package/dist/utils/video/trim.d.ts.map +1 -0
  224. package/dist/utils/video/trim.js +36 -0
  225. package/dist/utils/video/trim.js.map +1 -0
  226. package/dist/utils/video/types.d.ts +19 -0
  227. package/dist/utils/video/types.d.ts.map +1 -0
  228. package/dist/utils/video/types.js +2 -0
  229. package/dist/utils/video/types.js.map +1 -0
  230. package/dist/utils/video/video-info.d.ts +25 -0
  231. package/dist/utils/video/video-info.d.ts.map +1 -0
  232. package/dist/utils/video/video-info.js +58 -0
  233. package/dist/utils/video/video-info.js.map +1 -0
  234. package/dist/utils/video/video-worker.d.ts +52 -0
  235. package/dist/utils/video/video-worker.d.ts.map +1 -0
  236. package/dist/utils/video/video-worker.js +192 -0
  237. package/dist/utils/video/video-worker.js.map +1 -0
  238. package/dist/utils/video-job-queue.d.ts +62 -0
  239. package/dist/utils/video-job-queue.d.ts.map +1 -0
  240. package/dist/utils/video-job-queue.js +122 -0
  241. package/dist/utils/video-job-queue.js.map +1 -0
  242. package/package.json +51 -0
@@ -0,0 +1,694 @@
1
+ import { Hono } from "hono";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import logger, { serializeError } from "../utils/logger.js";
5
+ import { deleteAssetCompletely } from "../utils/asset-deletion.js";
6
+ import { getUniqueFilePath } from "../utils/get-unique-file-path.js";
7
+ import { deleteCachedFiles, getCacheSize, clearAllCache } from "../utils/cache.js";
8
+ import { sumTreeSize } from "../utils/storage-tree.js";
9
+ import { getBucketStats, recalculateBucketStats, } from "../utils/storage/stats-tracker.js";
10
+ import { FOLDER_PREVIEW_LIMIT, FOLDER_SUMMARY_MAX_KEYS, deriveFolderPaths, getMediaType, normalizeLevelPath, } from "../utils/storage-level.js";
11
+ import { getCachedFullListing, invalidateListingCache, } from "../utils/storage/listing-cache.js";
12
+ export function createStorageRoute(deps) {
13
+ const { storage: storageClient, queue } = deps;
14
+ const storageRoute = new Hono();
15
+ /**
16
+ * Full recursive listing of public/ objects (keys relative, prefix stripped),
17
+ * shared between /stats and /folders through a short-lived TTL cache
18
+ */
19
+ async function getPublicObjects(client) {
20
+ const objects = await getCachedFullListing(() => client.listAllParallel("public/"));
21
+ return objects
22
+ .filter((obj) => obj.key.startsWith("public/"))
23
+ .map((obj) => ({ ...obj, key: obj.key.substring(7) }));
24
+ }
25
+ function buildLocalTree(rootDir) {
26
+ const root = {
27
+ name: "storage",
28
+ path: "",
29
+ type: "directory",
30
+ children: [],
31
+ };
32
+ if (!fs.existsSync(rootDir)) {
33
+ return root;
34
+ }
35
+ const walk = (absoluteDir, relativeDir, parent) => {
36
+ const entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
37
+ for (const entry of entries) {
38
+ const relPath = relativeDir
39
+ ? `${relativeDir}/${entry.name}`
40
+ : entry.name;
41
+ const fullPath = path.join(absoluteDir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ const dirNode = {
44
+ name: entry.name,
45
+ path: relPath,
46
+ type: "directory",
47
+ children: [],
48
+ };
49
+ parent.children = parent.children || [];
50
+ parent.children.push(dirNode);
51
+ walk(fullPath, relPath, dirNode);
52
+ }
53
+ else if (entry.isFile()) {
54
+ const stats = fs.statSync(fullPath);
55
+ const fileNode = {
56
+ name: entry.name,
57
+ path: relPath,
58
+ type: "file",
59
+ size: stats.size,
60
+ mtime: stats.mtime.toISOString(),
61
+ };
62
+ parent.children = parent.children || [];
63
+ parent.children.push(fileNode);
64
+ }
65
+ }
66
+ };
67
+ walk(rootDir, "", root);
68
+ return root;
69
+ }
70
+ function localFolderSummary(dirAbs, relPath) {
71
+ let entries;
72
+ try {
73
+ entries = fs.readdirSync(dirAbs, { withFileTypes: true });
74
+ }
75
+ catch {
76
+ return { itemCount: 0, truncated: false, previewItems: [] };
77
+ }
78
+ const truncated = entries.length > FOLDER_SUMMARY_MAX_KEYS;
79
+ const limited = entries.slice(0, FOLDER_SUMMARY_MAX_KEYS);
80
+ const previewItems = [];
81
+ for (const entry of limited) {
82
+ if (previewItems.length >= FOLDER_PREVIEW_LIMIT)
83
+ break;
84
+ if (!entry.isFile())
85
+ continue;
86
+ const type = getMediaType(entry.name);
87
+ if (type) {
88
+ previewItems.push({ path: `${relPath}/${entry.name}`, type });
89
+ }
90
+ }
91
+ return { itemCount: limited.length, truncated, previewItems };
92
+ }
93
+ function listLocalLevel(folderPath) {
94
+ const dirAbs = path.join(".", "public", folderPath);
95
+ if (!fs.existsSync(dirAbs) || !fs.statSync(dirAbs).isDirectory()) {
96
+ return { folders: [], files: [] };
97
+ }
98
+ const folders = [];
99
+ const files = [];
100
+ for (const entry of fs.readdirSync(dirAbs, { withFileTypes: true })) {
101
+ const relPath = folderPath ? `${folderPath}/${entry.name}` : entry.name;
102
+ if (entry.isDirectory()) {
103
+ folders.push({ name: entry.name, path: relPath });
104
+ }
105
+ else if (entry.isFile()) {
106
+ const stats = fs.statSync(path.join(dirAbs, entry.name));
107
+ files.push({
108
+ name: entry.name,
109
+ path: relPath,
110
+ size: stats.size,
111
+ mtime: stats.mtime.toISOString(),
112
+ });
113
+ }
114
+ }
115
+ folders.sort((a, b) => a.name.localeCompare(b.name));
116
+ files.sort((a, b) => a.name.localeCompare(b.name));
117
+ return { folders, files };
118
+ }
119
+ /**
120
+ * List one directory level
121
+ * GET /storage?path=<folder> (empty or missing path = root)
122
+ * Returns the level's files plus each subfolder's name/path only - no
123
+ * per-folder summary (item count, preview thumbnails), which the client
124
+ * fetches separately via GET /storage/folder-summaries for the folders it
125
+ * actually renders. Keeps this endpoint's latency independent of how many
126
+ * subfolders a level has.
127
+ */
128
+ storageRoute.get("/", async (c) => {
129
+ const levelPath = normalizeLevelPath(c.req.query("path") ?? "");
130
+ if (levelPath === null) {
131
+ return c.json({ error: "Bad request", message: "Invalid path" }, 400);
132
+ }
133
+ try {
134
+ if (!storageClient) {
135
+ return c.json({ path: levelPath, ...listLocalLevel(levelPath) });
136
+ }
137
+ const { folderNames, files } = await storageClient.listLevel(levelPath);
138
+ const folders = folderNames.map((name) => ({
139
+ name,
140
+ path: levelPath ? `${levelPath}/${name}` : name,
141
+ }));
142
+ return c.json({ path: levelPath, folders, files });
143
+ }
144
+ catch (error) {
145
+ logger.error({ error: serializeError(error), levelPath }, "Failed to list storage contents");
146
+ return c.json({
147
+ error: "Failed to list storage contents",
148
+ }, 500);
149
+ }
150
+ });
151
+ const FOLDER_SUMMARIES_MAX_PATHS = 200;
152
+ /**
153
+ * Bounded per-folder summaries (item count, truncation flag, preview
154
+ * thumbnails) for an explicit set of folder paths.
155
+ * GET /storage/folder-summaries?paths=<comma-separated folder paths>
156
+ * Meant to be called only for folders the client is actually rendering
157
+ * (e.g. the currently virtualized rows), so a level with many subfolders
158
+ * doesn't pay for every summary up front - see GET /storage/.
159
+ * Note: This route must be registered before GET "/*"
160
+ */
161
+ storageRoute.get("/folder-summaries", async (c) => {
162
+ const raw = c.req.query("paths") ?? "";
163
+ const requested = raw
164
+ .split(",")
165
+ .map((p) => p.trim())
166
+ .filter(Boolean);
167
+ if (requested.length === 0) {
168
+ return c.json({ summaries: {} });
169
+ }
170
+ if (requested.length > FOLDER_SUMMARIES_MAX_PATHS) {
171
+ return c.json({
172
+ error: "Bad request",
173
+ message: `Too many paths (max ${FOLDER_SUMMARIES_MAX_PATHS})`,
174
+ }, 400);
175
+ }
176
+ const paths = [];
177
+ for (const p of requested) {
178
+ const normalized = normalizeLevelPath(p);
179
+ if (normalized === null) {
180
+ return c.json({ error: "Bad request", message: "Invalid path" }, 400);
181
+ }
182
+ paths.push(normalized);
183
+ }
184
+ try {
185
+ const summaries = {};
186
+ const batchSize = 16;
187
+ for (let i = 0; i < paths.length; i += batchSize) {
188
+ const batch = paths.slice(i, i + batchSize);
189
+ const results = await Promise.all(batch.map(async (folderPath) => {
190
+ if (storageClient) {
191
+ return [
192
+ folderPath,
193
+ await storageClient.getFolderSummary(folderPath),
194
+ ];
195
+ }
196
+ const dirAbs = path.join(".", "public", folderPath);
197
+ return [
198
+ folderPath,
199
+ localFolderSummary(dirAbs, folderPath),
200
+ ];
201
+ }));
202
+ for (const [folderPath, summary] of results) {
203
+ summaries[folderPath] = summary;
204
+ }
205
+ }
206
+ return c.json({ summaries });
207
+ }
208
+ catch (error) {
209
+ logger.error({ error: serializeError(error), paths }, "Failed to get folder summaries");
210
+ return c.json({ error: "Failed to get folder summaries" }, 500);
211
+ }
212
+ });
213
+ /**
214
+ * Builds the stats response body from tracked cloud aggregates (O(1) read,
215
+ * no bucket listing) or a local filesystem walk when running without cloud
216
+ * storage. Local disk cache is always live-computed (cheap readdir).
217
+ */
218
+ async function buildStatsResponse(cloudStats) {
219
+ const localCache = await getCacheSize();
220
+ if (cloudStats) {
221
+ return {
222
+ storage: cloudStats.storage,
223
+ cache: {
224
+ size: localCache.size + cloudStats.cache.size,
225
+ fileCount: localCache.fileCount + cloudStats.cache.fileCount,
226
+ },
227
+ updatedAt: cloudStats.updatedAt,
228
+ };
229
+ }
230
+ const root = buildLocalTree(path.join(".", "public"));
231
+ const { size, fileCount } = sumTreeSize(root);
232
+ return {
233
+ storage: { size, fileCount },
234
+ cache: localCache,
235
+ updatedAt: new Date().toISOString(),
236
+ };
237
+ }
238
+ /**
239
+ * Get aggregate storage and cache stats
240
+ * GET /storage/stats
241
+ * Cloud stats come from incrementally-tracked counters persisted in the
242
+ * bucket - no full listing on the read path (see stats-tracker.ts). The
243
+ * first-ever call seeds them with one full recomputation.
244
+ * Note: This route must be registered before GET "/*"
245
+ */
246
+ storageRoute.get("/stats", async (c) => {
247
+ try {
248
+ const cloudStats = storageClient
249
+ ? await getBucketStats(storageClient)
250
+ : null;
251
+ return c.json(await buildStatsResponse(cloudStats));
252
+ }
253
+ catch (error) {
254
+ logger.error({ error: serializeError(error) }, "Failed to get storage stats");
255
+ return c.json({
256
+ error: "Failed to get storage stats",
257
+ }, 500);
258
+ }
259
+ });
260
+ /**
261
+ * Recompute aggregate stats from full bucket listings, reconciling any
262
+ * drift in the incremental counters (external bucket changes, lost flush)
263
+ * POST /storage/stats/recalculate
264
+ * Note: This route must be registered before POST "/*"
265
+ */
266
+ storageRoute.post("/stats/recalculate", async (c) => {
267
+ try {
268
+ const cloudStats = storageClient
269
+ ? await recalculateBucketStats(storageClient)
270
+ : null;
271
+ return c.json(await buildStatsResponse(cloudStats));
272
+ }
273
+ catch (error) {
274
+ logger.error({ error: serializeError(error) }, "Failed to recalculate storage stats");
275
+ return c.json({
276
+ error: "Failed to recalculate storage stats",
277
+ }, 500);
278
+ }
279
+ });
280
+ /**
281
+ * List every folder path in storage (for "Move to" pickers)
282
+ * GET /storage/folders
283
+ * Note: This route must be registered before GET "/*"
284
+ */
285
+ storageRoute.get("/folders", async (c) => {
286
+ try {
287
+ if (storageClient) {
288
+ const publicObjects = await getPublicObjects(storageClient);
289
+ return c.json({
290
+ folders: deriveFolderPaths(publicObjects.map((obj) => obj.key)),
291
+ });
292
+ }
293
+ const folders = [];
294
+ const walk = (absoluteDir, relativeDir) => {
295
+ if (!fs.existsSync(absoluteDir))
296
+ return;
297
+ for (const entry of fs.readdirSync(absoluteDir, {
298
+ withFileTypes: true,
299
+ })) {
300
+ if (!entry.isDirectory())
301
+ continue;
302
+ const relPath = relativeDir
303
+ ? `${relativeDir}/${entry.name}`
304
+ : entry.name;
305
+ folders.push(relPath);
306
+ walk(path.join(absoluteDir, entry.name), relPath);
307
+ }
308
+ };
309
+ walk(path.join(".", "public"), "");
310
+ return c.json({ folders: folders.sort((a, b) => a.localeCompare(b)) });
311
+ }
312
+ catch (error) {
313
+ logger.error({ error: serializeError(error) }, "Failed to list storage folders");
314
+ return c.json({
315
+ error: "Failed to list storage folders",
316
+ }, 500);
317
+ }
318
+ });
319
+ /**
320
+ * Clear all cached (transformed) files, local and cloud
321
+ * POST /storage/cache/clear
322
+ * Note: This route must be registered before POST "/*"
323
+ */
324
+ storageRoute.post("/cache/clear", async (c) => {
325
+ try {
326
+ const localDeleted = await clearAllCache();
327
+ const cloudDeleted = storageClient
328
+ ? await storageClient.clearAllCache()
329
+ : 0;
330
+ return c.json({
331
+ success: true,
332
+ message: "Cache cleared successfully",
333
+ details: {
334
+ localCacheFilesDeleted: localDeleted,
335
+ cloudCacheFilesDeleted: cloudDeleted,
336
+ },
337
+ });
338
+ }
339
+ catch (error) {
340
+ logger.error({ error: serializeError(error) }, "Failed to clear cache");
341
+ return c.json({
342
+ error: "Internal server error",
343
+ message: error instanceof Error ? error.message : "Unknown error",
344
+ }, 500);
345
+ }
346
+ });
347
+ /**
348
+ * Get file metadata (size, dates)
349
+ * GET /storage/{path}/metadata
350
+ * Note: This route must be placed after GET "/" but before DELETE "/*"
351
+ */
352
+ storageRoute.get("/*", async (c) => {
353
+ const requestPath = c.req.path;
354
+ // Only handle requests that end with /metadata
355
+ if (!requestPath.endsWith("/metadata")) {
356
+ // Let other routes handle this request
357
+ return c.notFound();
358
+ }
359
+ // Remove '/storage' prefix and '/metadata' suffix
360
+ // requestPath will be something like '/storage/cows/black.png/metadata'
361
+ // We need to extract 'cows/black.png'
362
+ const pathWithoutPrefix = requestPath
363
+ .replace(/^\/storage\/?/, "")
364
+ .replace(/\/metadata$/, "");
365
+ if (!pathWithoutPrefix) {
366
+ return c.json({
367
+ error: "Bad request",
368
+ message: "File path is required",
369
+ }, 400);
370
+ }
371
+ let filePath = pathWithoutPrefix.replace(/^\/+/, "").replace(/\/+$/, "");
372
+ try {
373
+ filePath = decodeURIComponent(filePath);
374
+ }
375
+ catch (error) {
376
+ // If decoding fails, use the original path
377
+ }
378
+ try {
379
+ if (storageClient) {
380
+ const metadata = await storageClient.getOriginalMetadata(filePath);
381
+ if (!metadata) {
382
+ return c.json({
383
+ error: "Not found",
384
+ message: "File not found",
385
+ }, 404);
386
+ }
387
+ return c.json({
388
+ size: metadata.size,
389
+ createdAt: metadata.createdAt.toISOString(),
390
+ updatedAt: metadata.updatedAt.toISOString(),
391
+ });
392
+ }
393
+ else {
394
+ const localPath = path.join(".", "public", filePath);
395
+ if (!fs.existsSync(localPath)) {
396
+ return c.json({
397
+ error: "Not found",
398
+ message: "File not found",
399
+ }, 404);
400
+ }
401
+ const stats = fs.statSync(localPath);
402
+ if (stats.isDirectory()) {
403
+ return c.json({
404
+ error: "Bad request",
405
+ message: "Cannot get metadata for directories",
406
+ }, 400);
407
+ }
408
+ return c.json({
409
+ size: stats.size,
410
+ createdAt: stats.birthtime.toISOString(),
411
+ updatedAt: stats.mtime.toISOString(),
412
+ });
413
+ }
414
+ }
415
+ catch (error) {
416
+ logger.error({ error: serializeError(error), filePath }, "Failed to get file metadata");
417
+ return c.json({
418
+ error: "Internal server error",
419
+ message: error instanceof Error ? error.message : "Unknown error",
420
+ }, 500);
421
+ }
422
+ });
423
+ /**
424
+ * Delete a file from storage
425
+ * DELETE /storage/*
426
+ * This now performs a complete deletion including cache and jobs
427
+ */
428
+ storageRoute.delete("/*", async (c) => {
429
+ const requestPath = c.req.path;
430
+ // Remove '/storage' prefix from the path
431
+ // requestPath will be something like '/storage/cows/black.png'
432
+ // We need to extract 'cows/black.png'
433
+ const pathWithoutPrefix = requestPath.replace(/^\/storage\/?/, "");
434
+ if (!pathWithoutPrefix) {
435
+ return c.json({
436
+ error: "Bad request",
437
+ message: "File path is required",
438
+ }, 400);
439
+ }
440
+ let filePath = pathWithoutPrefix.replace(/^\/+/, "").replace(/\/+$/, "");
441
+ try {
442
+ filePath = decodeURIComponent(filePath);
443
+ }
444
+ catch (error) {
445
+ // If decoding fails, use the original path
446
+ }
447
+ try {
448
+ // Use the complete asset deletion function
449
+ const result = await deleteAssetCompletely(filePath, storageClient, queue.getStore());
450
+ if (result.success || result.originalFileDeleted) {
451
+ invalidateListingCache();
452
+ }
453
+ if (!result.success) {
454
+ // Check if the file was not found
455
+ if (result.errors.some((err) => err.includes("not found"))) {
456
+ return c.json({
457
+ error: "Not found",
458
+ message: "File not found",
459
+ }, 404);
460
+ }
461
+ // Check if trying to delete a directory
462
+ if (result.errors.some((err) => err.includes("Cannot delete directories"))) {
463
+ return c.json({
464
+ error: "Bad request",
465
+ message: "Cannot delete directories",
466
+ }, 400);
467
+ }
468
+ // Other errors
469
+ return c.json({
470
+ error: "Partial deletion",
471
+ message: "Asset deleted but some cleanup operations failed",
472
+ details: {
473
+ originalFileDeleted: result.originalFileDeleted,
474
+ jobsDeleted: result.jobsDeleted,
475
+ localCacheFilesDeleted: result.localCacheFilesDeleted,
476
+ cloudCacheFilesDeleted: result.cloudCacheFilesDeleted,
477
+ errors: result.errors,
478
+ },
479
+ }, result.originalFileDeleted ? 200 : 500);
480
+ }
481
+ return c.json({
482
+ success: true,
483
+ message: "Asset deleted successfully",
484
+ details: {
485
+ jobsDeleted: result.jobsDeleted,
486
+ localCacheFilesDeleted: result.localCacheFilesDeleted,
487
+ cloudCacheFilesDeleted: result.cloudCacheFilesDeleted,
488
+ },
489
+ });
490
+ }
491
+ catch (error) {
492
+ logger.error({ error: serializeError(error), filePath }, "Failed to delete asset");
493
+ return c.json({
494
+ error: "Internal server error",
495
+ message: error instanceof Error ? error.message : "Unknown error",
496
+ }, 500);
497
+ }
498
+ });
499
+ async function pathExists(filePath) {
500
+ if (storageClient) {
501
+ return storageClient.existsOriginalPath(filePath);
502
+ }
503
+ return fs.existsSync(path.join(".", "public", filePath));
504
+ }
505
+ async function isDirectoryPath(filePath) {
506
+ if (storageClient) {
507
+ return storageClient.folderExists(filePath);
508
+ }
509
+ const localPath = path.join(".", "public", filePath);
510
+ return fs.existsSync(localPath) && fs.statSync(localPath).isDirectory();
511
+ }
512
+ function extractFilePath(requestPath, suffix) {
513
+ let pathWithoutPrefix = requestPath.replace(/^\/storage\/?/, "");
514
+ if (suffix) {
515
+ pathWithoutPrefix = pathWithoutPrefix.replace(suffix, "");
516
+ }
517
+ let filePath = pathWithoutPrefix.replace(/^\/+/, "").replace(/\/+$/, "");
518
+ try {
519
+ filePath = decodeURIComponent(filePath);
520
+ }
521
+ catch {
522
+ // If decoding fails, use the original path
523
+ }
524
+ return filePath;
525
+ }
526
+ /**
527
+ * Rename a file in place
528
+ * PATCH /storage/* with body { name: string }
529
+ */
530
+ storageRoute.patch("/*", async (c) => {
531
+ const filePath = extractFilePath(c.req.path);
532
+ if (!filePath) {
533
+ return c.json({ error: "Bad request", message: "File path is required" }, 400);
534
+ }
535
+ let body = {};
536
+ try {
537
+ body = await c.req.json();
538
+ }
539
+ catch {
540
+ return c.json({ error: "Bad request", message: "Invalid JSON body" }, 400);
541
+ }
542
+ const newName = typeof body.name === "string" ? body.name.trim() : "";
543
+ if (!newName || newName.includes("/") || newName.includes("\\")) {
544
+ return c.json({ error: "Bad request", message: "A valid name is required" }, 400);
545
+ }
546
+ const dir = path.posix.dirname(filePath);
547
+ const newPath = dir === "." ? newName : `${dir}/${newName}`;
548
+ try {
549
+ const isFile = await pathExists(filePath);
550
+ const isFolder = !isFile && (await isDirectoryPath(filePath));
551
+ if (!isFile && !isFolder) {
552
+ return c.json({ error: "Not found", message: "File or folder not found" }, 404);
553
+ }
554
+ if (newPath !== filePath) {
555
+ const targetIsFile = await pathExists(newPath);
556
+ const targetIsFolder = !targetIsFile && (await isDirectoryPath(newPath));
557
+ if (targetIsFile || targetIsFolder) {
558
+ return c.json({
559
+ error: "Conflict",
560
+ message: "A file or folder with that name already exists",
561
+ }, 409);
562
+ }
563
+ }
564
+ if (isFolder && storageClient) {
565
+ await storageClient.renameFolder(filePath, newPath);
566
+ storageClient.invalidateAllCacheEntries(filePath);
567
+ }
568
+ else if (storageClient) {
569
+ await storageClient.renameOriginal(filePath, newPath);
570
+ storageClient.invalidateAllCacheEntries(filePath);
571
+ }
572
+ else {
573
+ const sourceAbs = path.join(".", "public", filePath);
574
+ const destAbs = path.join(".", "public", newPath);
575
+ fs.renameSync(sourceAbs, destAbs);
576
+ }
577
+ await deleteCachedFiles(filePath);
578
+ invalidateListingCache();
579
+ return c.json({ success: true, path: newPath });
580
+ }
581
+ catch (error) {
582
+ logger.error({ error: serializeError(error), filePath, newPath }, "Failed to rename asset");
583
+ return c.json({
584
+ error: "Internal server error",
585
+ message: error instanceof Error ? error.message : "Unknown error",
586
+ }, 500);
587
+ }
588
+ });
589
+ /**
590
+ * Copy or move a file
591
+ * POST /storage/*\/copy
592
+ * POST /storage/*\/move with body { destination?: string }
593
+ */
594
+ storageRoute.post("/*", async (c) => {
595
+ const requestPath = c.req.path;
596
+ const isCopy = requestPath.endsWith("/copy");
597
+ const isMove = requestPath.endsWith("/move");
598
+ if (!isCopy && !isMove) {
599
+ return c.notFound();
600
+ }
601
+ const filePath = extractFilePath(requestPath, /\/(copy|move)$/);
602
+ if (!filePath) {
603
+ return c.json({ error: "Bad request", message: "File path is required" }, 400);
604
+ }
605
+ try {
606
+ const isFile = await pathExists(filePath);
607
+ const isFolder = !isFile && isMove && (await isDirectoryPath(filePath));
608
+ if (!isFile && !isFolder) {
609
+ return c.json({ error: "Not found", message: "File not found" }, 404);
610
+ }
611
+ const dir = path.posix.dirname(filePath);
612
+ const normalizedDir = dir === "." ? "" : dir;
613
+ const baseName = path.posix.basename(filePath);
614
+ let targetDir = normalizedDir;
615
+ if (isMove) {
616
+ let body = {};
617
+ try {
618
+ body = await c.req.json();
619
+ }
620
+ catch {
621
+ // No body means move to root
622
+ }
623
+ targetDir =
624
+ typeof body.destination === "string"
625
+ ? body.destination.trim().replace(/^\/+|\/+$/g, "")
626
+ : "";
627
+ if (targetDir === normalizedDir) {
628
+ return c.json({ error: "Bad request", message: "File is already in that folder" }, 400);
629
+ }
630
+ }
631
+ if (isFolder) {
632
+ const newPath = targetDir ? `${targetDir}/${baseName}` : baseName;
633
+ if (storageClient) {
634
+ await storageClient.renameFolder(filePath, newPath);
635
+ storageClient.invalidateAllCacheEntries(filePath);
636
+ }
637
+ else {
638
+ const sourceAbs = path.join(".", "public", filePath);
639
+ const destAbs = path.join(".", "public", newPath);
640
+ fs.mkdirSync(path.dirname(destAbs), { recursive: true });
641
+ fs.renameSync(sourceAbs, destAbs);
642
+ }
643
+ await deleteCachedFiles(filePath);
644
+ invalidateListingCache();
645
+ return c.json({ success: true, path: newPath });
646
+ }
647
+ let candidatePath;
648
+ if (isCopy) {
649
+ const ext = path.posix.extname(baseName);
650
+ const nameWithoutExt = ext ? baseName.slice(0, -ext.length) : baseName;
651
+ const copyName = `${nameWithoutExt} copy${ext}`;
652
+ candidatePath = targetDir ? `${targetDir}/${copyName}` : copyName;
653
+ }
654
+ else {
655
+ candidatePath = targetDir ? `${targetDir}/${baseName}` : baseName;
656
+ }
657
+ const newPath = await getUniqueFilePath(candidatePath, pathExists);
658
+ if (storageClient) {
659
+ if (isCopy) {
660
+ await storageClient.copyOriginal(filePath, newPath);
661
+ }
662
+ else {
663
+ await storageClient.renameOriginal(filePath, newPath);
664
+ storageClient.invalidateAllCacheEntries(filePath);
665
+ }
666
+ }
667
+ else {
668
+ const sourceAbs = path.join(".", "public", filePath);
669
+ const destAbs = path.join(".", "public", newPath);
670
+ fs.mkdirSync(path.dirname(destAbs), { recursive: true });
671
+ if (isCopy) {
672
+ fs.copyFileSync(sourceAbs, destAbs);
673
+ }
674
+ else {
675
+ fs.renameSync(sourceAbs, destAbs);
676
+ }
677
+ }
678
+ if (isMove) {
679
+ await deleteCachedFiles(filePath);
680
+ }
681
+ invalidateListingCache();
682
+ return c.json({ success: true, path: newPath });
683
+ }
684
+ catch (error) {
685
+ logger.error({ error: serializeError(error), filePath }, `Failed to ${isCopy ? "copy" : "move"} asset`);
686
+ return c.json({
687
+ error: "Internal server error",
688
+ message: error instanceof Error ? error.message : "Unknown error",
689
+ }, 500);
690
+ }
691
+ });
692
+ return storageRoute;
693
+ }
694
+ //# sourceMappingURL=storage.js.map