@exabugs/dynamodb-client 1.5.2 → 1.5.3
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 +50 -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 +143 -0
- package/dist/server/media/media-handler.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 +73 -0
- package/dist/server/media/presign.js.map +1 -0
- package/dist/server/media/process-handler-dlq.d.ts +16 -0
- package/dist/server/media/process-handler-dlq.d.ts.map +1 -0
- package/dist/server/media/process-handler-dlq.js +52 -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 +138 -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-handler.cjs +57450 -0
- package/dist/server/media-handler.cjs.map +7 -0
- package/dist/server/media-process-handler-dlq.cjs +36195 -0
- package/dist/server/media-process-handler-dlq.cjs.map +7 -0
- package/dist/server/media-process-handler.cjs +60568 -0
- package/dist/server/media-process-handler.cjs.map +7 -0
- package/dist/sharp-layer.zip +0 -0
- package/package.json +34 -3
- package/terraform/main.tf +3 -2
- package/terraform/media/README.md +64 -0
- package/terraform/media/main.tf +718 -0
- package/terraform/media/outputs.tf +66 -0
- package/terraform/media/variables.tf +84 -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;AAiBH;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAW7E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAE/D"}
|
|
@@ -0,0 +1,50 @@
|
|
|
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] },
|
|
16
|
+
{ bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61] },
|
|
17
|
+
],
|
|
18
|
+
'image/webp': [
|
|
19
|
+
{ bytes: [0x52, 0x49, 0x46, 0x46] }, // "RIFF"
|
|
20
|
+
{ bytes: [0x57, 0x45, 0x42, 0x50], offset: 8 }, // "WEBP"
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* 申告されたcontentTypeと実バイト列が一致するか検証する。
|
|
25
|
+
*
|
|
26
|
+
* 既知の画像形式についてのみ厳密に照合し、未知のcontentTypeは
|
|
27
|
+
* (画像として処理されないため)常に一致扱いとする。
|
|
28
|
+
*/
|
|
29
|
+
export function verifyMagicBytes(contentType, buffer) {
|
|
30
|
+
const signatures = SIGNATURES[contentType];
|
|
31
|
+
if (!signatures) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return signatures.every((sig) => {
|
|
35
|
+
const offset = sig.offset ?? 0;
|
|
36
|
+
if (buffer.length < offset + sig.bytes.length)
|
|
37
|
+
return false;
|
|
38
|
+
return sig.bytes.every((byte, i) => buffer[offset + i] === byte);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* sharpで処理してよい「画像」として扱うかどうか。
|
|
43
|
+
*
|
|
44
|
+
* image/svg+xmlは画像だが埋め込みスクリプトのリスクがあるため除外し、
|
|
45
|
+
* 非画像ファイルと同じ経路(無加工コピー + Content-Disposition: attachment)で扱う。
|
|
46
|
+
*/
|
|
47
|
+
export function isProcessableImage(contentType) {
|
|
48
|
+
return contentType.startsWith('image/') && contentType !== 'image/svg+xml';
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=magicBytes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magicBytes.js","sourceRoot":"","sources":["../../../src/server/media/magicBytes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,UAAU,GAAqC;IACnD,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;IAC7C,WAAW,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;IAC1E,WAAW,EAAE;QACX,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;QAC/C,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;KAChD;IACD,YAAY,EAAE;QACZ,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS;QAC9C,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,SAAS;KAC1D;CACF,CAAC;AAEF;;;;;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,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;QAC/B,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC5D,OAAO,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,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;AAoF5F,wBAAsB,OAAO,CAC3B,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,iCAAiC,CAAC,CAkF5C"}
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
function notFoundResponse(message) {
|
|
13
|
+
return {
|
|
14
|
+
statusCode: 404,
|
|
15
|
+
headers: { 'Cache-Control': CACHE_CONTROL_NO_STORE, 'Content-Type': 'text/plain' },
|
|
16
|
+
body: message,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function badRequestResponse(message) {
|
|
20
|
+
return {
|
|
21
|
+
statusCode: 400,
|
|
22
|
+
headers: { 'Cache-Control': CACHE_CONTROL_NO_STORE, 'Content-Type': 'text/plain' },
|
|
23
|
+
body: message,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function imageResponse(body, contentType) {
|
|
27
|
+
return {
|
|
28
|
+
statusCode: 200,
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': contentType ?? 'application/octet-stream',
|
|
31
|
+
'Cache-Control': CACHE_CONTROL_IMMUTABLE,
|
|
32
|
+
},
|
|
33
|
+
body: body.toString('base64'),
|
|
34
|
+
isBase64Encoded: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** パス(例: /resize/{fileId})からfileIdを取り出す */
|
|
38
|
+
function extractFileId(rawPath) {
|
|
39
|
+
return rawPath.replace(/^\/resize\//, '').replace(/^\//, '');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* s3:ListBucketが無いロールでGetObjectが存在しないキーを指すと、S3はNoSuchKey
|
|
43
|
+
* ではなく403 AccessDeniedを返す(存在確認防止のためのAWS仕様)。IAM側で
|
|
44
|
+
* ListBucketを許可済みだが、念のためAccessDeniedも「存在しない」として扱う
|
|
45
|
+
* (IAM設定の将来的な regression に対する防御)。
|
|
46
|
+
*/
|
|
47
|
+
function isNotFoundError(error) {
|
|
48
|
+
if (error instanceof NoSuchKey)
|
|
49
|
+
return true;
|
|
50
|
+
if (error instanceof Error) {
|
|
51
|
+
const name = error.name;
|
|
52
|
+
const statusCode = error.$metadata
|
|
53
|
+
?.httpStatusCode;
|
|
54
|
+
return name === 'AccessDenied' || statusCode === 403;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
async function tryGetFromS3(bucket, key) {
|
|
59
|
+
try {
|
|
60
|
+
const result = await s3Client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
61
|
+
const bytes = await result.Body.transformToByteArray();
|
|
62
|
+
return Buffer.from(bytes);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (isNotFoundError(error))
|
|
66
|
+
return undefined;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function handler(event) {
|
|
71
|
+
const requestId = event.requestContext.requestId;
|
|
72
|
+
const bucket = process.env.MEDIA_BUCKET;
|
|
73
|
+
const imagePolicyParam = process.env.IMAGE_POLICY_PARAM;
|
|
74
|
+
if (!bucket || !imagePolicyParam) {
|
|
75
|
+
logger.error('MEDIA_BUCKET or IMAGE_POLICY_PARAM not configured', { requestId });
|
|
76
|
+
return { statusCode: 500, body: 'Server misconfiguration' };
|
|
77
|
+
}
|
|
78
|
+
const fileId = extractFileId(event.rawPath);
|
|
79
|
+
if (!fileId) {
|
|
80
|
+
return badRequestResponse('fileId is required');
|
|
81
|
+
}
|
|
82
|
+
const widthParam = event.queryStringParameters?.width;
|
|
83
|
+
const width = widthParam ? Number(widthParam) : NaN;
|
|
84
|
+
if (!Number.isInteger(width) || width < 1) {
|
|
85
|
+
return badRequestResponse(`Invalid width: ${widthParam}`);
|
|
86
|
+
}
|
|
87
|
+
let imagePolicy;
|
|
88
|
+
try {
|
|
89
|
+
imagePolicy = await getImagePolicy(ssmClient, imagePolicyParam);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
logger.error('Failed to load image policy (fail-closed)', {
|
|
93
|
+
requestId,
|
|
94
|
+
error: error instanceof Error ? error.message : String(error),
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
statusCode: 500,
|
|
98
|
+
headers: { 'Cache-Control': CACHE_CONTROL_NO_STORE },
|
|
99
|
+
body: 'Image policy unavailable',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (width > imagePolicy.masterMaxDimension) {
|
|
103
|
+
return badRequestResponse(`width exceeds masterMaxDimension: ${width}`);
|
|
104
|
+
}
|
|
105
|
+
const cacheKey = `cache/${fileId}/${width}`;
|
|
106
|
+
const cached = await tryGetFromS3(bucket, cacheKey);
|
|
107
|
+
if (cached) {
|
|
108
|
+
logger.debug('Cache hit', { requestId, fileId, width });
|
|
109
|
+
return imageResponse(cached, 'image/jpeg');
|
|
110
|
+
}
|
|
111
|
+
const master = await tryGetFromS3(bucket, `master/${fileId}`);
|
|
112
|
+
if (!master) {
|
|
113
|
+
return notFoundResponse('Not found');
|
|
114
|
+
}
|
|
115
|
+
// リサイズは process-handler がラスター画像として正規化した master(常に
|
|
116
|
+
// image/jpeg)にのみ適用できる。非画像ファイル・SVGのmasterはオリジナル
|
|
117
|
+
// バイト列のまま保存されており、sharpで処理すると例外(PDF等)や意図しない
|
|
118
|
+
// ラスタライズ(SVG)を起こすため、ここで明示的に拒否する。
|
|
119
|
+
let resized;
|
|
120
|
+
try {
|
|
121
|
+
resized = await sharp(master, { limitInputPixels: MAX_INPUT_PIXELS })
|
|
122
|
+
.resize(width, width, { fit: 'inside', withoutEnlargement: true })
|
|
123
|
+
.jpeg()
|
|
124
|
+
.toBuffer();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
logger.warn('Resize not applicable (master is not a processable raster image)', {
|
|
128
|
+
requestId,
|
|
129
|
+
fileId,
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
});
|
|
132
|
+
return badRequestResponse('Resize is not applicable to this file');
|
|
133
|
+
}
|
|
134
|
+
await s3Client.send(new PutObjectCommand({
|
|
135
|
+
Bucket: bucket,
|
|
136
|
+
Key: cacheKey,
|
|
137
|
+
Body: resized,
|
|
138
|
+
ContentType: 'image/jpeg',
|
|
139
|
+
}));
|
|
140
|
+
logger.debug('Cache miss, generated resize', { requestId, fileId, width });
|
|
141
|
+
return imageResponse(resized, 'image/jpeg');
|
|
142
|
+
}
|
|
143
|
+
//# 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,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;AA4B/E;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CAwDhC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
const DEFAULT_EXPIRES_IN_SECONDS = 300;
|
|
11
|
+
/**
|
|
12
|
+
* S3はユーザー定義メタデータのキーを常に小文字で保存する(AWS仕様)。
|
|
13
|
+
* ここで大文字混じりのキーを許可すると、アップロード時に指定したキー名と
|
|
14
|
+
* process-handlerがHeadObjectで読み戻すキー名が一致しなくなり、呼び出し側の
|
|
15
|
+
* 権限スコープ判定(例: ownerIdでのフィルタ)がサイレントに失敗する。
|
|
16
|
+
* 呼び出し側の実装ミスを早期に検出するため、小文字以外のキーは拒否する。
|
|
17
|
+
*/
|
|
18
|
+
const METADATA_KEY_PATTERN = /^[a-z0-9-]+$/;
|
|
19
|
+
function validateMetadataKeys(metadata) {
|
|
20
|
+
for (const key of Object.keys(metadata)) {
|
|
21
|
+
if (!METADATA_KEY_PATTERN.test(key)) {
|
|
22
|
+
throw new Error(`Invalid metadata key "${key}": S3 lowercases metadata keys, so keys must already be ` +
|
|
23
|
+
`lowercase (and use only [a-z0-9-]) to avoid a silent mismatch after upload`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* presign付きアップロードURLを発行する
|
|
29
|
+
*
|
|
30
|
+
* @throws contentTypeがallowedContentTypesに含まれない場合
|
|
31
|
+
* @throws fileSizeがmaxUploadSizeを超える場合
|
|
32
|
+
*/
|
|
33
|
+
export async function generatePresignedUpload(params) {
|
|
34
|
+
const { s3Client, bucket, contentType, originalFilename, fileSize, allowedContentTypes, maxUploadSize, metadata, expiresInSeconds, } = params;
|
|
35
|
+
if (!allowedContentTypes.includes(contentType)) {
|
|
36
|
+
throw new Error(`Content type not allowed: ${contentType}`);
|
|
37
|
+
}
|
|
38
|
+
if (fileSize <= 0 || fileSize > maxUploadSize) {
|
|
39
|
+
throw new Error(`File size out of range: ${fileSize} (max: ${maxUploadSize})`);
|
|
40
|
+
}
|
|
41
|
+
validateMetadataKeys(metadata);
|
|
42
|
+
const fileId = ulid();
|
|
43
|
+
const key = `raw/${fileId}`;
|
|
44
|
+
const conditions = [
|
|
45
|
+
['content-length-range', 1, maxUploadSize],
|
|
46
|
+
{ 'Content-Type': contentType },
|
|
47
|
+
];
|
|
48
|
+
const fields = {
|
|
49
|
+
'Content-Type': contentType,
|
|
50
|
+
};
|
|
51
|
+
for (const [metaKey, metaValue] of Object.entries(metadata)) {
|
|
52
|
+
const fieldName = `x-amz-meta-${metaKey}`;
|
|
53
|
+
fields[fieldName] = metaValue;
|
|
54
|
+
conditions.push({ [fieldName]: metaValue });
|
|
55
|
+
}
|
|
56
|
+
if (originalFilename) {
|
|
57
|
+
fields['x-amz-meta-original-filename'] = originalFilename;
|
|
58
|
+
conditions.push({ 'x-amz-meta-original-filename': originalFilename });
|
|
59
|
+
}
|
|
60
|
+
const { url, fields: presignedFields } = await createPresignedPost(s3Client, {
|
|
61
|
+
Bucket: bucket,
|
|
62
|
+
Key: key,
|
|
63
|
+
Conditions: conditions,
|
|
64
|
+
Fields: fields,
|
|
65
|
+
Expires: expiresInSeconds ?? DEFAULT_EXPIRES_IN_SECONDS,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
fileId,
|
|
69
|
+
uploadUrl: url,
|
|
70
|
+
fields: presignedFields,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# 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,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAMvC;;;;;;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,16 @@
|
|
|
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 type { SQSHandler } from 'aws-lambda';
|
|
15
|
+
export declare const handler: SQSHandler;
|
|
16
|
+
//# sourceMappingURL=process-handler-dlq.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-handler-dlq.d.ts","sourceRoot":"","sources":["../../../src/server/media/process-handler-dlq.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAY,UAAU,EAAE,MAAM,YAAY,CAAC;AAwEvD,eAAO,MAAM,OAAO,EAAE,UAIrB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createLogger } from '../../shared/index.js';
|
|
2
|
+
import { handleFindMany } from '../operations/findMany.js';
|
|
3
|
+
import { handleUpdateOne } from '../operations/updateOne.js';
|
|
4
|
+
const logger = createLogger({ service: 'media-process-handler-dlq' });
|
|
5
|
+
function extractFileId(envelope) {
|
|
6
|
+
const key = envelope.requestPayload?.Records?.[0]?.s3?.object?.key;
|
|
7
|
+
if (!key)
|
|
8
|
+
return undefined;
|
|
9
|
+
return decodeURIComponent(key.replace(/\+/g, ' ')).replace(/^raw\//, '');
|
|
10
|
+
}
|
|
11
|
+
async function processMessage(body) {
|
|
12
|
+
let envelope;
|
|
13
|
+
try {
|
|
14
|
+
envelope = JSON.parse(body);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
logger.error('DLQ message is not valid JSON', { body });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const fileId = extractFileId(envelope);
|
|
21
|
+
if (!fileId) {
|
|
22
|
+
logger.error('Could not extract fileId from DLQ message', { envelope });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const requestId = `dlq-${fileId}`;
|
|
26
|
+
const existing = await handleFindMany('media', { ids: [fileId] }, requestId);
|
|
27
|
+
const existingRecord = existing[0];
|
|
28
|
+
if (existingRecord?.status === 'completed') {
|
|
29
|
+
logger.info('Media already completed, skipping DLQ failure write', { requestId, fileId });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const now = new Date().toISOString();
|
|
33
|
+
await handleUpdateOne('media', {
|
|
34
|
+
id: fileId,
|
|
35
|
+
data: {
|
|
36
|
+
$set: {
|
|
37
|
+
status: 'failed',
|
|
38
|
+
error: 'Processing failed after exhausting retries',
|
|
39
|
+
updatedAt: now,
|
|
40
|
+
},
|
|
41
|
+
$setOnInsert: { createdAt: now },
|
|
42
|
+
},
|
|
43
|
+
options: { upsert: true },
|
|
44
|
+
}, requestId);
|
|
45
|
+
logger.warn('Media marked as failed via DLQ', { requestId, fileId });
|
|
46
|
+
}
|
|
47
|
+
export const handler = async (event) => {
|
|
48
|
+
for (const record of event.Records) {
|
|
49
|
+
await processMessage(record.body);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
//# 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":"AAeA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAG7D,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC,CAAC;AActE,SAAS,aAAa,CAAC,QAAwC;IAC7D,MAAM,GAAG,GAAG,QAAQ,CAAC,cAAc,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC;IACnE,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,OAAO,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,IAAY;IACxC,IAAI,QAAwC,CAAC;IAC7C,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,KAAK,CAAC,+BAA+B,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,OAAO;IACT,CAAC;IAED,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,CAAC,KAAK,CAAC,2CAA2C,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;QACxE,OAAO;IACT,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,MAAM,EAAE,CAAC;IAElC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IAC7E,MAAM,cAAc,GAAG,QAAQ,CAAC,CAAC,CAA4B,CAAC;IAC9D,IAAI,cAAc,EAAE,MAAM,KAAK,WAAW,EAAE,CAAC;QAC3C,MAAM,CAAC,IAAI,CAAC,qDAAqD,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;QAC1F,OAAO;IACT,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,eAAe,CACnB,OAAO,EACP;QACE,EAAE,EAAE,MAAM;QACV,IAAI,EAAE;YACJ,IAAI,EAAE;gBACJ,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE,4CAA4C;gBACnD,SAAS,EAAE,GAAG;aACf;YACD,YAAY,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE;SACjC;QACD,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;KAC1B,EACD,SAAS,CACV,CAAC;IAEF,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,CAAC,MAAM,OAAO,GAAe,KAAK,EAAE,KAAe,EAAiB,EAAE;IAC1E,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,MAAM,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;AACH,CAAC,CAAC"}
|
|
@@ -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;AA8KrD,eAAO,MAAM,OAAO,EAAE,SAgBrB,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { CopyObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
3
|
+
import { SSMClient } from '@aws-sdk/client-ssm';
|
|
4
|
+
import { createLogger } from '../../shared/index.js';
|
|
5
|
+
import { handleUpdateOne } from '../operations/updateOne.js';
|
|
6
|
+
import { isProcessableImage, verifyMagicBytes } from './magicBytes.js';
|
|
7
|
+
import { getImagePolicy } from './ssmParams.js';
|
|
8
|
+
const logger = createLogger({ service: 'media-process-handler' });
|
|
9
|
+
const s3Client = new S3Client({ region: process.env.AWS_REGION });
|
|
10
|
+
const ssmClient = new SSMClient({ region: process.env.AWS_REGION });
|
|
11
|
+
/** sharp のDoS対策(極端に大きい画素数の画像を拒否) */
|
|
12
|
+
const MAX_INPUT_PIXELS = 100_000_000;
|
|
13
|
+
/** メディアレコードに持ち込まないS3メタデータキー(original-filenameは専用フィールドとして扱う) */
|
|
14
|
+
const RESERVED_METADATA_KEYS = new Set(['original-filename']);
|
|
15
|
+
/**
|
|
16
|
+
* S3オブジェクトメタデータ(x-amz-meta-プレフィックスを除いた小文字キー)から、
|
|
17
|
+
* アプリ固有の追加フィールドのみを抽出する。
|
|
18
|
+
*/
|
|
19
|
+
function extractAppMetadata(metadata) {
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
22
|
+
if (!RESERVED_METADATA_KEYS.has(key)) {
|
|
23
|
+
result[key] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
/** Content-Dispositionのfilenameパラメータに安全に埋め込めるよう制御文字・ダブルクォートを除去する */
|
|
29
|
+
function sanitizeFilenameForHeader(filename) {
|
|
30
|
+
return filename.replace(/["\r\n]/g, '');
|
|
31
|
+
}
|
|
32
|
+
async function streamToBuffer(body) {
|
|
33
|
+
const bytes = await body.transformToByteArray();
|
|
34
|
+
return Buffer.from(bytes);
|
|
35
|
+
}
|
|
36
|
+
async function writeMediaRecord(fileId, fields, requestId) {
|
|
37
|
+
const now = new Date().toISOString();
|
|
38
|
+
await handleUpdateOne('media', {
|
|
39
|
+
id: fileId,
|
|
40
|
+
data: {
|
|
41
|
+
$set: { ...fields, updatedAt: now },
|
|
42
|
+
$setOnInsert: { createdAt: now },
|
|
43
|
+
},
|
|
44
|
+
options: { upsert: true },
|
|
45
|
+
}, requestId);
|
|
46
|
+
}
|
|
47
|
+
async function processRecord(bucket, rawKey) {
|
|
48
|
+
const key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
|
|
49
|
+
const fileId = key.replace(/^raw\//, '');
|
|
50
|
+
const requestId = `s3-event-${fileId}`;
|
|
51
|
+
const head = await s3Client.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
52
|
+
const contentType = head.ContentType || 'application/octet-stream';
|
|
53
|
+
const metadata = head.Metadata || {};
|
|
54
|
+
const originalFilename = metadata['original-filename'];
|
|
55
|
+
const getResult = await s3Client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
56
|
+
const buffer = await streamToBuffer(getResult.Body);
|
|
57
|
+
if (!verifyMagicBytes(contentType, buffer)) {
|
|
58
|
+
logger.warn('Content type does not match file signature', { requestId, fileId, contentType });
|
|
59
|
+
await writeMediaRecord(fileId, {
|
|
60
|
+
status: 'failed',
|
|
61
|
+
contentType,
|
|
62
|
+
error: 'Content type does not match file signature',
|
|
63
|
+
}, requestId);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const masterKey = `master/${fileId}`;
|
|
67
|
+
const appMetadata = extractAppMetadata(metadata);
|
|
68
|
+
if (isProcessableImage(contentType)) {
|
|
69
|
+
const imagePolicyParam = process.env.IMAGE_POLICY_PARAM;
|
|
70
|
+
if (!imagePolicyParam) {
|
|
71
|
+
throw new Error('IMAGE_POLICY_PARAM environment variable is not set');
|
|
72
|
+
}
|
|
73
|
+
const imagePolicy = await getImagePolicy(ssmClient, imagePolicyParam);
|
|
74
|
+
const image = sharp(buffer, { limitInputPixels: MAX_INPUT_PIXELS }).rotate();
|
|
75
|
+
const inputMeta = await image.metadata();
|
|
76
|
+
const maxDim = imagePolicy.masterMaxDimension;
|
|
77
|
+
if ((inputMeta.width ?? 0) > maxDim || (inputMeta.height ?? 0) > maxDim) {
|
|
78
|
+
image.resize(maxDim, maxDim, { fit: 'inside', withoutEnlargement: true });
|
|
79
|
+
}
|
|
80
|
+
const outputBuffer = await image.jpeg().toBuffer();
|
|
81
|
+
const outputMeta = await sharp(outputBuffer).metadata();
|
|
82
|
+
await s3Client.send(new PutObjectCommand({
|
|
83
|
+
Bucket: bucket,
|
|
84
|
+
Key: masterKey,
|
|
85
|
+
Body: outputBuffer,
|
|
86
|
+
ContentType: 'image/jpeg',
|
|
87
|
+
}));
|
|
88
|
+
await writeMediaRecord(fileId, {
|
|
89
|
+
status: 'completed',
|
|
90
|
+
contentType,
|
|
91
|
+
outputContentType: 'image/jpeg',
|
|
92
|
+
dimensions: { width: outputMeta.width ?? 0, height: outputMeta.height ?? 0 },
|
|
93
|
+
size: outputBuffer.length,
|
|
94
|
+
...(originalFilename ? { originalFilename } : {}),
|
|
95
|
+
...appMetadata,
|
|
96
|
+
}, requestId);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
await s3Client.send(new CopyObjectCommand({
|
|
100
|
+
Bucket: bucket,
|
|
101
|
+
Key: masterKey,
|
|
102
|
+
CopySource: `${bucket}/${key}`,
|
|
103
|
+
MetadataDirective: 'REPLACE',
|
|
104
|
+
ContentType: contentType,
|
|
105
|
+
ContentDisposition: originalFilename
|
|
106
|
+
? `attachment; filename="${sanitizeFilenameForHeader(originalFilename)}"`
|
|
107
|
+
: 'attachment',
|
|
108
|
+
}));
|
|
109
|
+
await writeMediaRecord(fileId, {
|
|
110
|
+
status: 'completed',
|
|
111
|
+
contentType,
|
|
112
|
+
outputContentType: contentType,
|
|
113
|
+
size: head.ContentLength ?? buffer.length,
|
|
114
|
+
...(originalFilename ? { originalFilename } : {}),
|
|
115
|
+
...appMetadata,
|
|
116
|
+
}, requestId);
|
|
117
|
+
}
|
|
118
|
+
logger.info('Media processed', { requestId, fileId, contentType });
|
|
119
|
+
}
|
|
120
|
+
export const handler = async (event) => {
|
|
121
|
+
for (const record of event.Records) {
|
|
122
|
+
const bucket = record.s3.bucket.name;
|
|
123
|
+
const key = record.s3.object.key;
|
|
124
|
+
try {
|
|
125
|
+
await processRecord(bucket, key);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
logger.error('Failed to process media', {
|
|
129
|
+
bucket,
|
|
130
|
+
key,
|
|
131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
132
|
+
});
|
|
133
|
+
// 再スロー: Lambda非同期呼び出しのリトライに乗せ、尽きたらon-failure宛先(DLQ)へ送る
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=process-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-handler.js","sourceRoot":"","sources":["../../../src/server/media/process-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SignMediaUrlParams, SignMediaUrlResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* メディア配信用の署名付きURLを生成する。
|
|
4
|
+
*
|
|
5
|
+
* width指定時は /resize/{fileId}?width=N、省略時は /master/{fileId} に署名する
|
|
6
|
+
* (呼び出し元が具体的な幅を決めてから呼ぶこと。ワイルドカード署名はしない)。
|
|
7
|
+
*
|
|
8
|
+
* 秘密鍵の取得はこの関数の責務外(引数で受け取るだけ)。
|
|
9
|
+
*/
|
|
10
|
+
export declare function signMediaUrl(params: SignMediaUrlParams): SignMediaUrlResult;
|
|
11
|
+
//# sourceMappingURL=sign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../../../src/server/media/sign.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAmBzE;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CAqB3E"}
|