@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.
- package/LICENSE +661 -0
- package/dist/config/deps.d.ts +14 -0
- package/dist/config/deps.d.ts.map +1 -0
- package/dist/config/deps.js +2 -0
- package/dist/config/deps.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/authenticated.d.ts +4 -0
- package/dist/routes/authenticated.d.ts.map +1 -0
- package/dist/routes/authenticated.js +130 -0
- package/dist/routes/authenticated.js.map +1 -0
- package/dist/routes/download-folder.d.ts +4 -0
- package/dist/routes/download-folder.d.ts.map +1 -0
- package/dist/routes/download-folder.js +101 -0
- package/dist/routes/download-folder.js.map +1 -0
- package/dist/routes/download-zip.d.ts +15 -0
- package/dist/routes/download-zip.d.ts.map +1 -0
- package/dist/routes/download-zip.js +147 -0
- package/dist/routes/download-zip.js.map +1 -0
- package/dist/routes/download.d.ts +4 -0
- package/dist/routes/download.d.ts.map +1 -0
- package/dist/routes/download.js +83 -0
- package/dist/routes/download.js.map +1 -0
- package/dist/routes/invalidate.d.ts +4 -0
- package/dist/routes/invalidate.d.ts.map +1 -0
- package/dist/routes/invalidate.js +114 -0
- package/dist/routes/invalidate.js.map +1 -0
- package/dist/routes/queue-events.d.ts +4 -0
- package/dist/routes/queue-events.d.ts.map +1 -0
- package/dist/routes/queue-events.js +125 -0
- package/dist/routes/queue-events.js.map +1 -0
- package/dist/routes/queue.d.ts +4 -0
- package/dist/routes/queue.d.ts.map +1 -0
- package/dist/routes/queue.js +130 -0
- package/dist/routes/queue.js.map +1 -0
- package/dist/routes/storage.d.ts +4 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +694 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/transform-helpers.d.ts +62 -0
- package/dist/routes/transform-helpers.d.ts.map +1 -0
- package/dist/routes/transform-helpers.js +372 -0
- package/dist/routes/transform-helpers.js.map +1 -0
- package/dist/routes/transform.d.ts +4 -0
- package/dist/routes/transform.d.ts.map +1 -0
- package/dist/routes/transform.js +65 -0
- package/dist/routes/transform.js.map +1 -0
- package/dist/routes/video-status.d.ts +4 -0
- package/dist/routes/video-status.d.ts.map +1 -0
- package/dist/routes/video-status.js +208 -0
- package/dist/routes/video-status.js.map +1 -0
- package/dist/services/transform.service.d.ts +71 -0
- package/dist/services/transform.service.d.ts.map +1 -0
- package/dist/services/transform.service.js +362 -0
- package/dist/services/transform.service.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/asset-deletion.d.ts +16 -0
- package/dist/utils/asset-deletion.d.ts.map +1 -0
- package/dist/utils/asset-deletion.js +131 -0
- package/dist/utils/asset-deletion.js.map +1 -0
- package/dist/utils/cache.d.ts +44 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +248 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/get-unique-file-path.d.ts +8 -0
- package/dist/utils/get-unique-file-path.d.ts.map +1 -0
- package/dist/utils/get-unique-file-path.js +26 -0
- package/dist/utils/get-unique-file-path.js.map +1 -0
- package/dist/utils/image/aspect-ratio.d.ts +7 -0
- package/dist/utils/image/aspect-ratio.d.ts.map +1 -0
- package/dist/utils/image/aspect-ratio.js +40 -0
- package/dist/utils/image/aspect-ratio.js.map +1 -0
- package/dist/utils/image/background.d.ts +6 -0
- package/dist/utils/image/background.d.ts.map +1 -0
- package/dist/utils/image/background.js +22 -0
- package/dist/utils/image/background.js.map +1 -0
- package/dist/utils/image/compression.d.ts +44 -0
- package/dist/utils/image/compression.d.ts.map +1 -0
- package/dist/utils/image/compression.js +345 -0
- package/dist/utils/image/compression.js.map +1 -0
- package/dist/utils/image/gravity.d.ts +6 -0
- package/dist/utils/image/gravity.d.ts.map +1 -0
- package/dist/utils/image/gravity.js +24 -0
- package/dist/utils/image/gravity.js.map +1 -0
- package/dist/utils/image/index.d.ts +8 -0
- package/dist/utils/image/index.d.ts.map +1 -0
- package/dist/utils/image/index.js +77 -0
- package/dist/utils/image/index.js.map +1 -0
- package/dist/utils/image/param-registry.d.ts +63 -0
- package/dist/utils/image/param-registry.d.ts.map +1 -0
- package/dist/utils/image/param-registry.js +196 -0
- package/dist/utils/image/param-registry.js.map +1 -0
- package/dist/utils/image/quality.d.ts +8 -0
- package/dist/utils/image/quality.d.ts.map +1 -0
- package/dist/utils/image/quality.js +9 -0
- package/dist/utils/image/quality.js.map +1 -0
- package/dist/utils/image/resize.d.ts +7 -0
- package/dist/utils/image/resize.d.ts.map +1 -0
- package/dist/utils/image/resize.js +62 -0
- package/dist/utils/image/resize.js.map +1 -0
- package/dist/utils/image/rotation.d.ts +6 -0
- package/dist/utils/image/rotation.d.ts.map +1 -0
- package/dist/utils/image/rotation.js +20 -0
- package/dist/utils/image/rotation.js.map +1 -0
- package/dist/utils/image/round-corners.d.ts +12 -0
- package/dist/utils/image/round-corners.d.ts.map +1 -0
- package/dist/utils/image/round-corners.js +102 -0
- package/dist/utils/image/round-corners.js.map +1 -0
- package/dist/utils/image/types.d.ts +2 -0
- package/dist/utils/image/types.d.ts.map +1 -0
- package/dist/utils/image/types.js +2 -0
- package/dist/utils/image/types.js.map +1 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +82 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/parser.d.ts +8 -0
- package/dist/utils/parser.d.ts.map +1 -0
- package/dist/utils/parser.js +226 -0
- package/dist/utils/parser.js.map +1 -0
- package/dist/utils/signature.d.ts +35 -0
- package/dist/utils/signature.d.ts.map +1 -0
- package/dist/utils/signature.js +100 -0
- package/dist/utils/signature.js.map +1 -0
- package/dist/utils/storage/cache.d.ts +41 -0
- package/dist/utils/storage/cache.d.ts.map +1 -0
- package/dist/utils/storage/cache.js +90 -0
- package/dist/utils/storage/cache.js.map +1 -0
- package/dist/utils/storage/cloud-storage.d.ts +188 -0
- package/dist/utils/storage/cloud-storage.d.ts.map +1 -0
- package/dist/utils/storage/cloud-storage.js +514 -0
- package/dist/utils/storage/cloud-storage.js.map +1 -0
- package/dist/utils/storage/factory.d.ts +10 -0
- package/dist/utils/storage/factory.d.ts.map +1 -0
- package/dist/utils/storage/factory.js +17 -0
- package/dist/utils/storage/factory.js.map +1 -0
- package/dist/utils/storage/index.d.ts +7 -0
- package/dist/utils/storage/index.d.ts.map +1 -0
- package/dist/utils/storage/index.js +8 -0
- package/dist/utils/storage/index.js.map +1 -0
- package/dist/utils/storage/key-generator.d.ts +11 -0
- package/dist/utils/storage/key-generator.d.ts.map +1 -0
- package/dist/utils/storage/key-generator.js +23 -0
- package/dist/utils/storage/key-generator.js.map +1 -0
- package/dist/utils/storage/listing-cache.d.ts +14 -0
- package/dist/utils/storage/listing-cache.d.ts.map +1 -0
- package/dist/utils/storage/listing-cache.js +29 -0
- package/dist/utils/storage/listing-cache.js.map +1 -0
- package/dist/utils/storage/s3-client.d.ts +97 -0
- package/dist/utils/storage/s3-client.d.ts.map +1 -0
- package/dist/utils/storage/s3-client.js +297 -0
- package/dist/utils/storage/s3-client.js.map +1 -0
- package/dist/utils/storage/stats-tracker.d.ts +45 -0
- package/dist/utils/storage/stats-tracker.d.ts.map +1 -0
- package/dist/utils/storage/stats-tracker.js +97 -0
- package/dist/utils/storage/stats-tracker.js.map +1 -0
- package/dist/utils/storage/types.d.ts +2 -0
- package/dist/utils/storage/types.d.ts.map +1 -0
- package/dist/utils/storage/types.js +2 -0
- package/dist/utils/storage/types.js.map +1 -0
- package/dist/utils/storage-level.d.ts +66 -0
- package/dist/utils/storage-level.d.ts.map +1 -0
- package/dist/utils/storage-level.js +106 -0
- package/dist/utils/storage-level.js.map +1 -0
- package/dist/utils/storage-tree.d.ts +30 -0
- package/dist/utils/storage-tree.d.ts.map +1 -0
- package/dist/utils/storage-tree.js +81 -0
- package/dist/utils/storage-tree.js.map +1 -0
- package/dist/utils/upload-signature.d.ts +25 -0
- package/dist/utils/upload-signature.d.ts.map +1 -0
- package/dist/utils/upload-signature.js +56 -0
- package/dist/utils/upload-signature.js.map +1 -0
- package/dist/utils/video/auto-downscale.d.ts +12 -0
- package/dist/utils/video/auto-downscale.d.ts.map +1 -0
- package/dist/utils/video/auto-downscale.js +29 -0
- package/dist/utils/video/auto-downscale.js.map +1 -0
- package/dist/utils/video/command-builder.d.ts +22 -0
- package/dist/utils/video/command-builder.d.ts.map +1 -0
- package/dist/utils/video/command-builder.js +79 -0
- package/dist/utils/video/command-builder.js.map +1 -0
- package/dist/utils/video/config.d.ts +51 -0
- package/dist/utils/video/config.d.ts.map +1 -0
- package/dist/utils/video/config.js +170 -0
- package/dist/utils/video/config.js.map +1 -0
- package/dist/utils/video/format.d.ts +22 -0
- package/dist/utils/video/format.d.ts.map +1 -0
- package/dist/utils/video/format.js +64 -0
- package/dist/utils/video/format.js.map +1 -0
- package/dist/utils/video/index.d.ts +25 -0
- package/dist/utils/video/index.d.ts.map +1 -0
- package/dist/utils/video/index.js +104 -0
- package/dist/utils/video/index.js.map +1 -0
- package/dist/utils/video/param-registry.d.ts +64 -0
- package/dist/utils/video/param-registry.d.ts.map +1 -0
- package/dist/utils/video/param-registry.js +201 -0
- package/dist/utils/video/param-registry.js.map +1 -0
- package/dist/utils/video/quality.d.ts +12 -0
- package/dist/utils/video/quality.d.ts.map +1 -0
- package/dist/utils/video/quality.js +45 -0
- package/dist/utils/video/quality.js.map +1 -0
- package/dist/utils/video/queue-store.d.ts +51 -0
- package/dist/utils/video/queue-store.d.ts.map +1 -0
- package/dist/utils/video/queue-store.js +2 -0
- package/dist/utils/video/queue-store.js.map +1 -0
- package/dist/utils/video/resize.d.ts +7 -0
- package/dist/utils/video/resize.d.ts.map +1 -0
- package/dist/utils/video/resize.js +40 -0
- package/dist/utils/video/resize.js.map +1 -0
- package/dist/utils/video/sqlite-video-job-store.d.ts +29 -0
- package/dist/utils/video/sqlite-video-job-store.d.ts.map +1 -0
- package/dist/utils/video/sqlite-video-job-store.js +320 -0
- package/dist/utils/video/sqlite-video-job-store.js.map +1 -0
- package/dist/utils/video/thumbnail.d.ts +7 -0
- package/dist/utils/video/thumbnail.d.ts.map +1 -0
- package/dist/utils/video/thumbnail.js +22 -0
- package/dist/utils/video/thumbnail.js.map +1 -0
- package/dist/utils/video/trim.d.ts +7 -0
- package/dist/utils/video/trim.d.ts.map +1 -0
- package/dist/utils/video/trim.js +36 -0
- package/dist/utils/video/trim.js.map +1 -0
- package/dist/utils/video/types.d.ts +19 -0
- package/dist/utils/video/types.d.ts.map +1 -0
- package/dist/utils/video/types.js +2 -0
- package/dist/utils/video/types.js.map +1 -0
- package/dist/utils/video/video-info.d.ts +25 -0
- package/dist/utils/video/video-info.d.ts.map +1 -0
- package/dist/utils/video/video-info.js +58 -0
- package/dist/utils/video/video-info.js.map +1 -0
- package/dist/utils/video/video-worker.d.ts +52 -0
- package/dist/utils/video/video-worker.d.ts.map +1 -0
- package/dist/utils/video/video-worker.js +192 -0
- package/dist/utils/video/video-worker.js.map +1 -0
- package/dist/utils/video-job-queue.d.ts +62 -0
- package/dist/utils/video-job-queue.d.ts.map +1 -0
- package/dist/utils/video-job-queue.js +122 -0
- package/dist/utils/video-job-queue.js.map +1 -0
- 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
|