@filebox/s3-server 1.0.0 → 1.0.1

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/dist/index.mjs ADDED
@@ -0,0 +1,1341 @@
1
+ // src/index.ts
2
+ import bodyParser from "body-parser";
3
+ import express from "express";
4
+ import http from "http";
5
+ import https from "https";
6
+
7
+ // src/context.ts
8
+ var createContext = (req) => {
9
+ const authorization = req.headers?.authorization?.split(" ")[1];
10
+ const url = new URL(req.url, `http://${req.headers.host}`);
11
+ const path4 = url.pathname;
12
+ const query = {};
13
+ url.searchParams.forEach((value, key) => {
14
+ query[key] = value;
15
+ });
16
+ const ctx = {
17
+ req,
18
+ method: (req.method || "").toLowerCase(),
19
+ path: path4,
20
+ query,
21
+ auth: { accessKeyId: void 0, secretAccessKey: void 0 },
22
+ // 获取请求头
23
+ get(field) {
24
+ const req2 = this.req;
25
+ switch (field = field.toLowerCase()) {
26
+ case "referer":
27
+ case "referrer":
28
+ return req2.headers.referrer || req2.headers.referer || "";
29
+ default:
30
+ return req2.headers[field] || "";
31
+ }
32
+ },
33
+ // 解析S3请求参数
34
+ getBucketAndKey() {
35
+ const parts = this.path.split("/").filter(Boolean);
36
+ const bucket = parts[0];
37
+ const key = parts.slice(1).join("/");
38
+ return { bucket, key };
39
+ }
40
+ };
41
+ if (authorization) {
42
+ try {
43
+ const decoded = Buffer.from(authorization, "base64").toString("utf8");
44
+ const [accessKeyId, secretAccessKey] = decoded.split(":");
45
+ ctx.auth = { accessKeyId, secretAccessKey };
46
+ } catch (error) {
47
+ }
48
+ }
49
+ return ctx;
50
+ };
51
+ var context_default = createContext;
52
+
53
+ // src/responses.ts
54
+ import crypto from "crypto";
55
+ import { Builder } from "xml2js";
56
+ var Responses = class {
57
+ constructor() {
58
+ this.xmlBuilder = new Builder({
59
+ xmldec: {
60
+ version: "1.0",
61
+ encoding: "utf-8"
62
+ }
63
+ });
64
+ }
65
+ /**
66
+ * 发送XML响应
67
+ * @param {object} res - Express响应对象
68
+ * @param {object} xmlObject - 要转换为XML的对象
69
+ * @param {number} status - HTTP状态码
70
+ */
71
+ async sendXmlResponse(res, xmlObject, status = 200) {
72
+ if (res.headersSent) {
73
+ return;
74
+ }
75
+ const response = this.xmlBuilder.buildObject(xmlObject);
76
+ res.set("Content-Type", "application/xml; charset=utf-8");
77
+ res.set("Content-Length", Buffer.byteLength(response).toString());
78
+ res.set("Date", (/* @__PURE__ */ new Date()).toUTCString());
79
+ res.status(status);
80
+ return new Promise((resolve) => {
81
+ res.end(response, () => {
82
+ resolve();
83
+ });
84
+ });
85
+ }
86
+ /**
87
+ * 发送错误响应
88
+ * @param {object} res - Express响应对象
89
+ * @param {number} status - HTTP状态码
90
+ * @param {string} code - 错误代码
91
+ * @param {string} message - 错误消息
92
+ */
93
+ async error(res, status, code, message) {
94
+ return this.sendXmlResponse(
95
+ res,
96
+ {
97
+ Error: {
98
+ Code: code,
99
+ Message: message
100
+ }
101
+ },
102
+ status
103
+ );
104
+ }
105
+ /**
106
+ * 列出所有存储桶
107
+ * @param {object} res - Express响应对象
108
+ * @param {Array} buckets - 存储桶列表
109
+ * @param {object} owner - 所有者信息
110
+ */
111
+ async listBuckets(res, buckets, owner) {
112
+ return this.sendXmlResponse(res, {
113
+ ListAllMyBucketsResult: {
114
+ $: { xmlns: "http://s3.amazonaws.com/doc/2006-03-01/" },
115
+ Buckets: {
116
+ Bucket: buckets.map((bucket) => ({
117
+ CreationDate: new Date(bucket.creationDate).toISOString(),
118
+ Name: bucket.name
119
+ }))
120
+ },
121
+ Owner: {
122
+ ID: crypto.createHash("sha256").update(owner.id).digest("hex"),
123
+ DisplayName: owner.displayName || ""
124
+ }
125
+ }
126
+ });
127
+ }
128
+ /**
129
+ * 列出对象(V2)
130
+ * @param {object} res - Express响应对象
131
+ * @param {string} prefix - 前缀
132
+ * @param {Array} objects - 对象列表
133
+ * @param {Array} commonPrefixes - 公共前缀
134
+ * @param {string} bucket - 存储桶名称
135
+ * @param {string} delimiter - 分隔符
136
+ */
137
+ async listObjectsV2(res, prefix, objects, commonPrefixes, bucket, delimiter) {
138
+ return this.sendXmlResponse(res, {
139
+ ListBucketResult: {
140
+ IsTruncated: false,
141
+ Contents: objects.map((object) => ({
142
+ Key: object.path,
143
+ LastModified: new Date(object.mtimeMs).toISOString(),
144
+ Size: object.size.toString(),
145
+ ETag: `"${object.uuid || crypto.randomUUID()}"`,
146
+ StorageClass: "STANDARD"
147
+ })),
148
+ CommonPrefixes: commonPrefixes.map((prefix2) => ({
149
+ Prefix: prefix2
150
+ })),
151
+ KeyCount: objects.length.toString(),
152
+ Prefix: prefix,
153
+ Delimiter: delimiter,
154
+ MaxKeys: 1e3,
155
+ Name: bucket
156
+ }
157
+ });
158
+ }
159
+ /**
160
+ * 删除对象响应
161
+ * @param {object} res - Express响应对象
162
+ * @param {Array} deleted - 已删除的对象
163
+ * @param {Array} errors - 错误信息
164
+ */
165
+ async deleteObjects(res, deleted, errors) {
166
+ return this.sendXmlResponse(res, {
167
+ DeleteResult: {
168
+ Deleted: deleted.map((del) => ({
169
+ Key: del.Key
170
+ })),
171
+ Error: errors.map((error) => ({
172
+ Key: error.Key,
173
+ Code: error.Code,
174
+ Message: error.Message
175
+ }))
176
+ }
177
+ });
178
+ }
179
+ /**
180
+ * 获取存储桶位置
181
+ * @param {object} res - Express响应对象
182
+ * @param {string} region - 区域
183
+ */
184
+ async getBucketLocation(res, region) {
185
+ return this.sendXmlResponse(res, {
186
+ LocationConstraint: region || ""
187
+ });
188
+ }
189
+ /**
190
+ * 成功响应(200 OK)
191
+ * @param {object} res - Express响应对象
192
+ */
193
+ async ok(res) {
194
+ if (res.headersSent) {
195
+ return;
196
+ }
197
+ res.set("Content-Type", "application/xml; charset=utf-8");
198
+ res.set("Content-Length", "0");
199
+ res.set("Date", (/* @__PURE__ */ new Date()).toUTCString());
200
+ res.status(200);
201
+ return new Promise((resolve) => {
202
+ res.end("", () => {
203
+ resolve();
204
+ });
205
+ });
206
+ }
207
+ /**
208
+ * 无内容响应(204 No Content)
209
+ * @param {object} res - Express响应对象
210
+ */
211
+ async noContent(res) {
212
+ if (res.headersSent) {
213
+ return;
214
+ }
215
+ res.set("Content-Type", "application/xml; charset=utf-8");
216
+ res.set("Content-Length", "0");
217
+ res.set("Date", (/* @__PURE__ */ new Date()).toUTCString());
218
+ res.status(204);
219
+ return new Promise((resolve) => {
220
+ res.end("", () => {
221
+ resolve();
222
+ });
223
+ });
224
+ }
225
+ };
226
+ var responses_default = new Responses();
227
+
228
+ // src/handlers/createBucket.ts
229
+ var CreateBucket = class {
230
+ constructor(server) {
231
+ this.server = server;
232
+ this.handle = this.handle.bind(this);
233
+ }
234
+ /**
235
+ * 处理CreateBucket请求
236
+ * @param {object} req - Express请求对象
237
+ * @param {object} res - Express响应对象
238
+ */
239
+ async handle(req, res) {
240
+ try {
241
+ const { bucket } = this.extractBucketAndKey(req);
242
+ if (!bucket) {
243
+ await responses_default.error(
244
+ res,
245
+ 400,
246
+ "InvalidBucketName",
247
+ "\u65E0\u6548\u7684\u5B58\u50A8\u6876\u540D\u79F0"
248
+ );
249
+ return;
250
+ }
251
+ if (!this.isValidBucketName(bucket)) {
252
+ await responses_default.error(
253
+ res,
254
+ 400,
255
+ "InvalidBucketName",
256
+ "\u5B58\u50A8\u6876\u540D\u79F0\u4E0D\u7B26\u5408\u89C4\u8303"
257
+ );
258
+ return;
259
+ }
260
+ const bucketExists = await this.server.driver.bucketExists(bucket);
261
+ if (bucketExists) {
262
+ if (await this.server.driver.isBucketOwner(bucket)) {
263
+ res.set("Location", `/${bucket}`);
264
+ await responses_default.ok(res);
265
+ return;
266
+ }
267
+ await responses_default.error(
268
+ res,
269
+ 409,
270
+ "BucketAlreadyExists",
271
+ "\u5B58\u50A8\u6876\u5DF2\u5B58\u5728\u4E14\u4E0D\u5C5E\u4E8E\u60A8"
272
+ );
273
+ return;
274
+ }
275
+ await this.server.driver.createBucket(bucket);
276
+ res.set("Location", `/${bucket}`);
277
+ await responses_default.ok(res);
278
+ } catch (error) {
279
+ console.error("Error in CreateBucket:", error);
280
+ await responses_default.error(res, 500, "InternalError", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF");
281
+ }
282
+ }
283
+ /**
284
+ * 从请求参数中提取存储桶和键
285
+ * @param {object} req - Express请求对象
286
+ * @returns {object} 包含存储桶和键的对象
287
+ */
288
+ extractBucketAndKey(req) {
289
+ const url = new URL(req.url, `http://${req.headers.host}`);
290
+ const pathParts = url.pathname.split("/").filter(Boolean);
291
+ return {
292
+ bucket: pathParts[0],
293
+ key: pathParts.slice(1).join("/")
294
+ };
295
+ }
296
+ /**
297
+ * 验证存储桶名称是否符合规范
298
+ * @param {string} name - 存储桶名称
299
+ * @returns {boolean} 是否有效
300
+ */
301
+ isValidBucketName(name) {
302
+ if (!name || name.length < 3 || name.length > 63) {
303
+ return false;
304
+ }
305
+ if (!/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/.test(name)) {
306
+ return false;
307
+ }
308
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) {
309
+ return false;
310
+ }
311
+ if (name.includes("..") || name.includes(".-") || name.includes("-.")) {
312
+ return false;
313
+ }
314
+ return true;
315
+ }
316
+ };
317
+
318
+ // src/handlers/deleteBucket.ts
319
+ var DeleteBucket = class {
320
+ constructor(server) {
321
+ this.server = server;
322
+ this.handle = this.handle.bind(this);
323
+ }
324
+ /**
325
+ * 处理DeleteBucket请求
326
+ * @param {object} req - Express请求对象
327
+ * @param {object} res - Express响应对象
328
+ */
329
+ async handle(req, res) {
330
+ try {
331
+ const { bucket } = this.extractBucketAndKey(req);
332
+ if (!bucket) {
333
+ await responses_default.error(
334
+ res,
335
+ 400,
336
+ "InvalidBucketName",
337
+ "\u65E0\u6548\u7684\u5B58\u50A8\u6876\u540D\u79F0"
338
+ );
339
+ return;
340
+ }
341
+ const bucketExists = await this.server.driver.bucketExists(bucket);
342
+ if (!bucketExists) {
343
+ await responses_default.error(res, 404, "NoSuchBucket", "\u5B58\u50A8\u6876\u4E0D\u5B58\u5728");
344
+ return;
345
+ }
346
+ const isEmpty = await this.server.driver.isBucketEmpty(bucket);
347
+ if (!isEmpty) {
348
+ await responses_default.error(res, 409, "BucketNotEmpty", "\u5B58\u50A8\u6876\u4E0D\u4E3A\u7A7A");
349
+ return;
350
+ }
351
+ const isOwner = await this.server.driver.isBucketOwner(bucket);
352
+ if (!isOwner) {
353
+ await responses_default.error(res, 403, "AccessDenied", "\u6CA1\u6709\u6743\u9650\u5220\u9664\u6B64\u5B58\u50A8\u6876");
354
+ return;
355
+ }
356
+ await this.server.driver.deleteBucket(bucket);
357
+ await responses_default.noContent(res);
358
+ } catch (error) {
359
+ console.error("Error in DeleteBucket:", error);
360
+ await responses_default.error(res, 500, "InternalError", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF");
361
+ }
362
+ }
363
+ /**
364
+ * 从请求参数中提取存储桶和键
365
+ * @param {object} req - Express请求对象
366
+ * @returns {object} 包含存储桶和键的对象
367
+ */
368
+ extractBucketAndKey(req) {
369
+ const url = new URL(req.url, `http://${req.headers.host}`);
370
+ const pathParts = url.pathname.split("/").filter(Boolean);
371
+ return {
372
+ bucket: pathParts[0],
373
+ key: pathParts.slice(1).join("/")
374
+ };
375
+ }
376
+ };
377
+
378
+ // src/handlers/deleteObject.ts
379
+ var DeleteObject = class {
380
+ constructor(server) {
381
+ this.server = server;
382
+ this.handle = this.handle.bind(this);
383
+ }
384
+ /**
385
+ * 处理DeleteObject请求
386
+ * @param {object} req - Express请求对象
387
+ * @param {object} res - Express响应对象
388
+ */
389
+ async handle(req, res) {
390
+ try {
391
+ const { bucket, key } = this.extractBucketAndKey(req);
392
+ if (!bucket) {
393
+ await responses_default.error(res, 404, "NoSuchBucket", "\u5B58\u50A8\u6876\u4E0D\u5B58\u5728");
394
+ return;
395
+ }
396
+ if (!key) {
397
+ await responses_default.error(res, 400, "InvalidRequest", "\u65E0\u6548\u7684\u5BF9\u8C61\u952E");
398
+ return;
399
+ }
400
+ const objectPath = `/${bucket}/${key}`;
401
+ const stats = await this.server.driver.stat(objectPath);
402
+ if (!stats) {
403
+ await responses_default.noContent(res);
404
+ return;
405
+ }
406
+ if (stats.isDirectory) {
407
+ await responses_default.error(res, 400, "InvalidRequest", "\u8BF7\u6C42\u7684\u952E\u662F\u4E00\u4E2A\u76EE\u5F55");
408
+ return;
409
+ }
410
+ await this.server.driver.unlink(objectPath);
411
+ await responses_default.noContent(res);
412
+ } catch (error) {
413
+ console.error("Error in DeleteObject:", error);
414
+ await responses_default.error(res, 500, "InternalError", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF");
415
+ }
416
+ }
417
+ /**
418
+ * 从请求参数中提取存储桶和键
419
+ * @param {object} req - Express请求对象
420
+ * @returns {object} 包含存储桶和键的对象
421
+ */
422
+ extractBucketAndKey(req) {
423
+ const url = new URL(req.url, `http://${req.headers.host}`);
424
+ const pathParts = url.pathname.split("/").filter(Boolean);
425
+ return {
426
+ bucket: pathParts[0],
427
+ key: pathParts.slice(1).join("/")
428
+ };
429
+ }
430
+ };
431
+
432
+ // src/handlers/deleteObjects.ts
433
+ import { parseStringPromise } from "xml2js";
434
+ var DeleteObjects = class {
435
+ constructor(server) {
436
+ this.server = server;
437
+ this.handle = this.handle.bind(this);
438
+ }
439
+ /**
440
+ * 处理DeleteObjects请求
441
+ * @param {object} req - Express请求对象
442
+ * @param {object} res - Express响应对象
443
+ */
444
+ async handle(req, res) {
445
+ try {
446
+ const { bucket } = this.extractBucketAndKey(req);
447
+ if (!bucket) {
448
+ await responses_default.error(res, 404, "NoSuchBucket", "\u5B58\u50A8\u6876\u4E0D\u5B58\u5728");
449
+ return;
450
+ }
451
+ const bucketExists = await this.server.driver.bucketExists(bucket);
452
+ if (!bucketExists) {
453
+ await responses_default.error(res, 404, "NoSuchBucket", "\u5B58\u50A8\u6876\u4E0D\u5B58\u5728");
454
+ return;
455
+ }
456
+ const body = await this.getRequestBody(req);
457
+ let deleteData;
458
+ try {
459
+ const parsedXml = await parseStringPromise(body);
460
+ deleteData = parsedXml.Delete;
461
+ } catch (error) {
462
+ await responses_default.error(res, 400, "MalformedXML", "XML\u683C\u5F0F\u9519\u8BEF");
463
+ return;
464
+ }
465
+ if (!deleteData || !deleteData.Object || !Array.isArray(deleteData.Object)) {
466
+ await responses_default.error(res, 400, "MalformedXML", "\u65E0\u6548\u7684\u5220\u9664\u8BF7\u6C42");
467
+ return;
468
+ }
469
+ const objectsToDelete = deleteData.Object.map((obj) => ({
470
+ Key: obj.Key[0]
471
+ }));
472
+ const deleted = [];
473
+ const errors = [];
474
+ for (const obj of objectsToDelete) {
475
+ try {
476
+ const objectPath = `/${bucket}/${obj.Key}`;
477
+ const stats = await this.server.driver.stat(objectPath);
478
+ if (!stats) {
479
+ deleted.push({ Key: obj.Key });
480
+ continue;
481
+ }
482
+ if (stats.isDirectory) {
483
+ errors.push({
484
+ Key: obj.Key,
485
+ Code: "InvalidRequest",
486
+ Message: "\u8BF7\u6C42\u7684\u952E\u662F\u4E00\u4E2A\u76EE\u5F55"
487
+ });
488
+ continue;
489
+ }
490
+ await this.server.driver.unlink(objectPath);
491
+ deleted.push({ Key: obj.Key });
492
+ } catch (error) {
493
+ errors.push({
494
+ Key: obj.Key,
495
+ Code: "InternalError",
496
+ Message: "\u5220\u9664\u5BF9\u8C61\u65F6\u53D1\u751F\u9519\u8BEF"
497
+ });
498
+ }
499
+ }
500
+ await responses_default.deleteObjects(res, deleted, errors);
501
+ } catch (error) {
502
+ console.error("Error in DeleteObjects:", error);
503
+ await responses_default.error(res, 500, "InternalError", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF");
504
+ }
505
+ }
506
+ /**
507
+ * 从请求参数中提取存储桶和键
508
+ * @param {object} req - Express请求对象
509
+ * @returns {object} 包含存储桶和键的对象
510
+ */
511
+ extractBucketAndKey(req) {
512
+ const url = new URL(req.url, `http://${req.headers.host}`);
513
+ const pathParts = url.pathname.split("/").filter(Boolean);
514
+ return {
515
+ bucket: pathParts[0],
516
+ key: pathParts.slice(1).join("/")
517
+ };
518
+ }
519
+ /**
520
+ * 获取请求体内容
521
+ * @param {object} req - Express请求对象
522
+ * @returns {Promise<string>} 请求体内容
523
+ */
524
+ getRequestBody(req) {
525
+ return new Promise((resolve, reject) => {
526
+ let body = "";
527
+ req.on("data", (chunk) => {
528
+ body += chunk.toString();
529
+ });
530
+ req.on("end", () => {
531
+ resolve(body);
532
+ });
533
+ req.on("error", (error) => {
534
+ reject(error);
535
+ });
536
+ });
537
+ }
538
+ };
539
+
540
+ // src/handlers/getObject.ts
541
+ import crypto2 from "crypto";
542
+ import path from "path";
543
+ var GetObject = class {
544
+ constructor(server) {
545
+ this.server = server;
546
+ this.handle = this.handle.bind(this);
547
+ }
548
+ /**
549
+ * 处理GetObject请求
550
+ * @param {object} req - Express请求对象
551
+ * @param {object} res - Express响应对象
552
+ */
553
+ async handle(req, res) {
554
+ try {
555
+ const { bucket, key } = this.extractBucketAndKey(req);
556
+ if (!bucket) {
557
+ await responses_default.error(res, 404, "NoSuchBucket", "\u5B58\u50A8\u6876\u4E0D\u5B58\u5728");
558
+ return;
559
+ }
560
+ if (!key) {
561
+ await responses_default.error(res, 400, "InvalidRequest", "\u65E0\u6548\u7684\u5BF9\u8C61\u952E");
562
+ return;
563
+ }
564
+ const objectPath = `/${bucket}/${key}`;
565
+ const stats = await this.server.driver.stat(objectPath);
566
+ if (!stats) {
567
+ await responses_default.error(res, 404, "NoSuchKey", "\u5BF9\u8C61\u4E0D\u5B58\u5728");
568
+ return;
569
+ }
570
+ if (stats.isDirectory) {
571
+ await responses_default.error(res, 400, "InvalidRequest", "\u8BF7\u6C42\u7684\u952E\u662F\u4E00\u4E2A\u76EE\u5F55");
572
+ return;
573
+ }
574
+ res.set("Content-Type", this.getContentType(key));
575
+ res.set("Content-Length", stats.size.toString());
576
+ res.set("Last-Modified", new Date(stats.mtime).toUTCString());
577
+ res.set("ETag", `"${stats.uuid || crypto2.randomUUID()}"`);
578
+ res.set("Accept-Ranges", "bytes");
579
+ res.set("Date", (/* @__PURE__ */ new Date()).toUTCString());
580
+ if (req.headers.range) {
581
+ const range2 = this.parseRange(req.headers.range, stats.size);
582
+ if (range2) {
583
+ res.set(
584
+ "Content-Range",
585
+ `bytes ${range2.start}-${range2.end}/${stats.size}`
586
+ );
587
+ res.set("Content-Length", (range2.end - range2.start + 1).toString());
588
+ res.status(206);
589
+ } else {
590
+ res.set("Content-Range", `bytes */${stats.size}`);
591
+ res.status(416);
592
+ res.end();
593
+ return;
594
+ }
595
+ } else {
596
+ res.status(200);
597
+ }
598
+ const stream = await this.server.driver.createReadStream(objectPath, {
599
+ start: range?.start,
600
+ end: range?.end
601
+ });
602
+ stream.pipe(res);
603
+ stream.on("error", async (error) => {
604
+ console.error("Error streaming object:", error);
605
+ if (!res.headersSent) {
606
+ await responses_default.error(
607
+ res,
608
+ 500,
609
+ "InternalError",
610
+ "\u8BFB\u53D6\u5BF9\u8C61\u65F6\u53D1\u751F\u9519\u8BEF"
611
+ );
612
+ } else {
613
+ res.end();
614
+ }
615
+ });
616
+ } catch (error) {
617
+ console.error("Error in GetObject:", error);
618
+ if (!res.headersSent) {
619
+ await responses_default.error(res, 500, "InternalError", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF");
620
+ }
621
+ }
622
+ }
623
+ /**
624
+ * 从请求参数中提取存储桶和键
625
+ * @param {object} req - Express请求对象
626
+ * @returns {object} 包含存储桶和键的对象
627
+ */
628
+ extractBucketAndKey(req) {
629
+ const url = new URL(req.url, `http://${req.headers.host}`);
630
+ const pathParts = url.pathname.split("/").filter(Boolean);
631
+ return {
632
+ bucket: pathParts[0],
633
+ key: pathParts.slice(1).join("/")
634
+ };
635
+ }
636
+ /**
637
+ * 根据文件扩展名获取内容类型
638
+ * @param {string} filename - 文件名
639
+ * @returns {string} 内容类型
640
+ */
641
+ getContentType(filename) {
642
+ const ext = path.extname(filename).toLowerCase();
643
+ const mimeTypes = {
644
+ ".html": "text/html",
645
+ ".htm": "text/html",
646
+ ".css": "text/css",
647
+ ".js": "application/javascript",
648
+ ".json": "application/json",
649
+ ".png": "image/png",
650
+ ".jpg": "image/jpeg",
651
+ ".jpeg": "image/jpeg",
652
+ ".gif": "image/gif",
653
+ ".webp": "image/webp",
654
+ ".svg": "image/svg+xml",
655
+ ".pdf": "application/pdf",
656
+ ".txt": "text/plain",
657
+ ".xml": "application/xml",
658
+ ".zip": "application/zip",
659
+ ".mp3": "audio/mpeg",
660
+ ".mp4": "video/mp4",
661
+ ".webm": "video/webm",
662
+ ".doc": "application/msword",
663
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
664
+ ".xls": "application/vnd.ms-excel",
665
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
666
+ ".ppt": "application/vnd.ms-powerpoint",
667
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
668
+ };
669
+ return mimeTypes[ext] || "application/octet-stream";
670
+ }
671
+ /**
672
+ * 解析Range头
673
+ * @param {string} rangeHeader - Range头值
674
+ * @param {number} size - 文件大小
675
+ * @returns {object|null} 范围对象或null
676
+ */
677
+ parseRange(rangeHeader, size) {
678
+ const matches = rangeHeader.match(/bytes=(\d+)-(\d*)/);
679
+ if (!matches) return null;
680
+ const start = parseInt(matches[1], 10);
681
+ const end = matches[2] ? parseInt(matches[2], 10) : size - 1;
682
+ if (isNaN(start) || isNaN(end) || start > end || start >= size || end >= size) {
683
+ return null;
684
+ }
685
+ return { start, end };
686
+ }
687
+ };
688
+
689
+ // src/handlers/headBucket.ts
690
+ var HeadBucket = class {
691
+ constructor(server) {
692
+ this.server = server;
693
+ this.handle = this.handle.bind(this);
694
+ }
695
+ /**
696
+ * 处理HeadBucket请求
697
+ * @param {object} req - Express请求对象
698
+ * @param {object} res - Express响应对象
699
+ */
700
+ async handle(req, res) {
701
+ try {
702
+ const { bucket } = this.extractBucketAndKey(req);
703
+ if (!bucket) {
704
+ res.status(404).end();
705
+ return;
706
+ }
707
+ const bucketExists = await this.server.driver.bucketExists(bucket);
708
+ if (!bucketExists) {
709
+ res.status(404).end();
710
+ return;
711
+ }
712
+ const hasAccess = await this.server.driver.hasBucketAccess(bucket);
713
+ if (!hasAccess) {
714
+ res.status(403).end();
715
+ return;
716
+ }
717
+ res.set("Date", (/* @__PURE__ */ new Date()).toUTCString());
718
+ res.set("x-amz-bucket-region", this.server.config.region || "default");
719
+ res.status(200).end();
720
+ } catch (error) {
721
+ console.error("Error in HeadBucket:", error);
722
+ res.status(500).end();
723
+ }
724
+ }
725
+ /**
726
+ * 从请求参数中提取存储桶和键
727
+ * @param {object} req - Express请求对象
728
+ * @returns {object} 包含存储桶和键的对象
729
+ */
730
+ extractBucketAndKey(req) {
731
+ const url = new URL(req.url, `http://${req.headers.host}`);
732
+ const pathParts = url.pathname.split("/").filter(Boolean);
733
+ return {
734
+ bucket: pathParts[0],
735
+ key: pathParts.slice(1).join("/")
736
+ };
737
+ }
738
+ };
739
+
740
+ // src/handlers/headObject.ts
741
+ import crypto3 from "crypto";
742
+ import path2 from "path";
743
+ var HeadObject = class {
744
+ constructor(server) {
745
+ this.server = server;
746
+ this.handle = this.handle.bind(this);
747
+ }
748
+ /**
749
+ * 处理HeadObject请求
750
+ * @param {object} req - Express请求对象
751
+ * @param {object} res - Express响应对象
752
+ */
753
+ async handle(req, res) {
754
+ try {
755
+ const { bucket, key } = this.extractBucketAndKey(req);
756
+ if (!bucket) {
757
+ res.status(404).end();
758
+ return;
759
+ }
760
+ if (!key) {
761
+ res.status(400).end();
762
+ return;
763
+ }
764
+ const objectPath = `/${bucket}/${key}`;
765
+ const stats = await this.server.driver.stat(objectPath);
766
+ if (!stats) {
767
+ res.status(404).end();
768
+ return;
769
+ }
770
+ if (stats.isDirectory) {
771
+ res.status(400).end();
772
+ return;
773
+ }
774
+ res.set("Content-Type", this.getContentType(key));
775
+ res.set("Content-Length", stats.size.toString());
776
+ res.set("Last-Modified", new Date(stats.mtime).toUTCString());
777
+ res.set("ETag", `"${stats.uuid || crypto3.randomUUID()}"`);
778
+ res.set("Accept-Ranges", "bytes");
779
+ res.set("Date", (/* @__PURE__ */ new Date()).toUTCString());
780
+ const metadata = await this.server.driver.getObjectMetadata(objectPath);
781
+ if (metadata) {
782
+ Object.entries(metadata).forEach(([key2, value]) => {
783
+ if (key2.startsWith("x-amz-meta-")) {
784
+ res.set(key2, value);
785
+ }
786
+ });
787
+ }
788
+ res.status(200).end();
789
+ } catch (error) {
790
+ console.error("Error in HeadObject:", error);
791
+ res.status(500).end();
792
+ }
793
+ }
794
+ /**
795
+ * 从请求参数中提取存储桶和键
796
+ * @param {object} req - Express请求对象
797
+ * @returns {object} 包含存储桶和键的对象
798
+ */
799
+ extractBucketAndKey(req) {
800
+ const url = new URL(req.url, `http://${req.headers.host}`);
801
+ const pathParts = url.pathname.split("/").filter(Boolean);
802
+ return {
803
+ bucket: pathParts[0],
804
+ key: pathParts.slice(1).join("/")
805
+ };
806
+ }
807
+ /**
808
+ * 根据文件扩展名获取内容类型
809
+ * @param {string} filename - 文件名
810
+ * @returns {string} 内容类型
811
+ */
812
+ getContentType(filename) {
813
+ const ext = path2.extname(filename).toLowerCase();
814
+ const mimeTypes = {
815
+ ".html": "text/html",
816
+ ".htm": "text/html",
817
+ ".css": "text/css",
818
+ ".js": "application/javascript",
819
+ ".json": "application/json",
820
+ ".png": "image/png",
821
+ ".jpg": "image/jpeg",
822
+ ".jpeg": "image/jpeg",
823
+ ".gif": "image/gif",
824
+ ".webp": "image/webp",
825
+ ".svg": "image/svg+xml",
826
+ ".pdf": "application/pdf",
827
+ ".txt": "text/plain",
828
+ ".xml": "application/xml",
829
+ ".zip": "application/zip",
830
+ ".mp3": "audio/mpeg",
831
+ ".mp4": "video/mp4",
832
+ ".webm": "video/webm",
833
+ ".doc": "application/msword",
834
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
835
+ ".xls": "application/vnd.ms-excel",
836
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
837
+ ".ppt": "application/vnd.ms-powerpoint",
838
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
839
+ };
840
+ return mimeTypes[ext] || "application/octet-stream";
841
+ }
842
+ };
843
+
844
+ // src/handlers/listBuckets.ts
845
+ var ListBuckets = class {
846
+ constructor(server) {
847
+ this.server = server;
848
+ this.handle = this.handle.bind(this);
849
+ }
850
+ /**
851
+ * 处理ListBuckets请求
852
+ * @param {object} req - Express请求对象
853
+ * @param {object} res - Express响应对象
854
+ */
855
+ async handle(req, res) {
856
+ try {
857
+ const buckets = await this.server.driver.listBuckets();
858
+ const owner = {
859
+ id: this.server.config.accessKeyId || "anonymous",
860
+ displayName: this.server.config.displayName || "S3 User"
861
+ };
862
+ await responses_default.listBuckets(res, buckets, owner);
863
+ } catch (error) {
864
+ console.error("Error in ListBuckets:", error);
865
+ await responses_default.error(res, 500, "InternalError", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF");
866
+ }
867
+ }
868
+ };
869
+
870
+ // src/handlers/listObjects.ts
871
+ import crypto4 from "crypto";
872
+ import path3 from "path";
873
+ var ListObjects = class {
874
+ constructor(server) {
875
+ this.server = server;
876
+ this.handle = this.handle.bind(this);
877
+ }
878
+ /**
879
+ * 解析请求查询参数
880
+ * @param {object} req - Express请求对象
881
+ * @returns {object} 解析后的参数
882
+ */
883
+ parseQueryParams(req) {
884
+ if (!req || !req.query) {
885
+ return { prefix: "", delimiter: "" };
886
+ }
887
+ return {
888
+ prefix: typeof req.query.prefix === "string" && req.query.prefix.length > 0 ? req.query.prefix : "",
889
+ delimiter: typeof req.query.delimiter === "string" && req.query.delimiter.length > 0 ? req.query.delimiter : ""
890
+ };
891
+ }
892
+ /**
893
+ * 规范化前缀
894
+ * @param {string} prefix - 原始前缀
895
+ * @returns {string} 规范化后的前缀
896
+ */
897
+ normalizePrefix(prefix) {
898
+ let trimmed = decodeURIComponent(prefix).trim();
899
+ if (trimmed.length === 0 || trimmed === "/" || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.includes("../")) {
900
+ return "/";
901
+ }
902
+ if (!trimmed.startsWith("/")) {
903
+ trimmed = `/${trimmed}`;
904
+ }
905
+ return trimmed;
906
+ }
907
+ /**
908
+ * 处理ListObjects请求
909
+ * @param {object} req - Express请求对象
910
+ * @param {object} res - Express响应对象
911
+ */
912
+ async handle(req, res) {
913
+ try {
914
+ if (req.url.includes("?location")) {
915
+ await responses_default.getBucketLocation(
916
+ res,
917
+ this.server.config.region || "default"
918
+ );
919
+ return;
920
+ }
921
+ const { bucket } = this.extractBucketAndKey(req);
922
+ if (!bucket) {
923
+ await responses_default.error(res, 404, "NoSuchBucket", "\u5B58\u50A8\u6876\u4E0D\u5B58\u5728");
924
+ return;
925
+ }
926
+ const params = this.parseQueryParams(req);
927
+ const normalizedPrefix = this.normalizePrefix(params.prefix);
928
+ let dirname = this.normalizePrefix(
929
+ normalizedPrefix === "/" ? `/${bucket}` : path3.posix.dirname(path3.posix.join(bucket, normalizedPrefix))
930
+ );
931
+ const requestedPath = this.normalizePrefix(
932
+ path3.posix.join(bucket, normalizedPrefix)
933
+ );
934
+ const requestedPathStats = await this.server.driver.stat(requestedPath);
935
+ if (requestedPathStats && requestedPathStats.isDirectory) {
936
+ dirname = requestedPath;
937
+ }
938
+ const dirnameStats = await this.server.driver.stat(dirname);
939
+ if (!dirnameStats) {
940
+ await responses_default.listObjectsV2(
941
+ res,
942
+ params.prefix,
943
+ [],
944
+ [],
945
+ bucket,
946
+ params.delimiter
947
+ );
948
+ return;
949
+ }
950
+ const depth = params.delimiter.length === 0 ? 10 : 0;
951
+ const dirContent = await this.readdir(dirname, depth);
952
+ const objects = [];
953
+ const commonPrefixes = [];
954
+ for (const item of dirContent) {
955
+ const itemPath = path3.posix.join(dirname, item);
956
+ const itemStats = await this.server.driver.stat(itemPath);
957
+ if (!itemStats) continue;
958
+ const relativePath = itemPath.startsWith(`/${bucket}/`) ? itemPath.slice(`/${bucket}/`.length) : itemPath;
959
+ if (itemStats.isDirectory) {
960
+ commonPrefixes.push(`${relativePath}/`);
961
+ } else {
962
+ objects.push({
963
+ path: relativePath,
964
+ mtimeMs: itemStats.mtime || Date.now(),
965
+ size: itemStats.size || 0,
966
+ uuid: itemStats.uuid || crypto4.randomUUID()
967
+ });
968
+ }
969
+ }
970
+ await responses_default.listObjectsV2(
971
+ res,
972
+ normalizedPrefix === "/" ? "/" : normalizedPrefix.slice(1),
973
+ objects,
974
+ commonPrefixes,
975
+ bucket,
976
+ params.delimiter
977
+ );
978
+ } catch (error) {
979
+ console.error("Error in ListObjects:", error);
980
+ await responses_default.error(res, 500, "InternalError", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF");
981
+ }
982
+ }
983
+ /**
984
+ * 从请求参数中提取存储桶和键
985
+ * @param {object} req - Express请求对象
986
+ * @returns {object} 包含存储桶和键的对象
987
+ */
988
+ extractBucketAndKey(req) {
989
+ const url = new URL(req.url, `http://${req.headers.host}`);
990
+ const pathParts = url.pathname.split("/").filter(Boolean);
991
+ return {
992
+ bucket: pathParts[0],
993
+ key: pathParts.slice(1).join("/")
994
+ };
995
+ }
996
+ /**
997
+ * 读取目录内容
998
+ * @param {string} dirPath - 目录路径
999
+ * @param {number} depth - 递归深度
1000
+ * @returns {Promise<Array>} 目录内容列表
1001
+ */
1002
+ async readdir(dirPath, depth) {
1003
+ try {
1004
+ if (depth <= 0) {
1005
+ return await this.server.driver.readdir(dirPath);
1006
+ }
1007
+ const items = [];
1008
+ const traverse = async (currentPath, currentDepth) => {
1009
+ if (currentDepth >= depth) {
1010
+ return;
1011
+ }
1012
+ const entries = await this.server.driver.readdir(currentPath);
1013
+ for (const entry of entries) {
1014
+ const entryPath = path3.posix.join(currentPath, entry);
1015
+ const stats = await this.server.driver.stat(entryPath);
1016
+ items.push(entryPath.slice(dirPath.length));
1017
+ if (stats && stats.isDirectory) {
1018
+ await traverse(entryPath, currentDepth + 1);
1019
+ }
1020
+ }
1021
+ };
1022
+ await traverse(dirPath, 0);
1023
+ return items;
1024
+ } catch (error) {
1025
+ console.error("Error reading directory:", error);
1026
+ return [];
1027
+ }
1028
+ }
1029
+ };
1030
+
1031
+ // src/handlers/putObject.ts
1032
+ import crypto5 from "crypto";
1033
+ var PutObject = class {
1034
+ constructor(server) {
1035
+ this.server = server;
1036
+ this.handle = this.handle.bind(this);
1037
+ }
1038
+ /**
1039
+ * 处理PutObject请求
1040
+ * @param {object} req - Express请求对象
1041
+ * @param {object} res - Express响应对象
1042
+ */
1043
+ async handle(req, res) {
1044
+ try {
1045
+ const { bucket, key } = this.extractBucketAndKey(req);
1046
+ if (!bucket) {
1047
+ await responses_default.error(res, 404, "NoSuchBucket", "\u5B58\u50A8\u6876\u4E0D\u5B58\u5728");
1048
+ return;
1049
+ }
1050
+ if (!key) {
1051
+ await responses_default.error(res, 400, "InvalidRequest", "\u65E0\u6548\u7684\u5BF9\u8C61\u952E");
1052
+ return;
1053
+ }
1054
+ const bucketExists = await this.server.driver.bucketExists(bucket);
1055
+ if (!bucketExists) {
1056
+ await responses_default.error(res, 404, "NoSuchBucket", "\u5B58\u50A8\u6876\u4E0D\u5B58\u5728");
1057
+ return;
1058
+ }
1059
+ const objectPath = `/${bucket}/${key}`;
1060
+ const contentLength = parseInt(req.headers["content-length"] || "0", 10);
1061
+ if (isNaN(contentLength) || contentLength < 0) {
1062
+ await responses_default.error(
1063
+ res,
1064
+ 400,
1065
+ "InvalidRequest",
1066
+ "\u65E0\u6548\u7684Content-Length"
1067
+ );
1068
+ return;
1069
+ }
1070
+ const writeStream = await this.server.driver.createWriteStream(objectPath);
1071
+ const md5Hash = crypto5.createHash("md5");
1072
+ let bytesWritten = 0;
1073
+ await new Promise((resolve, reject) => {
1074
+ let settled = false;
1075
+ const rejectOnce = (error) => {
1076
+ if (settled) return;
1077
+ settled = true;
1078
+ writeStream.destroy();
1079
+ reject(error);
1080
+ };
1081
+ const complete = async () => {
1082
+ if (settled) return;
1083
+ settled = true;
1084
+ try {
1085
+ const etag = md5Hash.digest("hex");
1086
+ await this.server.driver.updateObjectMetadata(objectPath, {
1087
+ etag,
1088
+ contentType: req.headers["content-type"] || "application/octet-stream",
1089
+ contentLength: bytesWritten,
1090
+ lastModified: /* @__PURE__ */ new Date()
1091
+ });
1092
+ res.set("ETag", `"${etag}"`);
1093
+ res.set("Date", (/* @__PURE__ */ new Date()).toUTCString());
1094
+ res.status(200).end();
1095
+ resolve();
1096
+ } catch (error) {
1097
+ reject(error);
1098
+ }
1099
+ };
1100
+ writeStream.on("error", rejectOnce);
1101
+ const rawBody = req.body;
1102
+ if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
1103
+ const chunk = Buffer.from(rawBody);
1104
+ if (chunk.length > 0) {
1105
+ writeStream.write(chunk);
1106
+ md5Hash.update(chunk);
1107
+ bytesWritten += chunk.length;
1108
+ }
1109
+ writeStream.end(() => void complete());
1110
+ return;
1111
+ }
1112
+ if (typeof rawBody === "string") {
1113
+ const chunk = Buffer.from(rawBody);
1114
+ if (chunk.length > 0) {
1115
+ writeStream.write(chunk);
1116
+ md5Hash.update(chunk);
1117
+ bytesWritten += chunk.length;
1118
+ }
1119
+ writeStream.end(() => void complete());
1120
+ return;
1121
+ }
1122
+ req.on("data", (chunk) => {
1123
+ writeStream.write(chunk);
1124
+ md5Hash.update(chunk);
1125
+ bytesWritten += chunk.length;
1126
+ });
1127
+ req.on("end", () => {
1128
+ writeStream.end(() => void complete());
1129
+ });
1130
+ req.on("error", rejectOnce);
1131
+ });
1132
+ } catch (error) {
1133
+ console.error("Error in PutObject:", error);
1134
+ if (!res.headersSent) {
1135
+ await responses_default.error(res, 500, "InternalError", "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF");
1136
+ }
1137
+ }
1138
+ }
1139
+ /**
1140
+ * 从请求参数中提取存储桶和键
1141
+ * @param {object} req - Express请求对象
1142
+ * @returns {object} 包含存储桶和键的对象
1143
+ */
1144
+ extractBucketAndKey(req) {
1145
+ const url = new URL(req.url, `http://${req.headers.host}`);
1146
+ const pathParts = url.pathname.split("/").filter(Boolean);
1147
+ return {
1148
+ bucket: pathParts[0],
1149
+ key: pathParts.slice(1).join("/")
1150
+ };
1151
+ }
1152
+ };
1153
+
1154
+ // src/index.ts
1155
+ var S3Server = class {
1156
+ /**
1157
+ * 创建S3服务器实例
1158
+ * @param {Object} config - 服务器配置
1159
+ * @param {Object} driver - 存储驱动
1160
+ */
1161
+ constructor(config, driver) {
1162
+ this.config = {
1163
+ hostname: config.hostname || "0.0.0.0",
1164
+ port: config.port || 9e3,
1165
+ https: config.https || false,
1166
+ region: config.region || "default",
1167
+ accessKeyId: config.accessKeyId || "accessKey1",
1168
+ secretAccessKey: config.secretAccessKey || "verySecretKey1",
1169
+ displayName: config.displayName || "S3 User"
1170
+ };
1171
+ this.driver = driver;
1172
+ this.app = express();
1173
+ this.server = null;
1174
+ this.handlers = {
1175
+ listBuckets: new ListBuckets(this),
1176
+ listObjects: new ListObjects(this),
1177
+ getObject: new GetObject(this),
1178
+ putObject: new PutObject(this),
1179
+ deleteObject: new DeleteObject(this),
1180
+ deleteObjects: new DeleteObjects(this),
1181
+ headObject: new HeadObject(this),
1182
+ headBucket: new HeadBucket(this),
1183
+ createBucket: new CreateBucket(this),
1184
+ deleteBucket: new DeleteBucket(this)
1185
+ };
1186
+ this.setupMiddleware();
1187
+ this.setupRoutes();
1188
+ }
1189
+ /**
1190
+ * 配置Express中间件
1191
+ */
1192
+ setupMiddleware() {
1193
+ this.app.use(
1194
+ bodyParser.raw({
1195
+ type: "*/*",
1196
+ limit: "10gb"
1197
+ })
1198
+ );
1199
+ this.app.use((req, res, next) => {
1200
+ const ctx = context_default(req);
1201
+ req.s3Context = ctx;
1202
+ if (this.config.accessKeyId && this.config.secretAccessKey) {
1203
+ const { accessKeyId, secretAccessKey } = ctx.auth;
1204
+ if (accessKeyId !== this.config.accessKeyId || secretAccessKey !== this.config.secretAccessKey) {
1205
+ res.status(403).send({
1206
+ Error: {
1207
+ Code: "AccessDenied",
1208
+ Message: "\u8BBF\u95EE\u88AB\u62D2\u7EDD"
1209
+ }
1210
+ });
1211
+ return;
1212
+ }
1213
+ }
1214
+ next();
1215
+ });
1216
+ this.app.use((req, res, next) => {
1217
+ console.log(`${(/* @__PURE__ */ new Date()).toISOString()} - ${req.method} ${req.url}`);
1218
+ next();
1219
+ });
1220
+ }
1221
+ /**
1222
+ * 配置API路由
1223
+ */
1224
+ setupRoutes() {
1225
+ this.app.get("/", (req, res) => {
1226
+ this.handlers.listBuckets.handle(req, res);
1227
+ });
1228
+ this.app.head("/:bucket", (req, res) => {
1229
+ this.handlers.headBucket.handle(req, res);
1230
+ });
1231
+ this.app.get("/:bucket", (req, res) => {
1232
+ this.handlers.listObjects.handle(req, res);
1233
+ });
1234
+ this.app.put("/:bucket", (req, res) => {
1235
+ this.handlers.createBucket.handle(req, res);
1236
+ });
1237
+ this.app.delete("/:bucket", (req, res) => {
1238
+ this.handlers.deleteBucket.handle(req, res);
1239
+ });
1240
+ this.app.head("/:bucket/*", (req, res) => {
1241
+ this.handlers.headObject.handle(req, res);
1242
+ });
1243
+ this.app.get("/:bucket/*", (req, res) => {
1244
+ this.handlers.getObject.handle(req, res);
1245
+ });
1246
+ this.app.put("/:bucket/*", (req, res) => {
1247
+ this.handlers.putObject.handle(req, res);
1248
+ });
1249
+ this.app.delete("/:bucket/*", (req, res) => {
1250
+ this.handlers.deleteObject.handle(req, res);
1251
+ });
1252
+ this.app.post("/:bucket", (req, res) => {
1253
+ if (req.query.delete !== void 0) {
1254
+ this.handlers.deleteObjects.handle(req, res);
1255
+ } else {
1256
+ res.status(400).send({
1257
+ Error: {
1258
+ Code: "InvalidRequest",
1259
+ Message: "\u65E0\u6548\u7684\u8BF7\u6C42"
1260
+ }
1261
+ });
1262
+ }
1263
+ });
1264
+ this.app.use((req, res) => {
1265
+ res.status(404).send({
1266
+ Error: {
1267
+ Code: "NotFound",
1268
+ Message: "\u8BF7\u6C42\u7684\u8D44\u6E90\u4E0D\u5B58\u5728"
1269
+ }
1270
+ });
1271
+ });
1272
+ this.app.use((err, req, res, next) => {
1273
+ console.error("Server error:", err);
1274
+ res.status(500).send({
1275
+ Error: {
1276
+ Code: "InternalError",
1277
+ Message: "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF"
1278
+ }
1279
+ });
1280
+ });
1281
+ }
1282
+ /**
1283
+ * 启动服务器
1284
+ * @returns {Promise<void>}
1285
+ */
1286
+ async start() {
1287
+ return new Promise((resolve, reject) => {
1288
+ try {
1289
+ if (this.config.https) {
1290
+ const options = {
1291
+ key: "...",
1292
+ // 私钥
1293
+ cert: "..."
1294
+ // 证书
1295
+ };
1296
+ this.server = https.createServer(options, this.app);
1297
+ } else {
1298
+ this.server = http.createServer(this.app);
1299
+ }
1300
+ this.server.listen(this.config.port, this.config.hostname, () => {
1301
+ console.log(
1302
+ `S3\u670D\u52A1\u5668\u5DF2\u542F\u52A8: ${this.config.https ? "https" : "http"}://${this.config.hostname}:${this.config.port}`
1303
+ );
1304
+ resolve();
1305
+ });
1306
+ this.server.on("error", (err) => {
1307
+ console.error("\u670D\u52A1\u5668\u542F\u52A8\u5931\u8D25:", err);
1308
+ reject(err);
1309
+ });
1310
+ } catch (error) {
1311
+ console.error("\u542F\u52A8\u670D\u52A1\u5668\u65F6\u53D1\u751F\u9519\u8BEF:", error);
1312
+ reject(error);
1313
+ }
1314
+ });
1315
+ }
1316
+ /**
1317
+ * 停止服务器
1318
+ * @returns {Promise<void>}
1319
+ */
1320
+ async stop() {
1321
+ return new Promise((resolve, reject) => {
1322
+ if (!this.server) {
1323
+ resolve();
1324
+ return;
1325
+ }
1326
+ this.server.close((err) => {
1327
+ if (err) {
1328
+ console.error("\u505C\u6B62\u670D\u52A1\u5668\u65F6\u53D1\u751F\u9519\u8BEF:", err);
1329
+ reject(err);
1330
+ return;
1331
+ }
1332
+ console.log("S3\u670D\u52A1\u5668\u5DF2\u505C\u6B62");
1333
+ this.server = null;
1334
+ resolve();
1335
+ });
1336
+ });
1337
+ }
1338
+ };
1339
+ export {
1340
+ S3Server as default
1341
+ };