@exabugs/dynamodb-client 1.5.2 → 1.6.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/CHANGELOG.md +15 -0
- package/dist/server/handler.cjs +10855 -6862
- package/dist/server/handler.cjs.map +4 -4
- package/dist/server/media/magicBytes.d.ts +25 -0
- package/dist/server/media/magicBytes.d.ts.map +1 -0
- package/dist/server/media/magicBytes.js +53 -0
- package/dist/server/media/magicBytes.js.map +1 -0
- package/dist/server/media/media-handler.d.ts +13 -0
- package/dist/server/media/media-handler.d.ts.map +1 -0
- package/dist/server/media/media-handler.js +177 -0
- package/dist/server/media/media-handler.js.map +1 -0
- package/dist/server/media/metadata.d.ts +29 -0
- package/dist/server/media/metadata.d.ts.map +1 -0
- package/dist/server/media/metadata.js +57 -0
- package/dist/server/media/metadata.js.map +1 -0
- package/dist/server/media/presign.d.ts +9 -0
- package/dist/server/media/presign.d.ts.map +1 -0
- package/dist/server/media/presign.js +90 -0
- package/dist/server/media/presign.js.map +1 -0
- package/dist/server/media/process-handler-dlq.d.ts +3 -0
- package/dist/server/media/process-handler-dlq.d.ts.map +1 -0
- package/dist/server/media/process-handler-dlq.js +94 -0
- package/dist/server/media/process-handler-dlq.js.map +1 -0
- package/dist/server/media/process-handler.d.ts +12 -0
- package/dist/server/media/process-handler.d.ts.map +1 -0
- package/dist/server/media/process-handler.js +153 -0
- package/dist/server/media/process-handler.js.map +1 -0
- package/dist/server/media/sign.d.ts +11 -0
- package/dist/server/media/sign.d.ts.map +1 -0
- package/dist/server/media/sign.js +49 -0
- package/dist/server/media/sign.js.map +1 -0
- package/dist/server/media/ssmParams.d.ts +29 -0
- package/dist/server/media/ssmParams.d.ts.map +1 -0
- package/dist/server/media/ssmParams.js +74 -0
- package/dist/server/media/ssmParams.js.map +1 -0
- package/dist/server/media/types.d.ts +109 -0
- package/dist/server/media/types.d.ts.map +1 -0
- package/dist/server/media/types.js +2 -0
- package/dist/server/media/types.js.map +1 -0
- package/dist/server/media/upload-handler.d.ts +50 -0
- package/dist/server/media/upload-handler.d.ts.map +1 -0
- package/dist/server/media/upload-handler.js +115 -0
- package/dist/server/media/upload-handler.js.map +1 -0
- package/dist/server/media-handler.cjs +57474 -0
- package/dist/server/media-handler.cjs.map +7 -0
- package/dist/server/media-process-handler-dlq.cjs +48138 -0
- package/dist/server/media-process-handler-dlq.cjs.map +7 -0
- package/dist/server/media-process-handler.cjs +60680 -0
- package/dist/server/media-process-handler.cjs.map +7 -0
- package/dist/sharp-layer.zip +0 -0
- package/package.json +38 -3
- package/terraform/main.tf +3 -2
- package/terraform/media/README.md +67 -0
- package/terraform/media/main.tf +791 -0
- package/terraform/media/outputs.tf +66 -0
- package/terraform/media/variables.tf +117 -0
- package/terraform/modules/parameter-store/main.tf +26 -1
- package/terraform/modules/parameter-store/variables.tf +7 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* マジックナンバー照合
|
|
3
|
+
*
|
|
4
|
+
* アップロード時に申告されたcontentTypeが、実際のバイト列と一致するかを検証する
|
|
5
|
+
* (contentType偽装による無加工配信XSS対策。詳細: docs/media-design.md セキュリティ表#4)。
|
|
6
|
+
*
|
|
7
|
+
* 既知の画像形式のみ照合する。既知形式以外(任意の非画像ファイル)は
|
|
8
|
+
* 「配信時にContent-Disposition: attachmentを付与し、ブラウザに解釈させない」という
|
|
9
|
+
* 別の対策で守るため、ここでは照合不要(常に一致扱い)とする。
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 申告されたcontentTypeと実バイト列が一致するか検証する。
|
|
13
|
+
*
|
|
14
|
+
* 既知の画像形式についてのみ厳密に照合し、未知のcontentTypeは
|
|
15
|
+
* (画像として処理されないため)常に一致扱いとする。
|
|
16
|
+
*/
|
|
17
|
+
export declare function verifyMagicBytes(contentType: string, buffer: Buffer): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* sharpで処理してよい「画像」として扱うかどうか。
|
|
20
|
+
*
|
|
21
|
+
* image/svg+xmlは画像だが埋め込みスクリプトのリスクがあるため除外し、
|
|
22
|
+
* 非画像ファイルと同じ経路(無加工コピー + Content-Disposition: attachment)で扱う。
|
|
23
|
+
*/
|
|
24
|
+
export declare function isProcessableImage(contentType: string): boolean;
|
|
25
|
+
//# sourceMappingURL=magicBytes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magicBytes.d.ts","sourceRoot":"","sources":["../../../src/server/media/magicBytes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAmCH;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAO7E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE/D"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* マジックナンバー照合
|
|
3
|
+
*
|
|
4
|
+
* アップロード時に申告されたcontentTypeが、実際のバイト列と一致するかを検証する
|
|
5
|
+
* (contentType偽装による無加工配信XSS対策。詳細: docs/media-design.md セキュリティ表#4)。
|
|
6
|
+
*
|
|
7
|
+
* 既知の画像形式のみ照合する。既知形式以外(任意の非画像ファイル)は
|
|
8
|
+
* 「配信時にContent-Disposition: attachmentを付与し、ブラウザに解釈させない」という
|
|
9
|
+
* 別の対策で守るため、ここでは照合不要(常に一致扱い)とする。
|
|
10
|
+
*/
|
|
11
|
+
const SIGNATURES = {
|
|
12
|
+
'image/jpeg': [[{ bytes: [0xff, 0xd8, 0xff] }]],
|
|
13
|
+
'image/png': [[{ bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] }]],
|
|
14
|
+
'image/gif': [
|
|
15
|
+
[{ bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61] }], // GIF87a
|
|
16
|
+
[{ bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61] }], // GIF89a
|
|
17
|
+
],
|
|
18
|
+
'image/webp': [
|
|
19
|
+
[
|
|
20
|
+
{ bytes: [0x52, 0x49, 0x46, 0x46] }, // "RIFF"
|
|
21
|
+
{ bytes: [0x57, 0x45, 0x42, 0x50], offset: 8 }, // "WEBP"
|
|
22
|
+
],
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
function matchesCondition(sig, buffer) {
|
|
26
|
+
const offset = sig.offset ?? 0;
|
|
27
|
+
if (buffer.length < offset + sig.bytes.length)
|
|
28
|
+
return false;
|
|
29
|
+
return sig.bytes.every((byte, i) => buffer[offset + i] === byte);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 申告されたcontentTypeと実バイト列が一致するか検証する。
|
|
33
|
+
*
|
|
34
|
+
* 既知の画像形式についてのみ厳密に照合し、未知のcontentTypeは
|
|
35
|
+
* (画像として処理されないため)常に一致扱いとする。
|
|
36
|
+
*/
|
|
37
|
+
export function verifyMagicBytes(contentType, buffer) {
|
|
38
|
+
const candidates = SIGNATURES[contentType];
|
|
39
|
+
if (!candidates) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return candidates.some((conditions) => conditions.every((sig) => matchesCondition(sig, buffer)));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* sharpで処理してよい「画像」として扱うかどうか。
|
|
46
|
+
*
|
|
47
|
+
* image/svg+xmlは画像だが埋め込みスクリプトのリスクがあるため除外し、
|
|
48
|
+
* 非画像ファイルと同じ経路(無加工コピー + Content-Disposition: attachment)で扱う。
|
|
49
|
+
*/
|
|
50
|
+
export function isProcessableImage(contentType) {
|
|
51
|
+
return contentType.startsWith('image/') && contentType !== 'image/svg+xml';
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=magicBytes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magicBytes.js","sourceRoot":"","sources":["../../../src/server/media/magicBytes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAcH,MAAM,UAAU,GAAyC;IACvD,YAAY,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/C,WAAW,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5E,WAAW,EAAE;QACX,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,SAAS;QAC5D,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,SAAS;KAC7D;IACD,YAAY,EAAE;QACZ;YACE,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS;YAC9C,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,SAAS;SAC1D;KACF;CACF,CAAC;AAEF,SAAS,gBAAgB,CAAC,GAAmB,EAAE,MAAc;IAC3D,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;IAC/B,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC5D,OAAO,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AACnE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,MAAc;IAClE,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IAC3C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AACnG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAmB;IACpD,OAAO,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,WAAW,KAAK,eAAe,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudFrontの `/resize/*` ビヘイビア専用オリジン(Lambda Function URL)
|
|
3
|
+
*
|
|
4
|
+
* Function URLは `authorization_type: AWS_IAM` で保護し、CloudFrontからのみ
|
|
5
|
+
* 呼び出せるようにする(詳細: docs/media-design.md、CloudFrontを経由しない
|
|
6
|
+
* 直接呼び出しによる署名検証の迂回を防ぐため)。
|
|
7
|
+
*
|
|
8
|
+
* widthの健全性チェックは「異常値を弾く粗いガード」でよい設計
|
|
9
|
+
* (署名付きURL必須のため、本来の認可はCloudFront側の署名検証で担保される)。
|
|
10
|
+
*/
|
|
11
|
+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
|
|
12
|
+
export declare function handler(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyStructuredResultV2>;
|
|
13
|
+
//# sourceMappingURL=media-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-handler.d.ts","sourceRoot":"","sources":["../../../src/server/media/media-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,iCAAiC,EAAE,MAAM,YAAY,CAAC;AAsG5F,wBAAsB,OAAO,CAC3B,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,iCAAiC,CAAC,CAqG5C"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { GetObjectCommand, NoSuchKey, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
3
|
+
import { SSMClient } from '@aws-sdk/client-ssm';
|
|
4
|
+
import { createLogger } from '../../shared/index.js';
|
|
5
|
+
import { getImagePolicy } from './ssmParams.js';
|
|
6
|
+
const logger = createLogger({ service: 'media-handler' });
|
|
7
|
+
const s3Client = new S3Client({ region: process.env.AWS_REGION });
|
|
8
|
+
const ssmClient = new SSMClient({ region: process.env.AWS_REGION });
|
|
9
|
+
const MAX_INPUT_PIXELS = 100_000_000;
|
|
10
|
+
const CACHE_CONTROL_IMMUTABLE = 'public, max-age=31536000, immutable';
|
|
11
|
+
const CACHE_CONTROL_NO_STORE = 'no-store';
|
|
12
|
+
/**
|
|
13
|
+
* Lambda Function URLのバッファ型応答上限(約6MB、base64化を考慮すると実質
|
|
14
|
+
* 約4.4MB)に対する安全マージン。これを超えると502になり、しかも一度
|
|
15
|
+
* cache/{fileId}/{width}に書き込まれると自動削除(S3 Lifecycle)されるまで
|
|
16
|
+
* 同じリクエストが502であり続けるため、キャッシュに書き込む前に検査する
|
|
17
|
+
* (詳細: docs/media-design.md「リサイズのサイズ設計」)。
|
|
18
|
+
*/
|
|
19
|
+
const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
|
|
20
|
+
const QUALITY_FALLBACK_STEPS = [80, 60, 40];
|
|
21
|
+
function payloadTooLargeResponse(message) {
|
|
22
|
+
return {
|
|
23
|
+
statusCode: 413,
|
|
24
|
+
headers: { 'Cache-Control': CACHE_CONTROL_NO_STORE, 'Content-Type': 'text/plain' },
|
|
25
|
+
body: message,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function notFoundResponse(message) {
|
|
29
|
+
return {
|
|
30
|
+
statusCode: 404,
|
|
31
|
+
headers: { 'Cache-Control': CACHE_CONTROL_NO_STORE, 'Content-Type': 'text/plain' },
|
|
32
|
+
body: message,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function badRequestResponse(message) {
|
|
36
|
+
return {
|
|
37
|
+
statusCode: 400,
|
|
38
|
+
headers: { 'Cache-Control': CACHE_CONTROL_NO_STORE, 'Content-Type': 'text/plain' },
|
|
39
|
+
body: message,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function imageResponse(body, contentType) {
|
|
43
|
+
return {
|
|
44
|
+
statusCode: 200,
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': contentType ?? 'application/octet-stream',
|
|
47
|
+
'Cache-Control': CACHE_CONTROL_IMMUTABLE,
|
|
48
|
+
},
|
|
49
|
+
body: body.toString('base64'),
|
|
50
|
+
isBase64Encoded: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** パス(例: /resize/{fileId})からfileIdを取り出す */
|
|
54
|
+
function extractFileId(rawPath) {
|
|
55
|
+
return rawPath.replace(/^\/resize\//, '').replace(/^\//, '');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* s3:ListBucketが無いロールでGetObjectが存在しないキーを指すと、S3はNoSuchKey
|
|
59
|
+
* ではなく403 AccessDeniedを返す(存在確認防止のためのAWS仕様)。IAM側で
|
|
60
|
+
* ListBucketを許可済みだが、念のためAccessDeniedも「存在しない」として扱う
|
|
61
|
+
* (IAM設定の将来的な regression に対する防御)。
|
|
62
|
+
*/
|
|
63
|
+
function isNotFoundError(error) {
|
|
64
|
+
if (error instanceof NoSuchKey)
|
|
65
|
+
return true;
|
|
66
|
+
if (error instanceof Error) {
|
|
67
|
+
const name = error.name;
|
|
68
|
+
const statusCode = error.$metadata
|
|
69
|
+
?.httpStatusCode;
|
|
70
|
+
return name === 'AccessDenied' || statusCode === 403;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
async function tryGetFromS3(bucket, key) {
|
|
75
|
+
try {
|
|
76
|
+
const result = await s3Client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
77
|
+
const bytes = await result.Body.transformToByteArray();
|
|
78
|
+
return Buffer.from(bytes);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (isNotFoundError(error))
|
|
82
|
+
return undefined;
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function handler(event) {
|
|
87
|
+
const requestId = event.requestContext.requestId;
|
|
88
|
+
const bucket = process.env.MEDIA_BUCKET;
|
|
89
|
+
const imagePolicyParam = process.env.IMAGE_POLICY_PARAM;
|
|
90
|
+
if (!bucket || !imagePolicyParam) {
|
|
91
|
+
logger.error('MEDIA_BUCKET or IMAGE_POLICY_PARAM not configured', { requestId });
|
|
92
|
+
return { statusCode: 500, body: 'Server misconfiguration' };
|
|
93
|
+
}
|
|
94
|
+
const fileId = extractFileId(event.rawPath);
|
|
95
|
+
if (!fileId) {
|
|
96
|
+
return badRequestResponse('fileId is required');
|
|
97
|
+
}
|
|
98
|
+
const widthParam = event.queryStringParameters?.width;
|
|
99
|
+
const width = widthParam ? Number(widthParam) : NaN;
|
|
100
|
+
if (!Number.isInteger(width) || width < 1) {
|
|
101
|
+
return badRequestResponse(`Invalid width: ${widthParam}`);
|
|
102
|
+
}
|
|
103
|
+
let imagePolicy;
|
|
104
|
+
try {
|
|
105
|
+
imagePolicy = await getImagePolicy(ssmClient, imagePolicyParam);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.error('Failed to load image policy (fail-closed)', {
|
|
109
|
+
requestId,
|
|
110
|
+
error: error instanceof Error ? error.message : String(error),
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
statusCode: 500,
|
|
114
|
+
headers: { 'Cache-Control': CACHE_CONTROL_NO_STORE },
|
|
115
|
+
body: 'Image policy unavailable',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (width > imagePolicy.masterMaxDimension) {
|
|
119
|
+
return badRequestResponse(`width exceeds masterMaxDimension: ${width}`);
|
|
120
|
+
}
|
|
121
|
+
const cacheKey = `cache/${fileId}/${width}`;
|
|
122
|
+
const cached = await tryGetFromS3(bucket, cacheKey);
|
|
123
|
+
if (cached) {
|
|
124
|
+
logger.debug('Cache hit', { requestId, fileId, width });
|
|
125
|
+
return imageResponse(cached, 'image/jpeg');
|
|
126
|
+
}
|
|
127
|
+
const master = await tryGetFromS3(bucket, `master/${fileId}`);
|
|
128
|
+
if (!master) {
|
|
129
|
+
return notFoundResponse('Not found');
|
|
130
|
+
}
|
|
131
|
+
// リサイズは process-handler がラスター画像として正規化した master(常に
|
|
132
|
+
// image/jpeg)にのみ適用できる。非画像ファイル・SVGのmasterはオリジナル
|
|
133
|
+
// バイト列のまま保存されており、sharpで処理すると例外(PDF等)や意図しない
|
|
134
|
+
// ラスタライズ(SVG)を起こすため、ここで明示的に拒否する。
|
|
135
|
+
//
|
|
136
|
+
// JPEG品質フォールバック: 応答サイズがLambda Function URLの上限を超える
|
|
137
|
+
// 場合、品質を段階的に下げて再エンコードする。最低品質でも閾値を超える
|
|
138
|
+
// 場合はキャッシュに書き込まず413を返す(詳細: docs/media-design.md)。
|
|
139
|
+
let resized;
|
|
140
|
+
try {
|
|
141
|
+
for (const quality of QUALITY_FALLBACK_STEPS) {
|
|
142
|
+
const buffer = await sharp(master, { limitInputPixels: MAX_INPUT_PIXELS })
|
|
143
|
+
.resize(width, width, { fit: 'inside', withoutEnlargement: true })
|
|
144
|
+
.jpeg({ quality })
|
|
145
|
+
.toBuffer();
|
|
146
|
+
if (buffer.length <= MAX_RESPONSE_BYTES) {
|
|
147
|
+
resized = buffer;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
logger.warn('Resize not applicable (master is not a processable raster image)', {
|
|
154
|
+
requestId,
|
|
155
|
+
fileId,
|
|
156
|
+
error: error instanceof Error ? error.message : String(error),
|
|
157
|
+
});
|
|
158
|
+
return badRequestResponse('Resize is not applicable to this file');
|
|
159
|
+
}
|
|
160
|
+
if (!resized) {
|
|
161
|
+
logger.warn('Resized output exceeds response size limit even at minimum quality', {
|
|
162
|
+
requestId,
|
|
163
|
+
fileId,
|
|
164
|
+
width,
|
|
165
|
+
});
|
|
166
|
+
return payloadTooLargeResponse('Resized output is too large to return');
|
|
167
|
+
}
|
|
168
|
+
await s3Client.send(new PutObjectCommand({
|
|
169
|
+
Bucket: bucket,
|
|
170
|
+
Key: cacheKey,
|
|
171
|
+
Body: resized,
|
|
172
|
+
ContentType: 'image/jpeg',
|
|
173
|
+
}));
|
|
174
|
+
logger.debug('Cache miss, generated resize', { requestId, fileId, width });
|
|
175
|
+
return imageResponse(resized, 'image/jpeg');
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=media-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-handler.js","sourceRoot":"","sources":["../../../src/server/media/media-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3オブジェクトメタデータの共通処理(process-handler・process-handler-dlqで共有)
|
|
3
|
+
*
|
|
4
|
+
* メタデータの契約の詳細: docs/media-design.md「メタデータの契約」参照
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* S3オブジェクトメタデータ(x-amz-meta-プレフィックスを除いた小文字キー)から、
|
|
8
|
+
* アプリ固有の追加フィールドのみを抽出する。
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractAppMetadata(metadata: Record<string, string>): Record<string, string>;
|
|
11
|
+
/**
|
|
12
|
+
* S3メタデータの値はUS-ASCIIのみ有効なため、呼び出し側は非ASCIIの
|
|
13
|
+
* originalFilename(日本語ファイル名等)をpercent-encodingして渡す契約になっている
|
|
14
|
+
* (詳細: docs/media-design.md「メタデータの契約」)。ここでデコードし、
|
|
15
|
+
* MediaRecordには人間が読める実際のファイル名を保存する。
|
|
16
|
+
*
|
|
17
|
+
* 不正なpercent-encodingの場合は、デコードを諦めて元の文字列をそのまま返す
|
|
18
|
+
* (安全側: 例外で処理全体を失敗させない)。
|
|
19
|
+
*/
|
|
20
|
+
export declare function decodeOriginalFilename(raw: string | undefined): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* 非画像ファイルのContent-Dispositionヘッダー値を構築する。
|
|
23
|
+
*
|
|
24
|
+
* 日本語等の非ASCIIファイル名は、RFC 6266の`filename*=UTF-8''...`拡張パラメータ
|
|
25
|
+
* (RFC 5987のpercent-encoding)で埋め込む。あわせて、拡張パラメータに対応しない
|
|
26
|
+
* 古いクライアント向けに、ASCII専用の`filename=`フォールバックも含める。
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildContentDisposition(decodedFilename: string | undefined): string;
|
|
29
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../../src/server/media/metadata.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ3F;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOlF;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUnF"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3オブジェクトメタデータの共通処理(process-handler・process-handler-dlqで共有)
|
|
3
|
+
*
|
|
4
|
+
* メタデータの契約の詳細: docs/media-design.md「メタデータの契約」参照
|
|
5
|
+
*/
|
|
6
|
+
/** メディアレコードに持ち込まないS3メタデータキー(original-filenameは専用フィールドとして扱う) */
|
|
7
|
+
const RESERVED_METADATA_KEYS = new Set(['original-filename']);
|
|
8
|
+
/**
|
|
9
|
+
* S3オブジェクトメタデータ(x-amz-meta-プレフィックスを除いた小文字キー)から、
|
|
10
|
+
* アプリ固有の追加フィールドのみを抽出する。
|
|
11
|
+
*/
|
|
12
|
+
export function extractAppMetadata(metadata) {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
15
|
+
if (!RESERVED_METADATA_KEYS.has(key)) {
|
|
16
|
+
result[key] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* S3メタデータの値はUS-ASCIIのみ有効なため、呼び出し側は非ASCIIの
|
|
23
|
+
* originalFilename(日本語ファイル名等)をpercent-encodingして渡す契約になっている
|
|
24
|
+
* (詳細: docs/media-design.md「メタデータの契約」)。ここでデコードし、
|
|
25
|
+
* MediaRecordには人間が読める実際のファイル名を保存する。
|
|
26
|
+
*
|
|
27
|
+
* 不正なpercent-encodingの場合は、デコードを諦めて元の文字列をそのまま返す
|
|
28
|
+
* (安全側: 例外で処理全体を失敗させない)。
|
|
29
|
+
*/
|
|
30
|
+
export function decodeOriginalFilename(raw) {
|
|
31
|
+
if (!raw)
|
|
32
|
+
return undefined;
|
|
33
|
+
try {
|
|
34
|
+
return decodeURIComponent(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 非画像ファイルのContent-Dispositionヘッダー値を構築する。
|
|
42
|
+
*
|
|
43
|
+
* 日本語等の非ASCIIファイル名は、RFC 6266の`filename*=UTF-8''...`拡張パラメータ
|
|
44
|
+
* (RFC 5987のpercent-encoding)で埋め込む。あわせて、拡張パラメータに対応しない
|
|
45
|
+
* 古いクライアント向けに、ASCII専用の`filename=`フォールバックも含める。
|
|
46
|
+
*/
|
|
47
|
+
export function buildContentDisposition(decodedFilename) {
|
|
48
|
+
if (!decodedFilename)
|
|
49
|
+
return 'attachment';
|
|
50
|
+
// 改行・ダブルクォートを除去(ヘッダーインジェクション対策)
|
|
51
|
+
const sanitized = decodedFilename.replace(/["\r\n]/g, '');
|
|
52
|
+
// ASCII専用フォールバック: 非ASCII文字を取り除いた簡易版(空ならdownloadにする)
|
|
53
|
+
const asciiFallback = sanitized.replace(/[^\x20-\x7e]/g, '').trim() || 'download';
|
|
54
|
+
const encoded = encodeURIComponent(sanitized);
|
|
55
|
+
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encoded}`;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../../src/server/media/metadata.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+DAA+D;AAC/D,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC;AAE9D;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgC;IACjE,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACrC,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAuB;IAC5D,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,eAAmC;IACzE,IAAI,CAAC,eAAe;QAAE,OAAO,YAAY,CAAC;IAE1C,gCAAgC;IAChC,MAAM,SAAS,GAAG,eAAe,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC1D,oDAAoD;IACpD,MAAM,aAAa,GAAG,SAAS,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,UAAU,CAAC;IAClF,MAAM,OAAO,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAE9C,OAAO,yBAAyB,aAAa,uBAAuB,OAAO,EAAE,CAAC;AAChF,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PresignedUploadParams, PresignedUploadResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* presign付きアップロードURLを発行する
|
|
4
|
+
*
|
|
5
|
+
* @throws contentTypeがallowedContentTypesに含まれない場合
|
|
6
|
+
* @throws fileSizeがmaxUploadSizeを超える場合
|
|
7
|
+
*/
|
|
8
|
+
export declare function generatePresignedUpload(params: PresignedUploadParams): Promise<PresignedUploadResult>;
|
|
9
|
+
//# sourceMappingURL=presign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presign.d.ts","sourceRoot":"","sources":["../../../src/server/media/presign.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AA+C/E;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CAwDhC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* presign発行(S3 Presigned POST)
|
|
3
|
+
*
|
|
4
|
+
* DynamoDBには一切触れない(詳細: docs/media-design.md 設計方針2)。
|
|
5
|
+
* アップロードされたオブジェクトのメタデータは、クライアントが自由に指定できる値では
|
|
6
|
+
* なく、ここでpolicyのeq条件として固定した値のみを許可する(サーバー側が値を強制する)。
|
|
7
|
+
*/
|
|
8
|
+
import { ulid } from 'ulid';
|
|
9
|
+
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
10
|
+
/**
|
|
11
|
+
* presignは「呼び出し側がアップロードを開始する直前」に発行する設計を前提とする
|
|
12
|
+
* (事前に先回りして取得・保持しておくものではない)。そのためTTLがカバえるべきは
|
|
13
|
+
* 「presignを受け取ってからS3へのPOSTを開始するまでの間隔」(ネットワーク往復・
|
|
14
|
+
* 悪条件下でのTCP/TLSハンドシェイク等)であり、ファイル転送そのものの所要時間では
|
|
15
|
+
* ない(S3は presigned POST のpolicy expirationをリクエスト開始時点で判定し、
|
|
16
|
+
* 転送開始後に期限が切れても転送自体は継続する。AWS公式ドキュメントで
|
|
17
|
+
* presigned URL(GET/PUT)について明記されている挙動 -
|
|
18
|
+
* https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
|
|
19
|
+
* 。POSTについても同様の挙動と推定されるが、本番投入前に実機で確認すること)。
|
|
20
|
+
* 悪条件のモバイル回線(DNS・TCP/TLSハンドシェイクのSYN再送込み)を想定しても
|
|
21
|
+
* 数秒〜10秒程度に収まるため、30秒は十分な余裕を持った値とする。
|
|
22
|
+
*
|
|
23
|
+
* 失敗時(ネットワークエラー・期限切れ等いずれの場合も)は、同じpresignで
|
|
24
|
+
* 再試行せず、必ず新しいpresignを取り直すこと。これによりTTLの長さを
|
|
25
|
+
* リトライ回数・間隔から独立させ、設計をシンプルに保てる。
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_EXPIRES_IN_SECONDS = 30;
|
|
28
|
+
/**
|
|
29
|
+
* S3はユーザー定義メタデータのキーを常に小文字で保存する(AWS仕様)。
|
|
30
|
+
* ここで大文字混じりのキーを許可すると、アップロード時に指定したキー名と
|
|
31
|
+
* process-handlerがHeadObjectで読み戻すキー名が一致しなくなり、呼び出し側の
|
|
32
|
+
* 権限スコープ判定(例: ownerIdでのフィルタ)がサイレントに失敗する。
|
|
33
|
+
* 呼び出し側の実装ミスを早期に検出するため、小文字以外のキーは拒否する。
|
|
34
|
+
*/
|
|
35
|
+
const METADATA_KEY_PATTERN = /^[a-z0-9-]+$/;
|
|
36
|
+
function validateMetadataKeys(metadata) {
|
|
37
|
+
for (const key of Object.keys(metadata)) {
|
|
38
|
+
if (!METADATA_KEY_PATTERN.test(key)) {
|
|
39
|
+
throw new Error(`Invalid metadata key "${key}": S3 lowercases metadata keys, so keys must already be ` +
|
|
40
|
+
`lowercase (and use only [a-z0-9-]) to avoid a silent mismatch after upload`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* presign付きアップロードURLを発行する
|
|
46
|
+
*
|
|
47
|
+
* @throws contentTypeがallowedContentTypesに含まれない場合
|
|
48
|
+
* @throws fileSizeがmaxUploadSizeを超える場合
|
|
49
|
+
*/
|
|
50
|
+
export async function generatePresignedUpload(params) {
|
|
51
|
+
const { s3Client, bucket, contentType, originalFilename, fileSize, allowedContentTypes, maxUploadSize, metadata, expiresInSeconds, } = params;
|
|
52
|
+
if (!allowedContentTypes.includes(contentType)) {
|
|
53
|
+
throw new Error(`Content type not allowed: ${contentType}`);
|
|
54
|
+
}
|
|
55
|
+
if (fileSize <= 0 || fileSize > maxUploadSize) {
|
|
56
|
+
throw new Error(`File size out of range: ${fileSize} (max: ${maxUploadSize})`);
|
|
57
|
+
}
|
|
58
|
+
validateMetadataKeys(metadata);
|
|
59
|
+
const fileId = ulid();
|
|
60
|
+
const key = `raw/${fileId}`;
|
|
61
|
+
const conditions = [
|
|
62
|
+
['content-length-range', 1, maxUploadSize],
|
|
63
|
+
{ 'Content-Type': contentType },
|
|
64
|
+
];
|
|
65
|
+
const fields = {
|
|
66
|
+
'Content-Type': contentType,
|
|
67
|
+
};
|
|
68
|
+
for (const [metaKey, metaValue] of Object.entries(metadata)) {
|
|
69
|
+
const fieldName = `x-amz-meta-${metaKey}`;
|
|
70
|
+
fields[fieldName] = metaValue;
|
|
71
|
+
conditions.push({ [fieldName]: metaValue });
|
|
72
|
+
}
|
|
73
|
+
if (originalFilename) {
|
|
74
|
+
fields['x-amz-meta-original-filename'] = originalFilename;
|
|
75
|
+
conditions.push({ 'x-amz-meta-original-filename': originalFilename });
|
|
76
|
+
}
|
|
77
|
+
const { url, fields: presignedFields } = await createPresignedPost(s3Client, {
|
|
78
|
+
Bucket: bucket,
|
|
79
|
+
Key: key,
|
|
80
|
+
Conditions: conditions,
|
|
81
|
+
Fields: fields,
|
|
82
|
+
Expires: expiresInSeconds ?? DEFAULT_EXPIRES_IN_SECONDS,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
fileId,
|
|
86
|
+
uploadUrl: url,
|
|
87
|
+
fields: presignedFields,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=presign.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presign.js","sourceRoot":"","sources":["../../../src/server/media/presign.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAIjE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,0BAA0B,GAAG,EAAE,CAAC;AAQtC;;;;;;GAMG;AACH,MAAM,oBAAoB,GAAG,cAAc,CAAC;AAE5C,SAAS,oBAAoB,CAAC,QAAgC;IAC5D,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,yBAAyB,GAAG,0DAA0D;gBACpF,4EAA4E,CAC/E,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,MAA6B;IAE7B,MAAM,EACJ,QAAQ,EACR,MAAM,EACN,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,gBAAgB,GACjB,GAAG,MAAM,CAAC;IAEX,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,6BAA6B,WAAW,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,IAAI,QAAQ,IAAI,CAAC,IAAI,QAAQ,GAAG,aAAa,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,UAAU,aAAa,GAAG,CAAC,CAAC;IACjF,CAAC;IACD,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAE/B,MAAM,MAAM,GAAG,IAAI,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,OAAO,MAAM,EAAE,CAAC;IAE5B,MAAM,UAAU,GAAuB;QACrC,CAAC,sBAAsB,EAAE,CAAC,EAAE,aAAa,CAAC;QAC1C,EAAE,cAAc,EAAE,WAAW,EAAE;KAChC,CAAC;IACF,MAAM,MAAM,GAA2B;QACrC,cAAc,EAAE,WAAW;KAC5B,CAAC;IAEF,KAAK,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,MAAM,SAAS,GAAG,cAAc,OAAO,EAAE,CAAC;QAC1C,MAAM,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;QAC9B,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,gBAAgB,EAAE,CAAC;QACrB,MAAM,CAAC,8BAA8B,CAAC,GAAG,gBAAgB,CAAC;QAC1D,UAAU,CAAC,IAAI,CAAC,EAAE,8BAA8B,EAAE,gBAAgB,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,mBAAmB,CAAC,QAAQ,EAAE;QAC3E,MAAM,EAAE,MAAM;QACd,GAAG,EAAE,GAAG;QACR,UAAU,EAAE,UAAU;QACtB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,gBAAgB,IAAI,0BAA0B;KACxD,CAAC,CAAC;IAEH,OAAO;QACL,MAAM;QACN,SAAS,EAAE,GAAG;QACd,MAAM,EAAE,eAAe;KACxB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-handler-dlq.d.ts","sourceRoot":"","sources":["../../../src/server/media/process-handler-dlq.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAY,UAAU,EAAE,MAAM,YAAY,CAAC;AA2GvD,eAAO,MAAM,OAAO,EAAE,UAIrB,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process-handler の失敗(Lambda非同期呼び出しのリトライが尽きた場合)を
|
|
3
|
+
* 受け取るDLQ(SQS)ハンドラー
|
|
4
|
+
*
|
|
5
|
+
* process-handler本体が例外を投げ続けてLambdaのリトライが尽きると、
|
|
6
|
+
* on-failure宛先として設定したこのSQSキューにメッセージが送られる。
|
|
7
|
+
* ここでは呼び出し側に「処理が永久に終わらない」状態(クライアントが
|
|
8
|
+
* 無限ポーリングし続ける)を解消するため、status:'failed'レコードを書き込む。
|
|
9
|
+
*
|
|
10
|
+
* 上書き防止: S3イベントの重複配信により、process-handler本体が先に
|
|
11
|
+
* completedを書き込んだ後にDLQ経由の古いリトライがfailedで上書きする
|
|
12
|
+
* 可能性があるため、書き込み前に既存レコードを確認する。
|
|
13
|
+
*/
|
|
14
|
+
import { HeadObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
15
|
+
import { createLogger } from '../../shared/index.js';
|
|
16
|
+
import { extractAppMetadata, decodeOriginalFilename } from './metadata.js';
|
|
17
|
+
import { handleFindMany } from '../operations/findMany.js';
|
|
18
|
+
import { handleUpdateOne } from '../operations/updateOne.js';
|
|
19
|
+
const logger = createLogger({ service: 'media-process-handler-dlq' });
|
|
20
|
+
const s3Client = new S3Client({ region: process.env.AWS_REGION });
|
|
21
|
+
function extractS3Ref(envelope) {
|
|
22
|
+
const record = envelope.requestPayload?.Records?.[0];
|
|
23
|
+
const bucket = record?.s3?.bucket?.name;
|
|
24
|
+
const rawKey = record?.s3?.object?.key;
|
|
25
|
+
if (!bucket || !rawKey)
|
|
26
|
+
return undefined;
|
|
27
|
+
const key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
|
|
28
|
+
return { bucket, key, fileId: key.replace(/^raw\//, '') };
|
|
29
|
+
}
|
|
30
|
+
async function processMessage(body) {
|
|
31
|
+
let envelope;
|
|
32
|
+
try {
|
|
33
|
+
envelope = JSON.parse(body);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
logger.error('DLQ message is not valid JSON', { body });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const ref = extractS3Ref(envelope);
|
|
40
|
+
if (!ref) {
|
|
41
|
+
logger.error('Could not extract S3 reference from DLQ message', { envelope });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { bucket, key, fileId } = ref;
|
|
45
|
+
const requestId = `dlq-${fileId}`;
|
|
46
|
+
const existing = await handleFindMany('media', { ids: [fileId] }, requestId);
|
|
47
|
+
const existingRecord = existing[0];
|
|
48
|
+
if (existingRecord?.status === 'completed') {
|
|
49
|
+
logger.info('Media already completed, skipping DLQ failure write', { requestId, fileId });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// 呼び出し側のアプリ固有メタデータ(ownerId等)を再取得する。これが無いと、
|
|
53
|
+
// 呼び出し側が権限スコープ付きのポーリングでこのfailedレコードを読めず、
|
|
54
|
+
// クライアントが無限ポーリングし続けることになる(詳細: docs/adr/0002-media-design.md)。
|
|
55
|
+
// raw/{fileId}が既に削除されている等でHeadObjectが失敗しても、処理全体は
|
|
56
|
+
// 失敗させず、メタデータ無しでfailedレコードを書き込む(fail-soft)。
|
|
57
|
+
let appMetadata = {};
|
|
58
|
+
let originalFilename;
|
|
59
|
+
try {
|
|
60
|
+
const head = await s3Client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
61
|
+
const metadata = head.Metadata || {};
|
|
62
|
+
appMetadata = extractAppMetadata(metadata);
|
|
63
|
+
originalFilename = decodeOriginalFilename(metadata['original-filename']);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
logger.warn('Failed to re-fetch metadata for DLQ failure record', {
|
|
67
|
+
requestId,
|
|
68
|
+
fileId,
|
|
69
|
+
error: error instanceof Error ? error.message : String(error),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
await handleUpdateOne('media', {
|
|
74
|
+
id: fileId,
|
|
75
|
+
data: {
|
|
76
|
+
$set: {
|
|
77
|
+
status: 'failed',
|
|
78
|
+
error: 'Processing failed after exhausting retries',
|
|
79
|
+
updatedAt: now,
|
|
80
|
+
...(originalFilename ? { originalFilename } : {}),
|
|
81
|
+
...appMetadata,
|
|
82
|
+
},
|
|
83
|
+
$setOnInsert: { createdAt: now },
|
|
84
|
+
},
|
|
85
|
+
options: { upsert: true },
|
|
86
|
+
}, requestId);
|
|
87
|
+
logger.warn('Media marked as failed via DLQ', { requestId, fileId });
|
|
88
|
+
}
|
|
89
|
+
export const handler = async (event) => {
|
|
90
|
+
for (const record of event.Records) {
|
|
91
|
+
await processMessage(record.body);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=process-handler-dlq.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-handler-dlq.js","sourceRoot":"","sources":["../../../src/server/media/process-handler-dlq.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 ObjectCreated(raw/*)イベントハンドラー
|
|
3
|
+
*
|
|
4
|
+
* アップロードされたファイルを検証・処理し、masterとしてS3に配置した上で
|
|
5
|
+
* 呼び出し側の既存テーブルにMediaRecordを作成/更新する(詳細: docs/media-design.md)。
|
|
6
|
+
*
|
|
7
|
+
* presign発行時点ではDynamoDBに一切触れない設計のため、DynamoDBレコードが
|
|
8
|
+
* 作られるのはこのハンドラーが成功した時点が初めて。
|
|
9
|
+
*/
|
|
10
|
+
import type { S3Handler } from 'aws-lambda';
|
|
11
|
+
export declare const handler: S3Handler;
|
|
12
|
+
//# sourceMappingURL=process-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-handler.d.ts","sourceRoot":"","sources":["../../../src/server/media/process-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAW,SAAS,EAAE,MAAM,YAAY,CAAC;AAiMrD,eAAO,MAAM,OAAO,EAAE,SAgBrB,CAAC"}
|