@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,208 @@
1
+ import { Hono } from "hono";
2
+ import { parseParams } from "../utils/parser.js";
3
+ import { getCachePath, existsInCache } from "../utils/cache.js";
4
+ import logger, { serializeError } from "../utils/logger.js";
5
+ export function createVideoStatusRoute(deps) {
6
+ const { storage, queue: videoJobQueue } = deps;
7
+ const videoStatus = new Hono();
8
+ /**
9
+ * GET /video-status/:path/size
10
+ * Get optimized video size
11
+ * This must be before the /* route to be matched first
12
+ */
13
+ videoStatus.get("/*/size", async (c) => {
14
+ const path = c.req.path;
15
+ // Remove '/video-status' prefix and '/size' suffix
16
+ const segments = path.split("/").slice(2, -1); // Remove 'video-status' and 'size'
17
+ const filePath = segments.join("/");
18
+ // Parse params from query string or path
19
+ // Use the same path format as transform.ts: /t/{filePath}
20
+ const fullPath = `/t/${filePath}`;
21
+ const params = parseParams(fullPath);
22
+ // Get job status first - if job is completed, we know the cache exists
23
+ const job = videoJobQueue.getJobByPath(filePath, params);
24
+ logger.info({
25
+ requestPath: path,
26
+ filePath,
27
+ fullPath,
28
+ params: JSON.stringify(params),
29
+ jobStatus: job?.status,
30
+ jobId: job?.id,
31
+ }, "Fetching optimized video size");
32
+ // Check cloud storage first (most reliable for optimized videos)
33
+ if (storage) {
34
+ try {
35
+ const existsInCloud = await storage.exists(filePath, params);
36
+ if (existsInCloud) {
37
+ // Get the size from cloud storage
38
+ const metadata = await storage.getOptimizedMetadata(filePath, params);
39
+ if (metadata) {
40
+ logger.info({
41
+ filePath,
42
+ size: metadata.size,
43
+ source: "cloud_storage",
44
+ }, "Got optimized size from cloud storage");
45
+ return c.json({
46
+ size: metadata.size,
47
+ status: "ready",
48
+ });
49
+ }
50
+ else {
51
+ logger.warn({ filePath }, "Optimized video exists in cloud but metadata unavailable");
52
+ }
53
+ }
54
+ }
55
+ catch (error) {
56
+ logger.error({ error: serializeError(error), filePath }, "Failed to get optimized size from cloud storage");
57
+ }
58
+ }
59
+ // Fallback: Check if optimized video exists in local cache
60
+ // Use the same path format as transform.ts uses for getCachePath
61
+ const cachePath = getCachePath(fullPath);
62
+ const existsLocally = await existsInCache(cachePath);
63
+ logger.info({
64
+ cachePath,
65
+ existsLocally,
66
+ jobStatus: job?.status,
67
+ jobCompleted: job?.status === "completed",
68
+ }, "Local cache check result");
69
+ if (existsLocally || job?.status === "completed") {
70
+ try {
71
+ const fs = await import("fs/promises");
72
+ const stats = await fs.stat(cachePath);
73
+ const fileAge = Date.now() - stats.mtime.getTime();
74
+ const minAge = 5000; // 5 seconds minimum age
75
+ logger.info({
76
+ cachePath,
77
+ size: stats.size,
78
+ fileAge,
79
+ minAge,
80
+ isValid: fileAge >= minAge || job?.status === "completed",
81
+ }, "Local cache file stats");
82
+ // If job is completed, trust the cache file even if it's new
83
+ // Otherwise, check file age
84
+ if (job?.status === "completed" || fileAge >= minAge) {
85
+ // File is old enough to be considered valid or job is completed
86
+ return c.json({
87
+ size: stats.size,
88
+ status: "ready",
89
+ });
90
+ }
91
+ else {
92
+ logger.debug({ filePath, fileAge, jobStatus: job?.status }, "Cache file too new and job not completed");
93
+ }
94
+ }
95
+ catch (error) {
96
+ logger.error({ error: serializeError(error), cachePath, jobStatus: job?.status }, "Failed to get cache file stats");
97
+ // If job is completed but we can't read the file, something is wrong
98
+ if (job?.status === "completed") {
99
+ return c.json({
100
+ error: "Cache file exists but cannot be read",
101
+ status: "error",
102
+ }, 500);
103
+ }
104
+ }
105
+ }
106
+ else {
107
+ logger.warn({
108
+ cachePath,
109
+ filePath,
110
+ jobStatus: job?.status,
111
+ jobExists: !!job,
112
+ }, "Cache file does not exist and job not completed");
113
+ }
114
+ // No optimized video found
115
+ return c.json({
116
+ status: "not_found",
117
+ message: "Optimized video not found",
118
+ }, 404);
119
+ });
120
+ /**
121
+ * GET /video-status/:path
122
+ * Check video processing status
123
+ */
124
+ videoStatus.get("/*", async (c) => {
125
+ const path = c.req.path;
126
+ const segments = path.split("/").slice(2); // Remove '/video-status' prefix
127
+ const filePath = segments.join("/");
128
+ // Parse params from query string or path
129
+ const fullPath = `/t/${filePath}`;
130
+ const params = parseParams(fullPath);
131
+ // Get job status
132
+ const job = videoJobQueue.getJobByPath(filePath, params);
133
+ if (!job) {
134
+ // Job doesn't exist - check if optimized video exists in cache/storage
135
+ // BUT: Only return "completed" if the file is recent enough to be valid
136
+ // This prevents false positives for newly uploaded videos
137
+ const cachePath = getCachePath(fullPath);
138
+ const existsLocally = await existsInCache(cachePath);
139
+ if (existsLocally) {
140
+ // Check file age to ensure it's not a stale cache entry
141
+ // If file was created less than 5 seconds ago, it might be incomplete
142
+ try {
143
+ const fs = await import("fs/promises");
144
+ const stats = await fs.stat(cachePath);
145
+ const fileAge = Date.now() - stats.mtime.getTime();
146
+ const minAge = 5000; // 5 seconds minimum age
147
+ if (fileAge >= minAge) {
148
+ // File is old enough to be considered valid
149
+ return c.json({
150
+ status: "completed",
151
+ progress: 100,
152
+ });
153
+ }
154
+ else {
155
+ // File is too new - might be incomplete, return not_found to trigger job creation
156
+ logger.debug({ filePath, fileAge }, "Cache file too new, treating as not found");
157
+ }
158
+ }
159
+ catch (error) {
160
+ // If we can't check file stats, don't assume it's ready
161
+ logger.warn({ error: serializeError(error), cachePath }, "Failed to check cache file stats");
162
+ }
163
+ }
164
+ // Check cloud storage if configured
165
+ if (storage) {
166
+ try {
167
+ const existsInCloud = await storage.exists(filePath, params);
168
+ if (existsInCloud) {
169
+ // For cloud storage, we trust the exists check
170
+ // Cloud storage typically handles this correctly
171
+ return c.json({
172
+ status: "completed",
173
+ progress: 100,
174
+ });
175
+ }
176
+ }
177
+ catch (error) {
178
+ logger.warn({ error: serializeError(error), filePath }, "Failed to check cloud storage for video status");
179
+ }
180
+ }
181
+ // No job and no valid cached video found
182
+ return c.json({
183
+ status: "not_found",
184
+ message: "No processing job found for this video",
185
+ }, 404);
186
+ }
187
+ // Return job status
188
+ // Only return error status if the job has actually failed (not just if error field exists)
189
+ const status = job.status === "error" && job.error ? "error" : job.status;
190
+ return c.json({
191
+ status,
192
+ progress: job.progress || 0,
193
+ startedAt: job.startedAt,
194
+ completedAt: job.completedAt,
195
+ error: job.error,
196
+ });
197
+ });
198
+ /**
199
+ * GET /video-status-stats
200
+ * Get queue statistics (for debugging)
201
+ */
202
+ videoStatus.get("/stats", (c) => {
203
+ const stats = videoJobQueue.getStats();
204
+ return c.json(stats);
205
+ });
206
+ return videoStatus;
207
+ }
208
+ //# sourceMappingURL=video-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-status.js","sourceRoot":"","sources":["../../src/routes/video-status.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ import { Context } from "hono";
2
+ import { CloudStorage } from "../utils/storage/index";
3
+ import type { VideoJobQueue } from "../utils/video-job-queue";
4
+ export interface TransformRequest {
5
+ path: string;
6
+ userAgent: string;
7
+ acceptHeader?: string;
8
+ context: Context;
9
+ }
10
+ export interface TransformResult {
11
+ buffer?: Buffer;
12
+ /** Set instead of buffer for large payloads served without in-memory buffering */
13
+ stream?: ReadableStream<Uint8Array>;
14
+ contentType: string;
15
+ headers: Record<string, string>;
16
+ isProcessing?: boolean;
17
+ optimizationResult?: any;
18
+ }
19
+ export interface CacheCheckResult {
20
+ cloudCacheBuffer?: Buffer;
21
+ localCacheBuffer?: Buffer;
22
+ cachePath?: string;
23
+ effectiveParams: any;
24
+ }
25
+ export declare class TransformService {
26
+ private storage;
27
+ private compression;
28
+ private queue;
29
+ constructor(storage: CloudStorage | null, queue: VideoJobQueue);
30
+ /**
31
+ * Main transformation method that handles the complete flow
32
+ */
33
+ transform(request: TransformRequest): Promise<TransformResult>;
34
+ /**
35
+ * Check if the first segment is a transformation string
36
+ */
37
+ private hasTransformSegment;
38
+ /**
39
+ * Get effective parameters with format optimization and cache path
40
+ */
41
+ private getEffectiveParamsAndCachePath;
42
+ /**
43
+ * Check both cloud and local caches
44
+ */
45
+ private checkCaches;
46
+ /**
47
+ * Format response for cached content
48
+ */
49
+ private formatCacheResponse;
50
+ /**
51
+ * Process the file (images and videos)
52
+ */
53
+ private processFile;
54
+ /**
55
+ * Process image file
56
+ */
57
+ private processImageFile;
58
+ /**
59
+ * Handle video job queue management
60
+ */
61
+ private handleVideoJobQueue;
62
+ /**
63
+ * Handle file not found scenario
64
+ */
65
+ private handleFileNotFound;
66
+ /**
67
+ * Handle transformation errors
68
+ */
69
+ private handleTransformationError;
70
+ }
71
+ //# sourceMappingURL=transform.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.service.d.ts","sourceRoot":"","sources":["../../src/services/transform.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAGtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAe9D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,MAAM,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,GAAG,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,GAAG,CAAC;CACtB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,KAAK,CAAgB;gBAEjB,OAAO,EAAE,YAAY,GAAG,IAAI,EAAE,KAAK,EAAE,aAAa;IAM9D;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IA4EpE;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;OAEG;YACW,8BAA8B;IA8B5C;;OAEG;YACW,WAAW;IA+BzB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAyB3B;;OAEG;YACW,WAAW;IAuHzB;;OAEG;YACW,gBAAgB;IAmB9B;;OAEG;YACW,mBAAmB;IAuIjC;;OAEG;YACW,kBAAkB;IAWhC;;OAEG;YACW,yBAAyB;CAsDxC"}
@@ -0,0 +1,362 @@
1
+ import { getCachePath, existsInCache, deleteCachedFiles } from "../utils/cache.js";
2
+ import { parseParams, isTransformSegment } from "../utils/parser.js";
3
+ import { Compression } from "../utils/image/compression.js";
4
+ import logger, { serializeError } from "../utils/logger.js";
5
+ import { checkCloudCache, checkLocalCache, verifyFileExists, prepareSourceFile, processImage, processVideo, saveToCaches, cleanupTempFile, performPeriodicCacheCleanup, } from "../routes/transform-helpers.js";
6
+ import { TRANSFORMATION_PRIORITY } from "../utils/video/config.js";
7
+ export class TransformService {
8
+ storage;
9
+ compression;
10
+ queue;
11
+ constructor(storage, queue) {
12
+ this.storage = storage;
13
+ this.queue = queue;
14
+ this.compression = new Compression();
15
+ }
16
+ /**
17
+ * Main transformation method that handles the complete flow
18
+ */
19
+ async transform(request) {
20
+ const { path, userAgent, acceptHeader } = request;
21
+ try {
22
+ // Parse path and parameters
23
+ const segments = path.split("/").slice(2); // Remove '/t' prefix
24
+ const params = parseParams(path);
25
+ // Determine file path segments
26
+ const hasTransform = this.hasTransformSegment(segments);
27
+ const fileSegments = hasTransform ? segments.slice(1) : segments;
28
+ const filePath = fileSegments.join("/");
29
+ const localPath = `./public/${filePath}`;
30
+ const ext = filePath.split(".").pop()?.toLowerCase();
31
+ // Get effective parameters with format optimization
32
+ const { effectiveParams, cachePath } = await this.getEffectiveParamsAndCachePath(path, params, userAgent, acceptHeader);
33
+ // Verify original file exists
34
+ const fileCheck = await verifyFileExists(this.storage, filePath, localPath);
35
+ if (!fileCheck.exists) {
36
+ await this.handleFileNotFound(filePath);
37
+ throw new Error(fileCheck.error || "File not found");
38
+ }
39
+ // Check caches
40
+ const cacheResult = await this.checkCaches(this.storage, filePath, effectiveParams, cachePath);
41
+ if (cacheResult.cloudCacheBuffer) {
42
+ return this.formatCacheResponse(cacheResult.cloudCacheBuffer, effectiveParams, ext, "cloud");
43
+ }
44
+ if (cacheResult.localCacheBuffer) {
45
+ return this.formatCacheResponse(cacheResult.localCacheBuffer, effectiveParams, ext, "local");
46
+ }
47
+ // Process the file
48
+ return await this.processFile(filePath, localPath, ext, effectiveParams, cachePath, userAgent, acceptHeader);
49
+ }
50
+ catch (error) {
51
+ return this.handleTransformationError(error, request);
52
+ }
53
+ }
54
+ /**
55
+ * Check if the first segment is a transformation string
56
+ */
57
+ hasTransformSegment(segments) {
58
+ return segments.length > 0 && isTransformSegment(segments[0]);
59
+ }
60
+ /**
61
+ * Get effective parameters with format optimization and cache path
62
+ */
63
+ async getEffectiveParamsAndCachePath(path, params, userAgent, acceptHeader) {
64
+ const ext = path.split(".").pop()?.toLowerCase();
65
+ let effectiveParams = { ...params };
66
+ let cachePath = getCachePath(path);
67
+ // Determine optimal format if not explicitly specified
68
+ if (!params.format && ext?.match(/jpe?g|png|webp|avif|gif|psd/)) {
69
+ const optimalFormat = this.compression.determineOptimalFormatForCache(userAgent, acceptHeader, ext);
70
+ effectiveParams = { ...params, format: optimalFormat };
71
+ // Update cache path to include the optimal format
72
+ const pathWithFormat = path.replace(/\/t\/(.*)$/, `/t/format:${optimalFormat}/$1`);
73
+ cachePath = getCachePath(pathWithFormat);
74
+ }
75
+ return { effectiveParams, cachePath };
76
+ }
77
+ /**
78
+ * Check both cloud and local caches
79
+ */
80
+ async checkCaches(storage, filePath, effectiveParams, cachePath) {
81
+ const result = {
82
+ effectiveParams,
83
+ cachePath,
84
+ };
85
+ // Check cloud cache first
86
+ const cloudCacheBuffer = await checkCloudCache(storage, filePath, effectiveParams);
87
+ if (cloudCacheBuffer) {
88
+ result.cloudCacheBuffer = cloudCacheBuffer;
89
+ return result;
90
+ }
91
+ // Check local cache
92
+ const localCacheBuffer = await checkLocalCache(cachePath);
93
+ if (localCacheBuffer) {
94
+ result.localCacheBuffer = localCacheBuffer;
95
+ }
96
+ return result;
97
+ }
98
+ /**
99
+ * Format response for cached content
100
+ */
101
+ formatCacheResponse(buffer, effectiveParams, ext, _cacheType) {
102
+ const headers = {
103
+ "Cache-Control": "public, max-age=31536000, must-revalidate",
104
+ ETag: `"${JSON.stringify(effectiveParams)}"`,
105
+ "Content-Length": buffer.length.toString(),
106
+ };
107
+ // For videos, add video-specific headers
108
+ if (ext?.match(/mp4|mov|webm/)) {
109
+ headers["X-Video-Status"] = "ready";
110
+ }
111
+ return {
112
+ buffer,
113
+ contentType: "", // Will be determined by the route handler
114
+ headers,
115
+ isProcessing: false,
116
+ };
117
+ }
118
+ /**
119
+ * Process the file (images and videos)
120
+ */
121
+ async processFile(filePath, localPath, ext, effectiveParams, cachePath, userAgent, acceptHeader) {
122
+ // Video transformations (non-thumbnail) are processed by the background
123
+ // job queue, which downloads its own source copy. Skip prepareSourceFile
124
+ // entirely: downloading the full original here would only delay the
125
+ // response and waste memory/bandwidth.
126
+ const isThumbnailRequest = effectiveParams.thumbnail === "true" || effectiveParams.thumbnail === "1";
127
+ if (ext?.match(/mp4|mov|webm/) && !isThumbnailRequest) {
128
+ return await this.handleVideoJobQueue(filePath, localPath, effectiveParams, cachePath);
129
+ }
130
+ // Prepare source file
131
+ const sourcePath = await prepareSourceFile(this.storage, filePath, localPath);
132
+ const isTempFile = !!this.storage;
133
+ try {
134
+ let buffer;
135
+ let contentType;
136
+ let optimizationResult;
137
+ // Process based on file type
138
+ if (ext?.match(/jpe?g|png|webp|avif|gif|psd/)) {
139
+ const result = await this.processImageFile(sourcePath, effectiveParams, userAgent, acceptHeader);
140
+ buffer = result.buffer;
141
+ contentType = result.contentType;
142
+ optimizationResult = result.optimizationResult;
143
+ }
144
+ else if (ext?.match(/mp4|mov|webm/)) {
145
+ // Only thumbnail requests reach this point (non-thumbnail video
146
+ // transformations are intercepted before prepareSourceFile above);
147
+ // thumbnails are extracted synchronously like images
148
+ const result = await processVideo(sourcePath, effectiveParams);
149
+ return {
150
+ buffer: result.buffer,
151
+ contentType: result.contentType,
152
+ headers: {
153
+ "Content-Length": result.buffer.length.toString(),
154
+ "Cache-Control": "public, max-age=31536000, must-revalidate",
155
+ },
156
+ };
157
+ }
158
+ else {
159
+ throw new Error("Unsupported file type");
160
+ }
161
+ // Save to caches
162
+ if (cachePath) {
163
+ await saveToCaches(this.storage, filePath, effectiveParams, cachePath, buffer, contentType);
164
+ }
165
+ // Periodic cache cleanup
166
+ await performPeriodicCacheCleanup();
167
+ // Format response
168
+ // encodeURIComponent keeps ETag ASCII-only: HTTP header values must be a
169
+ // ByteString, and raw file paths can contain non-ASCII or NFD-decomposed
170
+ // accented characters (e.g. a combining accent has a code point > 255)
171
+ const headers = {
172
+ "Content-Length": buffer.length.toString(),
173
+ "Cache-Control": "public, max-age=31536000, must-revalidate",
174
+ ETag: `"${encodeURIComponent(filePath)}-${JSON.stringify(effectiveParams)}"`,
175
+ };
176
+ // Add optimization headers if available
177
+ if (optimizationResult) {
178
+ headers["X-Original-Size"] = optimizationResult.originalSize.toString();
179
+ headers["X-Optimized-Size"] =
180
+ optimizationResult.optimizedSize.toString();
181
+ headers["X-Compression-Ratio"] =
182
+ optimizationResult.compressionRatio.toFixed(2);
183
+ headers["X-Savings-Percent"] = optimizationResult.savings.toFixed(1);
184
+ }
185
+ // For videos, indicate this is the optimized version
186
+ if (ext?.match(/mp4|mov|webm/)) {
187
+ headers["X-Video-Status"] = "ready";
188
+ }
189
+ return {
190
+ buffer,
191
+ contentType,
192
+ headers,
193
+ optimizationResult,
194
+ };
195
+ }
196
+ finally {
197
+ // Clean up temporary source file if used
198
+ if (isTempFile) {
199
+ await cleanupTempFile(sourcePath);
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * Process image file
205
+ */
206
+ async processImageFile(sourcePath, effectiveParams, userAgent, acceptHeader) {
207
+ return await processImage(sourcePath, effectiveParams, userAgent, acceptHeader, this.compression);
208
+ }
209
+ /**
210
+ * Handle video job queue management
211
+ */
212
+ async handleVideoJobQueue(filePath, localPath, params, cachePath) {
213
+ // Check if already being processed
214
+ let existingJob = this.queue.getJobByPath(filePath, params);
215
+ let shouldRequeue = false;
216
+ if (existingJob) {
217
+ logger.debug({ jobId: existingJob.id, status: existingJob.status }, "Video job exists");
218
+ // If completed, verify cache actually exists before serving
219
+ if (existingJob.status === "completed" && cachePath) {
220
+ const localCacheExists = await existsInCache(cachePath);
221
+ const cloudCacheExists = this.storage
222
+ ? await this.storage.exists(filePath, params)
223
+ : false;
224
+ if (localCacheExists || cloudCacheExists) {
225
+ // Cache exists, try to serve from local cache first
226
+ const cachedBuffer = await checkLocalCache(cachePath);
227
+ if (cachedBuffer) {
228
+ return {
229
+ buffer: cachedBuffer,
230
+ contentType: `video/${filePath.split(".").pop()}`,
231
+ headers: {
232
+ "X-Video-Status": "ready",
233
+ "Content-Length": cachedBuffer.length.toString(),
234
+ "Cache-Control": "public, max-age=31536000, must-revalidate",
235
+ },
236
+ };
237
+ }
238
+ }
239
+ else {
240
+ // Job is marked as completed but cache doesn't exist
241
+ logger.warn({ jobId: existingJob.id, filePath, cachePath }, "Job marked as completed but cache missing - resetting to pending");
242
+ try {
243
+ this.queue.getStore().updateJobStatus(existingJob.id, "pending", 0);
244
+ shouldRequeue = true;
245
+ existingJob = { ...existingJob, status: "pending" };
246
+ }
247
+ catch (error) {
248
+ logger.error({ error: serializeError(error), jobId: existingJob.id }, "Failed to reset job status");
249
+ shouldRequeue = true;
250
+ }
251
+ }
252
+ }
253
+ }
254
+ // Add to background processing queue
255
+ if (!existingJob ||
256
+ existingJob.status === "error" ||
257
+ existingJob.status === "pending" ||
258
+ shouldRequeue) {
259
+ this.queue
260
+ .addJob(filePath, params, cachePath, localPath, this.storage, TRANSFORMATION_PRIORITY)
261
+ .catch((error) => {
262
+ logger.error({ error: serializeError(error), filePath }, "Failed to add video job");
263
+ });
264
+ }
265
+ // Stream the original video immediately, without buffering it in memory:
266
+ // originals can weigh hundreds of MB and buffering both delays the first
267
+ // byte and pressures the container memory while ffmpeg jobs are running
268
+ // encodeURIComponent keeps ETag ASCII-only: HTTP header values must be a
269
+ // ByteString, and raw file paths can contain non-ASCII or NFD-decomposed
270
+ // accented characters (e.g. a combining accent has a code point > 255)
271
+ const processingHeaders = {
272
+ "X-Video-Status": "processing",
273
+ "X-Original-Video": "true",
274
+ "Cache-Control": "public, max-age=0, must-revalidate",
275
+ ETag: `"${encodeURIComponent(filePath)}-processing-${Date.now()}"`,
276
+ Vary: "Accept",
277
+ };
278
+ try {
279
+ if (this.storage) {
280
+ const { stream, contentLength } = await this.storage.downloadOriginalStream(filePath);
281
+ if (contentLength) {
282
+ processingHeaders["Content-Length"] = contentLength.toString();
283
+ }
284
+ return {
285
+ stream,
286
+ contentType: `video/${filePath.split(".").pop()}`,
287
+ headers: processingHeaders,
288
+ isProcessing: true,
289
+ };
290
+ }
291
+ const { createReadStream } = await import("fs");
292
+ const { stat } = await import("fs/promises");
293
+ const stats = await stat(localPath);
294
+ processingHeaders["Content-Length"] = stats.size.toString();
295
+ const { Readable } = await import("stream");
296
+ return {
297
+ stream: Readable.toWeb(createReadStream(localPath)),
298
+ contentType: `video/${filePath.split(".").pop()}`,
299
+ headers: processingHeaders,
300
+ isProcessing: true,
301
+ };
302
+ }
303
+ catch (error) {
304
+ logger.error({ error: serializeError(error), filePath }, "Failed to serve original video");
305
+ throw new Error("Failed to load video");
306
+ }
307
+ }
308
+ /**
309
+ * Handle file not found scenario
310
+ */
311
+ async handleFileNotFound(filePath) {
312
+ try {
313
+ await deleteCachedFiles(filePath);
314
+ }
315
+ catch (error) {
316
+ logger.warn({ error: serializeError(error), filePath }, "Failed to delete cached files");
317
+ }
318
+ }
319
+ /**
320
+ * Handle transformation errors
321
+ */
322
+ async handleTransformationError(error, request) {
323
+ const errorMessage = error instanceof Error ? error.message : String(error);
324
+ logger.error({
325
+ error: serializeError(error),
326
+ path: request.path,
327
+ }, "Processing error");
328
+ // Check if this is a "file not found" error from cloud storage
329
+ const isNotFoundError = errorMessage.includes("NoSuchKey") ||
330
+ errorMessage.includes("NotFound") ||
331
+ errorMessage.includes("404") ||
332
+ errorMessage.includes("does not exist") ||
333
+ errorMessage.includes("not found");
334
+ if (isNotFoundError && this.storage) {
335
+ // Invalidate cache since the file doesn't exist
336
+ const pathSegments = request.path.split("/").slice(2);
337
+ const hasTransform = pathSegments.length > 0 && isTransformSegment(pathSegments[0]);
338
+ const filePath = hasTransform
339
+ ? pathSegments.slice(1).join("/")
340
+ : pathSegments.join("/");
341
+ this.storage.invalidateAllCacheEntries(filePath);
342
+ // Delete local cache files
343
+ try {
344
+ await deleteCachedFiles(filePath);
345
+ }
346
+ catch (cleanupError) {
347
+ logger.warn({ error: serializeError(cleanupError), filePath }, "Failed to cleanup cache after not found error");
348
+ }
349
+ }
350
+ // Return error result (route handler will convert to appropriate HTTP response)
351
+ return {
352
+ buffer: Buffer.from(`Processing failed: ${errorMessage}`),
353
+ contentType: "text/plain",
354
+ headers: {
355
+ "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
356
+ Pragma: "no-cache",
357
+ Expires: "0",
358
+ },
359
+ };
360
+ }
361
+ }
362
+ //# sourceMappingURL=transform.service.js.map