@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,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
|