@c-time/frelio-cms 1.3.7 → 1.3.8
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/functions/api/auth/callback.js +122 -0
- package/functions/api/storage/_middleware.js +1012 -0
- package/functions/api/storage/files/[uuid].js +19 -0
- package/functions/api/storage/list.js +8 -0
- package/functions/api/storage/rebuild/[year].js +14 -0
- package/functions/api/storage/upload-set.js +7 -0
- package/functions/api/storage/upload.js +7 -0
- package/functions/api/storage/years.js +7 -0
- package/package.json +4 -3
- package/functions/api/auth/callback.ts +0 -138
- package/functions/api/storage/_middleware.ts +0 -76
- package/functions/api/storage/files/[uuid].ts +0 -35
- package/functions/api/storage/list.ts +0 -19
- package/functions/api/storage/rebuild/[year].ts +0 -27
- package/functions/api/storage/upload-set.ts +0 -18
- package/functions/api/storage/upload.ts +0 -18
- package/functions/api/storage/years.ts +0 -18
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
// ../../workers/file-upload/src/infra/LoggerSingleton.ts
|
|
2
|
+
var LOG_LEVEL_PRIORITY = {
|
|
3
|
+
debug: 0,
|
|
4
|
+
info: 1,
|
|
5
|
+
warn: 2,
|
|
6
|
+
error: 3
|
|
7
|
+
};
|
|
8
|
+
var LoggerSingleton = class _LoggerSingleton {
|
|
9
|
+
static instance = null;
|
|
10
|
+
minLevel = "info";
|
|
11
|
+
handlers = [];
|
|
12
|
+
requestId = null;
|
|
13
|
+
constructor() {
|
|
14
|
+
}
|
|
15
|
+
static getInstance() {
|
|
16
|
+
if (!_LoggerSingleton.instance) {
|
|
17
|
+
_LoggerSingleton.instance = new _LoggerSingleton();
|
|
18
|
+
}
|
|
19
|
+
return _LoggerSingleton.instance;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* リクエストごとに requestId を設定(トレーシング用)
|
|
23
|
+
*/
|
|
24
|
+
setRequestId(requestId) {
|
|
25
|
+
this.requestId = requestId;
|
|
26
|
+
}
|
|
27
|
+
clearRequestId() {
|
|
28
|
+
this.requestId = null;
|
|
29
|
+
}
|
|
30
|
+
setLevel(level) {
|
|
31
|
+
this.minLevel = level;
|
|
32
|
+
}
|
|
33
|
+
getLevel() {
|
|
34
|
+
return this.minLevel;
|
|
35
|
+
}
|
|
36
|
+
addHandler(handler) {
|
|
37
|
+
this.handlers.push(handler);
|
|
38
|
+
return () => {
|
|
39
|
+
const index = this.handlers.indexOf(handler);
|
|
40
|
+
if (index > -1) {
|
|
41
|
+
this.handlers.splice(index, 1);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
debug(context, messageOrData, data) {
|
|
46
|
+
this.log("debug", context, messageOrData, data);
|
|
47
|
+
}
|
|
48
|
+
info(context, messageOrData, data) {
|
|
49
|
+
this.log("info", context, messageOrData, data);
|
|
50
|
+
}
|
|
51
|
+
warn(context, messageOrData, data) {
|
|
52
|
+
this.log("warn", context, messageOrData, data);
|
|
53
|
+
}
|
|
54
|
+
error(context, messageOrData, data) {
|
|
55
|
+
this.log("error", context, messageOrData, data);
|
|
56
|
+
}
|
|
57
|
+
log(level, context, messageOrData, data) {
|
|
58
|
+
if (!this.shouldLog(level)) return;
|
|
59
|
+
const entry = {
|
|
60
|
+
level,
|
|
61
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
62
|
+
context,
|
|
63
|
+
message: typeof messageOrData === "string" ? messageOrData : void 0,
|
|
64
|
+
data: typeof messageOrData === "string" ? data : messageOrData,
|
|
65
|
+
requestId: this.requestId ?? void 0
|
|
66
|
+
};
|
|
67
|
+
this.logToConsole(entry);
|
|
68
|
+
this.handlers.forEach((handler) => {
|
|
69
|
+
try {
|
|
70
|
+
handler(entry);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
shouldLog(level) {
|
|
76
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.minLevel];
|
|
77
|
+
}
|
|
78
|
+
logToConsole(entry) {
|
|
79
|
+
const logObject = {
|
|
80
|
+
level: entry.level,
|
|
81
|
+
ts: entry.timestamp,
|
|
82
|
+
ctx: entry.context,
|
|
83
|
+
...entry.message && { msg: entry.message },
|
|
84
|
+
...entry.data && { data: entry.data },
|
|
85
|
+
...entry.requestId && { reqId: entry.requestId }
|
|
86
|
+
};
|
|
87
|
+
switch (entry.level) {
|
|
88
|
+
case "debug":
|
|
89
|
+
console.debug(JSON.stringify(logObject));
|
|
90
|
+
break;
|
|
91
|
+
case "info":
|
|
92
|
+
console.info(JSON.stringify(logObject));
|
|
93
|
+
break;
|
|
94
|
+
case "warn":
|
|
95
|
+
console.warn(JSON.stringify(logObject));
|
|
96
|
+
break;
|
|
97
|
+
case "error":
|
|
98
|
+
console.error(JSON.stringify(logObject));
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var logger = LoggerSingleton.getInstance();
|
|
104
|
+
|
|
105
|
+
// ../../workers/file-upload/src/presenter/HttpPresenterImpl.ts
|
|
106
|
+
var HttpPresenterImpl = class {
|
|
107
|
+
corsHeaders;
|
|
108
|
+
constructor(corsHeaders) {
|
|
109
|
+
this.corsHeaders = corsHeaders ?? {
|
|
110
|
+
"Access-Control-Allow-Origin": "*",
|
|
111
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
112
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* CORS プリフライトレスポンス
|
|
117
|
+
*/
|
|
118
|
+
cors() {
|
|
119
|
+
return new Response(null, { headers: this.corsHeaders });
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 成功レスポンス(200)
|
|
123
|
+
*/
|
|
124
|
+
ok(data) {
|
|
125
|
+
return new Response(JSON.stringify(data), {
|
|
126
|
+
status: 200,
|
|
127
|
+
headers: { ...this.corsHeaders, "Content-Type": "application/json" }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 作成成功レスポンス(201)
|
|
132
|
+
*/
|
|
133
|
+
created(data) {
|
|
134
|
+
return new Response(JSON.stringify(data), {
|
|
135
|
+
status: 201,
|
|
136
|
+
headers: { ...this.corsHeaders, "Content-Type": "application/json" }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* バリデーションエラー(400)
|
|
141
|
+
*/
|
|
142
|
+
badRequest(message) {
|
|
143
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
144
|
+
status: 400,
|
|
145
|
+
headers: { ...this.corsHeaders, "Content-Type": "application/json" }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 認証エラー(401)
|
|
150
|
+
*/
|
|
151
|
+
unauthorized(message = "Unauthorized") {
|
|
152
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
153
|
+
status: 401,
|
|
154
|
+
headers: { ...this.corsHeaders, "Content-Type": "application/json" }
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 見つからないエラー(404)
|
|
159
|
+
*/
|
|
160
|
+
notFound(message = "Not found") {
|
|
161
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
162
|
+
status: 404,
|
|
163
|
+
headers: { ...this.corsHeaders, "Content-Type": "application/json" }
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* サーバーエラー(500)
|
|
168
|
+
*/
|
|
169
|
+
serverError(message) {
|
|
170
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
171
|
+
status: 500,
|
|
172
|
+
headers: { ...this.corsHeaders, "Content-Type": "application/json" }
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// ../../workers/file-upload/src/domain/FileEntity.ts
|
|
178
|
+
var PRESET_NAMES = ["original", "retina", "large", "medium", "small", "thumbnail"];
|
|
179
|
+
function createFileEntry(params) {
|
|
180
|
+
return {
|
|
181
|
+
uuid: params.uuid,
|
|
182
|
+
originalName: params.originalName,
|
|
183
|
+
contentType: params.contentType,
|
|
184
|
+
size: params.size,
|
|
185
|
+
uploadedBy: params.uploadedBy,
|
|
186
|
+
uploadedAt: params.uploadedAt,
|
|
187
|
+
url: params.publicUrl,
|
|
188
|
+
...params.width != null && { width: params.width },
|
|
189
|
+
...params.height != null && { height: params.height },
|
|
190
|
+
...params.variants && { variants: params.variants }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function selectThumbnailUrl(variants) {
|
|
194
|
+
return variants.thumbnail || variants.small || variants.medium || variants.large || variants.original || "";
|
|
195
|
+
}
|
|
196
|
+
function recalculateYearFilesStats(yearFiles) {
|
|
197
|
+
return {
|
|
198
|
+
...yearFiles,
|
|
199
|
+
totalCount: yearFiles.files.length,
|
|
200
|
+
totalSize: yearFiles.files.reduce((sum, f) => sum + f.size, 0),
|
|
201
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function createEmptyYearFiles(year) {
|
|
205
|
+
return {
|
|
206
|
+
year,
|
|
207
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
208
|
+
totalCount: 0,
|
|
209
|
+
totalSize: 0,
|
|
210
|
+
files: []
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ../../workers/file-upload/src/usecase/DeleteFileUseCase.ts
|
|
215
|
+
var DeleteFileUseCase = class {
|
|
216
|
+
constructor(r2Repository) {
|
|
217
|
+
this.r2Repository = r2Repository;
|
|
218
|
+
}
|
|
219
|
+
async execute(params) {
|
|
220
|
+
logger.info("DeleteFileUseCase.execute", params);
|
|
221
|
+
if (params.year) {
|
|
222
|
+
const deleted2 = await this.deleteFromYear(params.uuid, params.year);
|
|
223
|
+
if (deleted2) {
|
|
224
|
+
return { success: true, uuid: params.uuid };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const deleted = await this.deleteOldFormat(params.uuid);
|
|
228
|
+
if (deleted) {
|
|
229
|
+
return { success: true, uuid: params.uuid };
|
|
230
|
+
}
|
|
231
|
+
logger.warn("DeleteFileUseCase.execute", "File not found", { uuid: params.uuid });
|
|
232
|
+
throw new Error("File not found");
|
|
233
|
+
}
|
|
234
|
+
async deleteFromYear(uuid, year) {
|
|
235
|
+
const setObjects = await this.r2Repository.list(`${year}/${uuid}/`);
|
|
236
|
+
if (setObjects.length > 0) {
|
|
237
|
+
await this.r2Repository.deleteMany(setObjects.map((o) => o.key));
|
|
238
|
+
await this.removeFromYearFiles(uuid, year);
|
|
239
|
+
logger.debug("DeleteFileUseCase", "Deleted image set", { uuid, year });
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
const singleObjects = await this.r2Repository.list(`${year}/${uuid}`);
|
|
243
|
+
for (const obj of singleObjects) {
|
|
244
|
+
if (obj.key.startsWith(`${year}/${uuid}.`) || obj.key === `${year}/${uuid}`) {
|
|
245
|
+
await this.r2Repository.delete(obj.key);
|
|
246
|
+
await this.removeFromYearFiles(uuid, year);
|
|
247
|
+
logger.debug("DeleteFileUseCase", "Deleted single file", { uuid, year });
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
async deleteOldFormat(uuid) {
|
|
254
|
+
const setObjects = await this.r2Repository.list(`${uuid}/`);
|
|
255
|
+
if (setObjects.length > 0) {
|
|
256
|
+
await this.r2Repository.deleteMany(setObjects.map((o) => o.key));
|
|
257
|
+
logger.debug("DeleteFileUseCase", "Deleted old format image set", { uuid });
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
const extensions = ["", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".pdf"];
|
|
261
|
+
for (const ext of extensions) {
|
|
262
|
+
const key = uuid + ext;
|
|
263
|
+
const obj = await this.r2Repository.head(key);
|
|
264
|
+
if (obj) {
|
|
265
|
+
await this.r2Repository.delete(key);
|
|
266
|
+
logger.debug("DeleteFileUseCase", "Deleted old format single file", { uuid, key });
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
async removeFromYearFiles(uuid, year) {
|
|
273
|
+
const yearFiles = await this.r2Repository.readYearFiles(year);
|
|
274
|
+
yearFiles.files = yearFiles.files.filter((f) => f.uuid !== uuid);
|
|
275
|
+
const updatedYearFiles = recalculateYearFilesStats(yearFiles);
|
|
276
|
+
await this.r2Repository.writeYearFiles(year, updatedYearFiles);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// ../../workers/file-upload/src/usecase/GetFileUseCase.ts
|
|
281
|
+
var GetFileUseCase = class {
|
|
282
|
+
constructor(r2Repository) {
|
|
283
|
+
this.r2Repository = r2Repository;
|
|
284
|
+
}
|
|
285
|
+
async execute(params) {
|
|
286
|
+
logger.debug("GetFileUseCase.execute", { uuid: params.uuid });
|
|
287
|
+
const extensions = ["", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".pdf"];
|
|
288
|
+
for (const ext of extensions) {
|
|
289
|
+
const key = params.uuid + ext;
|
|
290
|
+
const obj = await this.r2Repository.head(key);
|
|
291
|
+
if (obj) {
|
|
292
|
+
return {
|
|
293
|
+
uuid: params.uuid,
|
|
294
|
+
key,
|
|
295
|
+
url: key,
|
|
296
|
+
size: obj.size,
|
|
297
|
+
mimeType: obj.httpMetadata?.contentType,
|
|
298
|
+
originalName: obj.customMetadata?.originalName,
|
|
299
|
+
uploadedBy: obj.customMetadata?.uploadedBy
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
logger.warn("GetFileUseCase.execute", "File not found", { uuid: params.uuid });
|
|
304
|
+
throw new Error("File not found");
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// ../../workers/file-upload/src/usecase/ListFilesUseCase.ts
|
|
309
|
+
var ListFilesUseCase = class {
|
|
310
|
+
constructor(r2Repository) {
|
|
311
|
+
this.r2Repository = r2Repository;
|
|
312
|
+
}
|
|
313
|
+
async execute(params) {
|
|
314
|
+
const year = params.year ?? this.r2Repository.getCurrentYear();
|
|
315
|
+
logger.debug("ListFilesUseCase.execute", { year });
|
|
316
|
+
const yearFiles = await this.r2Repository.readYearFiles(year);
|
|
317
|
+
const availableYears = await this.r2Repository.getAvailableYears();
|
|
318
|
+
return {
|
|
319
|
+
year,
|
|
320
|
+
availableYears,
|
|
321
|
+
files: yearFiles.files,
|
|
322
|
+
totalSize: yearFiles.totalSize,
|
|
323
|
+
totalCount: yearFiles.totalCount,
|
|
324
|
+
baseUrl: this.r2Repository.getBaseUrl()
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async getAvailableYears() {
|
|
328
|
+
const years = await this.r2Repository.getAvailableYears();
|
|
329
|
+
const currentYear = this.r2Repository.getCurrentYear();
|
|
330
|
+
return { years, currentYear };
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// ../../workers/file-upload/src/usecase/RebuildIndexUseCase.ts
|
|
335
|
+
var RebuildIndexUseCase = class {
|
|
336
|
+
constructor(r2Repository) {
|
|
337
|
+
this.r2Repository = r2Repository;
|
|
338
|
+
}
|
|
339
|
+
async execute(params) {
|
|
340
|
+
const { year } = params;
|
|
341
|
+
logger.info("RebuildIndexUseCase.execute", { year });
|
|
342
|
+
const files = [];
|
|
343
|
+
const movedFiles = [];
|
|
344
|
+
const yearObjects = await this.r2Repository.list(`${year}/`);
|
|
345
|
+
const grouped = this.groupObjectsByUuid(yearObjects, year);
|
|
346
|
+
const allObjects = await this.r2Repository.listAll();
|
|
347
|
+
const oldFormatGrouped = this.findOldFormatFiles(allObjects, year);
|
|
348
|
+
for (const [uuid, objects] of oldFormatGrouped) {
|
|
349
|
+
for (const obj of objects) {
|
|
350
|
+
const oldKey = obj.key;
|
|
351
|
+
const correctNewKey = `${year}/${obj.key}`;
|
|
352
|
+
const content = await this.r2Repository.get(oldKey);
|
|
353
|
+
if (content) {
|
|
354
|
+
await this.r2Repository.upload({
|
|
355
|
+
key: correctNewKey,
|
|
356
|
+
content: content.body,
|
|
357
|
+
contentType: content.info.httpMetadata?.contentType || "application/octet-stream",
|
|
358
|
+
customMetadata: content.info.customMetadata
|
|
359
|
+
});
|
|
360
|
+
await this.r2Repository.delete(oldKey);
|
|
361
|
+
movedFiles.push(`${oldKey} -> ${correctNewKey}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const movedObjects = await this.r2Repository.list(`${year}/${uuid}`);
|
|
365
|
+
if (movedObjects.length > 0) {
|
|
366
|
+
grouped.set(uuid, movedObjects);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const [uuid, objects] of grouped) {
|
|
370
|
+
const entry = this.buildFileEntry(uuid, objects, year);
|
|
371
|
+
if (entry) {
|
|
372
|
+
files.push(entry);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const yearFiles = {
|
|
376
|
+
year,
|
|
377
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
378
|
+
totalCount: files.length,
|
|
379
|
+
totalSize: files.reduce((sum, f) => sum + f.size, 0),
|
|
380
|
+
files
|
|
381
|
+
};
|
|
382
|
+
await this.r2Repository.writeYearFiles(year, yearFiles);
|
|
383
|
+
logger.info("RebuildIndexUseCase.execute completed", {
|
|
384
|
+
year,
|
|
385
|
+
totalCount: files.length,
|
|
386
|
+
movedFilesCount: movedFiles.length
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
success: true,
|
|
390
|
+
year,
|
|
391
|
+
totalCount: files.length,
|
|
392
|
+
movedFiles: movedFiles.length,
|
|
393
|
+
movedFilesList: movedFiles
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
groupObjectsByUuid(objects, year) {
|
|
397
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
398
|
+
for (const obj of objects) {
|
|
399
|
+
if (obj.key === `${year}/files.json`) continue;
|
|
400
|
+
const pathAfterYear = obj.key.slice(`${year}/`.length);
|
|
401
|
+
const uuid = pathAfterYear.includes("/") ? pathAfterYear.split("/")[0] : pathAfterYear.split(".")[0];
|
|
402
|
+
if (!grouped.has(uuid)) grouped.set(uuid, []);
|
|
403
|
+
grouped.get(uuid).push(obj);
|
|
404
|
+
}
|
|
405
|
+
return grouped;
|
|
406
|
+
}
|
|
407
|
+
findOldFormatFiles(objects, year) {
|
|
408
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
409
|
+
for (const obj of objects) {
|
|
410
|
+
if (/^\d{4}\//.test(obj.key)) continue;
|
|
411
|
+
const uuid = obj.key.includes("/") ? obj.key.split("/")[0] : obj.key.split(".")[0];
|
|
412
|
+
const fileYear = obj.uploaded.getFullYear();
|
|
413
|
+
if (fileYear === year) {
|
|
414
|
+
if (!grouped.has(uuid)) grouped.set(uuid, []);
|
|
415
|
+
grouped.get(uuid).push(obj);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return grouped;
|
|
419
|
+
}
|
|
420
|
+
buildFileEntry(uuid, objects, year) {
|
|
421
|
+
const isImageSet = objects.some((o) => {
|
|
422
|
+
const pathAfterYear = o.key.slice(`${year}/`.length);
|
|
423
|
+
return pathAfterYear.includes("/");
|
|
424
|
+
});
|
|
425
|
+
if (isImageSet) {
|
|
426
|
+
const variants = {};
|
|
427
|
+
let totalSize = 0;
|
|
428
|
+
let uploadedAt = "";
|
|
429
|
+
let originalName = "";
|
|
430
|
+
let uploadedBy = "unknown";
|
|
431
|
+
for (const obj of objects) {
|
|
432
|
+
const pathAfterYear = obj.key.slice(`${year}/`.length);
|
|
433
|
+
const preset = pathAfterYear.split("/")[1]?.replace(".webp", "");
|
|
434
|
+
if (preset) variants[preset] = obj.key;
|
|
435
|
+
totalSize += obj.size;
|
|
436
|
+
uploadedAt = obj.uploaded.toISOString();
|
|
437
|
+
originalName = obj.customMetadata?.originalName || uuid;
|
|
438
|
+
uploadedBy = obj.customMetadata?.uploadedBy || "unknown";
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
uuid,
|
|
442
|
+
originalName,
|
|
443
|
+
contentType: "image/webp",
|
|
444
|
+
size: totalSize,
|
|
445
|
+
uploadedBy,
|
|
446
|
+
uploadedAt,
|
|
447
|
+
url: variants.thumbnail || variants.small || variants.medium || variants.large || variants.original || "",
|
|
448
|
+
variants
|
|
449
|
+
};
|
|
450
|
+
} else {
|
|
451
|
+
const obj = objects[0];
|
|
452
|
+
return {
|
|
453
|
+
uuid,
|
|
454
|
+
originalName: obj.customMetadata?.originalName || obj.key,
|
|
455
|
+
contentType: obj.httpMetadata?.contentType || "application/octet-stream",
|
|
456
|
+
size: obj.size,
|
|
457
|
+
uploadedBy: obj.customMetadata?.uploadedBy || "unknown",
|
|
458
|
+
uploadedAt: obj.uploaded.toISOString(),
|
|
459
|
+
url: obj.key
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// ../../workers/file-upload/src/usecase/UpdateFileMetadataUseCase.ts
|
|
466
|
+
var UpdateFileMetadataUseCase = class {
|
|
467
|
+
constructor(r2Repository) {
|
|
468
|
+
this.r2Repository = r2Repository;
|
|
469
|
+
}
|
|
470
|
+
async execute(params) {
|
|
471
|
+
logger.info("UpdateFileMetadataUseCase.execute", params);
|
|
472
|
+
const yearFiles = await this.r2Repository.readYearFiles(params.year);
|
|
473
|
+
const fileIndex = yearFiles.files.findIndex((f) => f.uuid === params.uuid);
|
|
474
|
+
if (fileIndex === -1) {
|
|
475
|
+
logger.warn("UpdateFileMetadataUseCase.execute", "File not found", { uuid: params.uuid });
|
|
476
|
+
throw new Error("File not found");
|
|
477
|
+
}
|
|
478
|
+
if (params.alt !== void 0) {
|
|
479
|
+
yearFiles.files[fileIndex].alt = params.alt || void 0;
|
|
480
|
+
}
|
|
481
|
+
yearFiles.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
482
|
+
await this.r2Repository.writeYearFiles(params.year, yearFiles);
|
|
483
|
+
logger.info("UpdateFileMetadataUseCase.execute completed", { uuid: params.uuid });
|
|
484
|
+
return yearFiles.files[fileIndex];
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// ../../workers/file-upload/src/usecase/UploadFileUseCase.ts
|
|
489
|
+
var UploadFileUseCase = class {
|
|
490
|
+
constructor(r2Repository) {
|
|
491
|
+
this.r2Repository = r2Repository;
|
|
492
|
+
}
|
|
493
|
+
async execute(params) {
|
|
494
|
+
logger.info("UploadFileUseCase.execute", {
|
|
495
|
+
fileName: params.file.name,
|
|
496
|
+
uploadedBy: params.uploadedBy
|
|
497
|
+
});
|
|
498
|
+
const uuid = crypto.randomUUID();
|
|
499
|
+
const year = this.r2Repository.getCurrentYear();
|
|
500
|
+
const extension = params.file.name.split(".").pop() || "";
|
|
501
|
+
const key = extension ? `${year}/${uuid}.${extension}` : `${year}/${uuid}`;
|
|
502
|
+
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
503
|
+
await this.r2Repository.upload({
|
|
504
|
+
key,
|
|
505
|
+
content: params.file.stream(),
|
|
506
|
+
contentType: params.file.type,
|
|
507
|
+
customMetadata: {
|
|
508
|
+
originalName: params.file.name,
|
|
509
|
+
uploadedBy: params.uploadedBy
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
const yearFiles = await this.r2Repository.readYearFiles(year);
|
|
513
|
+
const newEntry = createFileEntry({
|
|
514
|
+
uuid,
|
|
515
|
+
originalName: params.file.name,
|
|
516
|
+
contentType: params.file.type,
|
|
517
|
+
size: params.file.size,
|
|
518
|
+
uploadedBy: params.uploadedBy,
|
|
519
|
+
uploadedAt,
|
|
520
|
+
publicUrl: key
|
|
521
|
+
});
|
|
522
|
+
yearFiles.files.push(newEntry);
|
|
523
|
+
const updatedYearFiles = recalculateYearFilesStats(yearFiles);
|
|
524
|
+
await this.r2Repository.writeYearFiles(year, updatedYearFiles);
|
|
525
|
+
logger.info("UploadFileUseCase.execute completed", { uuid });
|
|
526
|
+
return newEntry;
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// ../../workers/file-upload/src/usecase/UploadImageSetUseCase.ts
|
|
531
|
+
var UploadImageSetUseCase = class {
|
|
532
|
+
constructor(r2Repository) {
|
|
533
|
+
this.r2Repository = r2Repository;
|
|
534
|
+
}
|
|
535
|
+
async execute(params) {
|
|
536
|
+
logger.info("UploadImageSetUseCase.execute", {
|
|
537
|
+
originalName: params.originalName,
|
|
538
|
+
uploadedBy: params.uploadedBy,
|
|
539
|
+
presetCount: params.files.size
|
|
540
|
+
});
|
|
541
|
+
const uuid = crypto.randomUUID();
|
|
542
|
+
const year = this.r2Repository.getCurrentYear();
|
|
543
|
+
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
544
|
+
const variants = {};
|
|
545
|
+
let totalSize = 0;
|
|
546
|
+
for (const name of PRESET_NAMES) {
|
|
547
|
+
const file = params.files.get(name);
|
|
548
|
+
if (file) {
|
|
549
|
+
const key = `${year}/${uuid}/${name}.webp`;
|
|
550
|
+
await this.r2Repository.upload({
|
|
551
|
+
key,
|
|
552
|
+
content: file.stream(),
|
|
553
|
+
contentType: "image/webp",
|
|
554
|
+
customMetadata: {
|
|
555
|
+
originalName: params.originalName,
|
|
556
|
+
uploadedBy: params.uploadedBy
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
variants[name] = key;
|
|
560
|
+
totalSize += file.size;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const thumbnailUrl = selectThumbnailUrl(variants);
|
|
564
|
+
const yearFiles = await this.r2Repository.readYearFiles(year);
|
|
565
|
+
const newEntry = createFileEntry({
|
|
566
|
+
uuid,
|
|
567
|
+
originalName: params.originalName,
|
|
568
|
+
contentType: "image/webp",
|
|
569
|
+
size: totalSize,
|
|
570
|
+
uploadedBy: params.uploadedBy,
|
|
571
|
+
uploadedAt,
|
|
572
|
+
publicUrl: thumbnailUrl,
|
|
573
|
+
width: params.width,
|
|
574
|
+
height: params.height,
|
|
575
|
+
variants
|
|
576
|
+
});
|
|
577
|
+
yearFiles.files.push(newEntry);
|
|
578
|
+
const updatedYearFiles = recalculateYearFilesStats(yearFiles);
|
|
579
|
+
await this.r2Repository.writeYearFiles(year, updatedYearFiles);
|
|
580
|
+
logger.info("UploadImageSetUseCase.execute completed", { uuid, variantCount: Object.keys(variants).length });
|
|
581
|
+
return newEntry;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* プリウォーム用の URL 一覧を取得
|
|
585
|
+
*/
|
|
586
|
+
getPrewarmUrls(variants) {
|
|
587
|
+
return Object.values(variants);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// ../../workers/file-upload/src/controller/FileController.ts
|
|
592
|
+
var FileController = class {
|
|
593
|
+
presenter;
|
|
594
|
+
uploadFileUseCase;
|
|
595
|
+
uploadImageSetUseCase;
|
|
596
|
+
deleteFileUseCase;
|
|
597
|
+
listFilesUseCase;
|
|
598
|
+
getFileUseCase;
|
|
599
|
+
updateFileMetadataUseCase;
|
|
600
|
+
rebuildIndexUseCase;
|
|
601
|
+
constructor(r2Repository, corsHeaders) {
|
|
602
|
+
this.presenter = new HttpPresenterImpl(corsHeaders);
|
|
603
|
+
this.uploadFileUseCase = new UploadFileUseCase(r2Repository);
|
|
604
|
+
this.uploadImageSetUseCase = new UploadImageSetUseCase(r2Repository);
|
|
605
|
+
this.deleteFileUseCase = new DeleteFileUseCase(r2Repository);
|
|
606
|
+
this.listFilesUseCase = new ListFilesUseCase(r2Repository);
|
|
607
|
+
this.getFileUseCase = new GetFileUseCase(r2Repository);
|
|
608
|
+
this.updateFileMetadataUseCase = new UpdateFileMetadataUseCase(r2Repository);
|
|
609
|
+
this.rebuildIndexUseCase = new RebuildIndexUseCase(r2Repository);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* CORS プリフライト
|
|
613
|
+
*/
|
|
614
|
+
handleCors() {
|
|
615
|
+
return this.presenter.cors();
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* POST /upload - 単一ファイルのアップロード
|
|
619
|
+
*/
|
|
620
|
+
async handleUpload(request) {
|
|
621
|
+
const formData = await request.formData();
|
|
622
|
+
const file = formData.get("file");
|
|
623
|
+
const uploadedBy = formData.get("uploadedBy");
|
|
624
|
+
if (!file) {
|
|
625
|
+
return this.presenter.badRequest("No file provided");
|
|
626
|
+
}
|
|
627
|
+
if (!uploadedBy) {
|
|
628
|
+
return this.presenter.badRequest("uploadedBy is required");
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const result = await this.uploadFileUseCase.execute({ file, uploadedBy });
|
|
632
|
+
return this.presenter.created(result);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
logger.error("FileController.handleUpload", { error });
|
|
635
|
+
return this.presenter.serverError(error.message);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* POST /upload-set - 画像セットのアップロード
|
|
640
|
+
*/
|
|
641
|
+
async handleUploadSet(request, ctx) {
|
|
642
|
+
const formData = await request.formData();
|
|
643
|
+
const uploadedBy = formData.get("uploadedBy");
|
|
644
|
+
const originalName = formData.get("originalName");
|
|
645
|
+
if (!uploadedBy) {
|
|
646
|
+
return this.presenter.badRequest("uploadedBy is required");
|
|
647
|
+
}
|
|
648
|
+
if (!originalName) {
|
|
649
|
+
return this.presenter.badRequest("originalName is required");
|
|
650
|
+
}
|
|
651
|
+
const files = /* @__PURE__ */ new Map();
|
|
652
|
+
const presetNames = ["original", "retina", "large", "medium", "small", "thumbnail"];
|
|
653
|
+
for (const name of presetNames) {
|
|
654
|
+
const file = formData.get(name);
|
|
655
|
+
if (file) {
|
|
656
|
+
files.set(name, file);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (files.size === 0) {
|
|
660
|
+
return this.presenter.badRequest("No image files provided");
|
|
661
|
+
}
|
|
662
|
+
const widthStr = formData.get("width");
|
|
663
|
+
const heightStr = formData.get("height");
|
|
664
|
+
const width = widthStr ? parseInt(widthStr) : void 0;
|
|
665
|
+
const height = heightStr ? parseInt(heightStr) : void 0;
|
|
666
|
+
try {
|
|
667
|
+
const result = await this.uploadImageSetUseCase.execute({ originalName, uploadedBy, files, width, height });
|
|
668
|
+
const prewarmUrls = this.uploadImageSetUseCase.getPrewarmUrls(result.variants);
|
|
669
|
+
ctx.waitUntil(this.prewarmUrls(prewarmUrls));
|
|
670
|
+
return this.presenter.created(result);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
logger.error("FileController.handleUploadSet", { error });
|
|
673
|
+
return this.presenter.serverError(error.message);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* GET /years - 利用可能な年一覧
|
|
678
|
+
*/
|
|
679
|
+
async handleGetYears() {
|
|
680
|
+
try {
|
|
681
|
+
const result = await this.listFilesUseCase.getAvailableYears();
|
|
682
|
+
return this.presenter.ok(result);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
logger.error("FileController.handleGetYears", { error });
|
|
685
|
+
return this.presenter.serverError(error.message);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* GET /list - ファイル一覧
|
|
690
|
+
*/
|
|
691
|
+
async handleListFiles(url) {
|
|
692
|
+
const yearParam = url.searchParams.get("year");
|
|
693
|
+
const year = yearParam ? parseInt(yearParam) : void 0;
|
|
694
|
+
try {
|
|
695
|
+
const result = await this.listFilesUseCase.execute({ year });
|
|
696
|
+
return this.presenter.ok(result);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
logger.error("FileController.handleListFiles", { error });
|
|
699
|
+
return this.presenter.serverError(error.message);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* GET /files/:uuid - ファイル情報
|
|
704
|
+
*/
|
|
705
|
+
async handleGetFile(uuid) {
|
|
706
|
+
try {
|
|
707
|
+
const result = await this.getFileUseCase.execute({ uuid });
|
|
708
|
+
return this.presenter.ok(result);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
if (error.message === "File not found") {
|
|
711
|
+
return this.presenter.notFound("File not found");
|
|
712
|
+
}
|
|
713
|
+
logger.error("FileController.handleGetFile", { error });
|
|
714
|
+
return this.presenter.serverError(error.message);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* PATCH /files/:uuid - メタデータ更新
|
|
719
|
+
*/
|
|
720
|
+
async handleUpdateFileMetadata(uuid, request) {
|
|
721
|
+
const body = await request.json();
|
|
722
|
+
const { year, alt } = body;
|
|
723
|
+
if (year === void 0) {
|
|
724
|
+
return this.presenter.badRequest("year is required");
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
const result = await this.updateFileMetadataUseCase.execute({ uuid, year, alt });
|
|
728
|
+
return this.presenter.ok(result);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (error.message === "File not found") {
|
|
731
|
+
return this.presenter.notFound("File not found");
|
|
732
|
+
}
|
|
733
|
+
logger.error("FileController.handleUpdateFileMetadata", { error });
|
|
734
|
+
return this.presenter.serverError(error.message);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* DELETE /files/:uuid - ファイル削除
|
|
739
|
+
*/
|
|
740
|
+
async handleDeleteFile(uuid, url) {
|
|
741
|
+
const yearParam = url.searchParams.get("year");
|
|
742
|
+
const year = yearParam ? parseInt(yearParam) : void 0;
|
|
743
|
+
try {
|
|
744
|
+
const result = await this.deleteFileUseCase.execute({ uuid, year });
|
|
745
|
+
return this.presenter.ok(result);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
if (error.message === "File not found") {
|
|
748
|
+
return this.presenter.notFound("File not found");
|
|
749
|
+
}
|
|
750
|
+
logger.error("FileController.handleDeleteFile", { error });
|
|
751
|
+
return this.presenter.serverError(error.message);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* POST /rebuild/:year - インデックス再構築
|
|
756
|
+
*/
|
|
757
|
+
async handleRebuildIndex(year) {
|
|
758
|
+
try {
|
|
759
|
+
const result = await this.rebuildIndexUseCase.execute({ year });
|
|
760
|
+
return this.presenter.ok(result);
|
|
761
|
+
} catch (error) {
|
|
762
|
+
logger.error("FileController.handleRebuildIndex", { error });
|
|
763
|
+
return this.presenter.serverError(error.message);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Not Found
|
|
768
|
+
*/
|
|
769
|
+
handleNotFound() {
|
|
770
|
+
return this.presenter.notFound();
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* プリウォーム: CDN キャッシュを温める
|
|
774
|
+
*/
|
|
775
|
+
async prewarmUrls(urls) {
|
|
776
|
+
const results = await Promise.allSettled(
|
|
777
|
+
urls.map(
|
|
778
|
+
(url) => fetch(url, {
|
|
779
|
+
method: "HEAD",
|
|
780
|
+
cf: { cacheTtl: 86400 }
|
|
781
|
+
})
|
|
782
|
+
)
|
|
783
|
+
);
|
|
784
|
+
logger.debug(
|
|
785
|
+
"FileController.prewarmUrls",
|
|
786
|
+
`Completed: ${results.filter((r) => r.status === "fulfilled").length}/${urls.length}`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// ../../workers/file-upload/src/domain/AuthEntity.ts
|
|
792
|
+
var AuthError = class extends Error {
|
|
793
|
+
constructor(message, statusCode) {
|
|
794
|
+
super(message);
|
|
795
|
+
this.statusCode = statusCode;
|
|
796
|
+
this.name = "AuthError";
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// ../../workers/file-upload/src/repository/GitHubAuthRepositoryImpl.ts
|
|
801
|
+
var tokenCache = /* @__PURE__ */ new Map();
|
|
802
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
803
|
+
var GitHubAuthRepositoryImpl = class {
|
|
804
|
+
async validateToken(token) {
|
|
805
|
+
const cached = tokenCache.get(token);
|
|
806
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
807
|
+
return cached.user;
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const res = await fetch("https://api.github.com/user", {
|
|
811
|
+
headers: {
|
|
812
|
+
Authorization: `Bearer ${token}`,
|
|
813
|
+
Accept: "application/vnd.github.v3+json",
|
|
814
|
+
"User-Agent": "frelio-file-upload"
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
if (!res.ok) {
|
|
818
|
+
logger.warn("GitHubAuthRepositoryImpl.validateToken", {
|
|
819
|
+
status: res.status
|
|
820
|
+
});
|
|
821
|
+
tokenCache.delete(token);
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const data = await res.json();
|
|
825
|
+
const user = { login: data.login };
|
|
826
|
+
tokenCache.set(token, {
|
|
827
|
+
user,
|
|
828
|
+
expiresAt: Date.now() + CACHE_TTL_MS
|
|
829
|
+
});
|
|
830
|
+
return user;
|
|
831
|
+
} catch (error) {
|
|
832
|
+
logger.error("GitHubAuthRepositoryImpl.validateToken", { error });
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// ../../workers/file-upload/src/repository/R2RepositoryImpl.ts
|
|
839
|
+
var R2RepositoryImpl = class {
|
|
840
|
+
constructor(bucket, publicUrl) {
|
|
841
|
+
this.bucket = bucket;
|
|
842
|
+
this.publicUrl = publicUrl;
|
|
843
|
+
}
|
|
844
|
+
async upload(params) {
|
|
845
|
+
logger.debug("R2RepositoryImpl.upload", { key: params.key });
|
|
846
|
+
await this.bucket.put(params.key, params.content, {
|
|
847
|
+
httpMetadata: {
|
|
848
|
+
contentType: params.contentType
|
|
849
|
+
},
|
|
850
|
+
customMetadata: params.customMetadata
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
async delete(key) {
|
|
854
|
+
logger.debug("R2RepositoryImpl.delete", { key });
|
|
855
|
+
await this.bucket.delete(key);
|
|
856
|
+
}
|
|
857
|
+
async deleteMany(keys) {
|
|
858
|
+
logger.debug("R2RepositoryImpl.deleteMany", { count: keys.length });
|
|
859
|
+
await Promise.all(keys.map((key) => this.bucket.delete(key)));
|
|
860
|
+
}
|
|
861
|
+
async head(key) {
|
|
862
|
+
const obj = await this.bucket.head(key);
|
|
863
|
+
if (!obj) return null;
|
|
864
|
+
return this.toObjectInfo(obj);
|
|
865
|
+
}
|
|
866
|
+
async get(key) {
|
|
867
|
+
const obj = await this.bucket.get(key);
|
|
868
|
+
if (!obj) return null;
|
|
869
|
+
return {
|
|
870
|
+
body: obj.body,
|
|
871
|
+
info: this.toObjectInfo(obj)
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
async list(prefix) {
|
|
875
|
+
const result = [];
|
|
876
|
+
let cursor;
|
|
877
|
+
do {
|
|
878
|
+
const listed = await this.bucket.list({
|
|
879
|
+
prefix,
|
|
880
|
+
cursor,
|
|
881
|
+
include: ["httpMetadata", "customMetadata"]
|
|
882
|
+
});
|
|
883
|
+
for (const obj of listed.objects) {
|
|
884
|
+
result.push(this.toObjectInfo(obj));
|
|
885
|
+
}
|
|
886
|
+
cursor = listed.truncated ? listed.cursor : void 0;
|
|
887
|
+
} while (cursor);
|
|
888
|
+
return result;
|
|
889
|
+
}
|
|
890
|
+
async listAll() {
|
|
891
|
+
const result = [];
|
|
892
|
+
let cursor;
|
|
893
|
+
do {
|
|
894
|
+
const listed = await this.bucket.list({
|
|
895
|
+
cursor,
|
|
896
|
+
include: ["httpMetadata", "customMetadata"]
|
|
897
|
+
});
|
|
898
|
+
for (const obj of listed.objects) {
|
|
899
|
+
result.push(this.toObjectInfo(obj));
|
|
900
|
+
}
|
|
901
|
+
cursor = listed.truncated ? listed.cursor : void 0;
|
|
902
|
+
} while (cursor);
|
|
903
|
+
return result;
|
|
904
|
+
}
|
|
905
|
+
async readYearFiles(year) {
|
|
906
|
+
const key = `${year}/files.json`;
|
|
907
|
+
const obj = await this.bucket.get(key);
|
|
908
|
+
if (!obj) {
|
|
909
|
+
return createEmptyYearFiles(year);
|
|
910
|
+
}
|
|
911
|
+
return JSON.parse(await obj.text());
|
|
912
|
+
}
|
|
913
|
+
async writeYearFiles(year, data) {
|
|
914
|
+
const key = `${year}/files.json`;
|
|
915
|
+
logger.debug("R2RepositoryImpl.writeYearFiles", { year, fileCount: data.files.length });
|
|
916
|
+
await this.bucket.put(key, JSON.stringify(data, null, 2), {
|
|
917
|
+
httpMetadata: { contentType: "application/json" }
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
async getAvailableYears() {
|
|
921
|
+
const years = /* @__PURE__ */ new Set();
|
|
922
|
+
const objects = await this.listAll();
|
|
923
|
+
for (const obj of objects) {
|
|
924
|
+
const match = obj.key.match(/^(\d{4})\//);
|
|
925
|
+
if (match) {
|
|
926
|
+
years.add(parseInt(match[1]));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
return Array.from(years).sort((a, b) => b - a);
|
|
930
|
+
}
|
|
931
|
+
getCurrentYear() {
|
|
932
|
+
return (/* @__PURE__ */ new Date()).getFullYear();
|
|
933
|
+
}
|
|
934
|
+
getBaseUrl() {
|
|
935
|
+
return this.publicUrl;
|
|
936
|
+
}
|
|
937
|
+
toObjectInfo(obj) {
|
|
938
|
+
return {
|
|
939
|
+
key: obj.key,
|
|
940
|
+
size: obj.size,
|
|
941
|
+
uploaded: obj.uploaded,
|
|
942
|
+
httpMetadata: obj.httpMetadata ? {
|
|
943
|
+
contentType: obj.httpMetadata.contentType
|
|
944
|
+
} : void 0,
|
|
945
|
+
customMetadata: obj.customMetadata
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// ../../workers/file-upload/src/usecase/ValidateAuthUseCase.ts
|
|
951
|
+
var ValidateAuthUseCase = class {
|
|
952
|
+
constructor(authRepository) {
|
|
953
|
+
this.authRepository = authRepository;
|
|
954
|
+
}
|
|
955
|
+
async execute(authHeader) {
|
|
956
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
957
|
+
throw new AuthError("Missing or invalid Authorization header", 401);
|
|
958
|
+
}
|
|
959
|
+
const token = authHeader.slice(7);
|
|
960
|
+
const user = await this.authRepository.validateToken(token);
|
|
961
|
+
if (!user) {
|
|
962
|
+
throw new AuthError("Invalid token", 401);
|
|
963
|
+
}
|
|
964
|
+
return user;
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
// functions/api/storage/_middleware.ts
|
|
969
|
+
var onRequest = async (context) => {
|
|
970
|
+
const requestId = crypto.randomUUID().slice(0, 8);
|
|
971
|
+
logger.setRequestId(requestId);
|
|
972
|
+
try {
|
|
973
|
+
const origin = context.request.headers.get("Origin") ?? "";
|
|
974
|
+
const allowedOrigins = context.env.ALLOWED_ORIGINS?.split(",").map((s) => s.trim()) ?? [];
|
|
975
|
+
const isSameOrigin = origin === new URL(context.request.url).origin;
|
|
976
|
+
const matchedOrigin = isSameOrigin || allowedOrigins.includes(origin) ? origin : null;
|
|
977
|
+
const corsHeaders = {
|
|
978
|
+
"Access-Control-Allow-Origin": matchedOrigin ?? "",
|
|
979
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
980
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
981
|
+
Vary: "Origin"
|
|
982
|
+
};
|
|
983
|
+
if (context.request.method === "OPTIONS") {
|
|
984
|
+
return new Response(null, { headers: corsHeaders });
|
|
985
|
+
}
|
|
986
|
+
const authRepository = new GitHubAuthRepositoryImpl();
|
|
987
|
+
const validateAuth = new ValidateAuthUseCase(authRepository);
|
|
988
|
+
const presenter = new HttpPresenterImpl(corsHeaders);
|
|
989
|
+
try {
|
|
990
|
+
await validateAuth.execute(context.request.headers.get("Authorization"));
|
|
991
|
+
} catch (error) {
|
|
992
|
+
if (error instanceof AuthError) {
|
|
993
|
+
return presenter.unauthorized(error.message);
|
|
994
|
+
}
|
|
995
|
+
throw error;
|
|
996
|
+
}
|
|
997
|
+
const r2Repository = new R2RepositoryImpl(context.env.R2, context.env.R2_PUBLIC_URL);
|
|
998
|
+
context.data.controller = new FileController(r2Repository, corsHeaders);
|
|
999
|
+
return await context.next();
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
logger.error("StorageMiddleware", "Unhandled error", { error });
|
|
1002
|
+
return new Response(JSON.stringify({ error: error.message }), {
|
|
1003
|
+
status: 500,
|
|
1004
|
+
headers: { "Content-Type": "application/json" }
|
|
1005
|
+
});
|
|
1006
|
+
} finally {
|
|
1007
|
+
logger.clearRequestId();
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
export {
|
|
1011
|
+
onRequest
|
|
1012
|
+
};
|