S3 Presigned URL ile Büyük Dosya Yükleme: Lambda ve CDK
Lambda proxy yerine S3 presigned URL ile büyük dosya yükleme: eksiksiz CDK stack, iki handler, tarayıcı tarafı ve güvenlik ödünleşimleri.
Dosya yüklemelerini Lambda üzerinden geçirmek ilk akla gelen tasarımdır ve dosyalar büyüyene kadar da iş görür. API Gateway bir REST API isteğinin gövdesini 10 MB, Lambda ise senkron çağrı payload’ını 6 MB ile sınırlar; dolayısıyla 2 GB’lık bir video fonksiyona hiç ulaşmaz. Payload sınırın altında kaldığında bile transfer sürdüğü her saniye için Lambda faturalanır.
Birkaç megabaytın üzerindeki dosyalarda daha iyi varsayılan S3 presigned URL’dir. Lambda bir URL imzalayıp döner, client byte’ları doğrudan S3’e PUT eder, nesne yerine oturunca S3 event notification bir işleme fonksiyonunu uyandırır. Bedeli şudur: Lambda’nın akış sırasında uyguladığı kontroller (dosya tipi, boyut limiti, virüs taraması) ya imzalama anına ya da sonraki event handler’a taşınmak zorundadır.
Lambda Upload Sorunu#
Klasik Lambda proxy yaklaşımı genelde şöyle görünür:
// Proxy şekli: fonksiyon transferin tamamını elinde tutar
export const uploadHandler = async (event: APIGatewayEvent) => {
// Yalnızca gövde API Gateway payload limitinin altında kaldığı sürece erişilebilir
const file = parseMultipartFormData(event.body);
// Buffer'lanan gövde tüm fonksiyonun memory tabanını belirler
const processedFile = await processVideo(file);
// Faturalanan süreye artık kullanıcının internet hızı da dahil
const result = await s3.upload({
Bucket: 'my-videos',
Key: `uploads/${uuidv4()}`,
Body: processedFile,
}).promise();
return { statusCode: 200, body: JSON.stringify(result) };
};
Sorunlar ayar meselesi değil, yapısal:
- Sert payload tavanları: API Gateway 10 MB’ı aşan REST API gövdelerini Lambda daha çağrılmadan reddeder
- Memory dosyayla birlikte büyür: gövdeyi buffer’lamak, fonksiyonu kabul ettiğiniz en büyük dosyaya göre boyutlandırmak demektir
- Faturalanan duvar saati: fonksiyon ne kadar az CPU kullanırsa kullansın, GB-saniye transfer penceresi boyunca birikir
- Yavaş ağlarda timeout: zayıf bağlantıdaki bir mobil client integration timeout’unu tüketebilir
- İlerleme sinyali yok: client tek bir uzun istek görür, byte sayacı görmez
Daha Verimli Bir Mimari#
Veri yolu değişir; kontrol yolu olduğu yerde kalır.
Client artık Lambda’dan signed URL ister (S3 çağrısı olmayan, yerel bir imzalama işlemi), byte’ları doğrudan S3’e yükler, yükleme bitince S3 işleme Lambda’sını tetikler. Ağır trafik Lambda’dan geçmez.
Uçtan Uca Uygulama#
Büyük Dosya Yüklemeleri için CDK Altyapısı#
Bu pattern’i kuran eksiksiz bir CDK stack örneği:
// lib/file-upload-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
import * as iam from 'aws-cdk-lib/aws-iam';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class FileUploadStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Maliyet optimizasyonu için lifecycle policy'leri olan S3 bucket
const uploadBucket = new s3.Bucket(this, 'UploadBucket', {
bucketName: `${this.stackName}-uploads-${this.account}`,
cors: [
{
allowedOrigins: ['*'],
allowedMethods: [
s3.HttpMethods.PUT,
s3.HttpMethods.POST,
s3.HttpMethods.GET,
s3.HttpMethods.HEAD,
],
allowedHeaders: ['*'],
exposedHeaders: ['ETag'],
maxAge: 3600,
},
],
// Tamamlanmamış multipart upload'ları 7 gün sonra otomatik sil
lifecycleRules: [
{
id: 'AbortIncompleteMultipartUploads',
enabled: true,
abortIncompleteMultipartUploadsAfter: cdk.Duration.days(7),
},
{
id: 'TransitionToIA',
enabled: true,
transitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30),
},
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
},
],
// Güvenlik için public access'i engelle
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
});
// Signed URL generate eden Lambda - sadece CPU, S3'e gidiş dönüş yok
const signedUrlGenerator = new NodejsFunction(this, 'SignedUrlGenerator', {
entry: 'src/handlers/generate-signed-url.ts',
runtime: lambda.Runtime.NODEJS_20_X,
architecture: lambda.Architecture.ARM_64,
memorySize: 512, // Küçük memory footprint
timeout: cdk.Duration.seconds(30),
environment: {
UPLOAD_BUCKET: uploadBucket.bucketName,
ALLOWED_FILE_TYPES: 'video/mp4,video/quicktime,video/x-msvideo,image/jpeg,image/png',
MAX_FILE_SIZE: '10737418240', // 10GB byte cinsinden
SIGNED_URL_EXPIRY: '3600', // 1 saat
},
bundling: {
minify: true,
sourceMap: true,
target: 'es2022',
},
});
// Signed URL generator'a signed URL oluşturma yetkisi ver
uploadBucket.grantReadWrite(signedUrlGenerator);
signedUrlGenerator.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:PutObjectAcl', 's3:GetObject'],
resources: [uploadBucket.arnForObjects('*')],
})
);
// Post-upload processing için Lambda
const fileProcessor = new NodejsFunction(this, 'FileProcessor', {
entry: 'src/handlers/process-file.ts',
runtime: lambda.Runtime.NODEJS_20_X,
architecture: lambda.Architecture.ARM_64,
memorySize: 2048, // Processing için daha yüksek memory
timeout: cdk.Duration.minutes(5),
environment: {
UPLOAD_BUCKET: uploadBucket.bucketName,
},
bundling: {
minify: true,
sourceMap: true,
target: 'es2022',
// Video processing için gerekirse ffmpeg dahil et
nodeModules: ['fluent-ffmpeg'],
},
});
uploadBucket.grantReadWrite(fileProcessor);
// Processing tetiklemek için S3 event notification
uploadBucket.addEventNotification(
s3.EventType.OBJECT_CREATED,
new s3n.LambdaDestination(fileProcessor),
{ prefix: 'uploads/' } // Sadece uploads/ prefix'indeki dosyaları işle
);
// Signed URL generation için API Gateway
const api = new apigateway.RestApi(this, 'FileUploadApi', {
restApiName: 'File Upload API',
description: 'S3 signed URL generate etme API\'si',
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
},
});
const uploads = api.root.addResource('uploads');
const signedUrl = uploads.addResource('signed-url');
signedUrl.addMethod(
'POST',
new apigateway.LambdaIntegration(signedUrlGenerator, {
requestTemplates: {
'application/json': '{"body": $input.json("$")}',
},
})
);
// Outputlar
new cdk.CfnOutput(this, 'ApiUrl', {
value: api.url,
description: 'API Gateway URL',
});
new cdk.CfnOutput(this, 'BucketName', {
value: uploadBucket.bucketName,
description: 'S3 Upload Bucket Name',
});
}
}
Signed URL Üreteci#
İmzalama yerel bir SigV4 hesabıdır, yani bu handler S3 API’sini hiç çağırmaz. İsteği parse eder, politika kontrollerini uygular ve URL’i döner:
// src/handlers/generate-signed-url.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { z } from 'zod';
// Input validation şeması
const SignedUrlRequestSchema = z.object({
fileName: z.string().min(1).max(255),
fileSize: z.number().int().min(1).max(10737418240), // 10GB max
fileType: z.string().regex(/^(video|image|audio)\/[a-zA-Z0-9][a-zA-Z0-9\!\-\_]*[a-zA-Z0-9]*$/),
uploadId: z.string().uuid().optional(), // Takip için
});
const s3Client = new S3Client({ region: process.env.AWS_REGION });
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
console.log('Signed URL generate isteği:', {
body: event.body,
headers: event.headers
});
try {
// Request'i parse et ve validate et
const body = JSON.parse(event.body || '{}');
const request = SignedUrlRequestSchema.parse(body);
// Güvenlik kontrolleri
const allowedTypes = process.env.ALLOWED_FILE_TYPES?.split(',') || [];
if (!allowedTypes.includes(request.fileType)) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Dosya tipi izin verilmeyen',
allowedTypes,
}),
};
}
const maxSize = parseInt(process.env.MAX_FILE_SIZE || '5368709120'); // 5GB default
if (request.fileSize > maxSize) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Dosya çok büyük',
maxSize,
receivedSize: request.fileSize,
}),
};
}
// Timestamp ve sanitized filename ile unique key generate et
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const sanitizedFileName = request.fileName.replace(/[^a-zA-Z0-9.-]/g, '_');
const key = `uploads/${timestamp}-${sanitizedFileName}`;
// PUT request için signed URL oluştur
const putObjectCommand = new PutObjectCommand({
Bucket: process.env.UPLOAD_BUCKET!,
Key: key,
ContentType: request.fileType,
ContentLength: request.fileSize,
// Processing için metadata ekle
Metadata: {
'original-filename': request.fileName,
'upload-id': request.uploadId || 'direct-upload',
'file-size': request.fileSize.toString(),
'uploaded-at': new Date().toISOString(),
},
// Güvenlik header'ları
ServerSideEncryption: 'AES256',
});
const signedUrl = await getSignedUrl(s3Client, putObjectCommand, {
expiresIn: parseInt(process.env.SIGNED_URL_EXPIRY || '3600'), // 1 saat default
});
console.log('Signed URL başarıyla generate edildi:', {
key,
fileSize: request.fileSize,
fileType: request.fileType,
expiresIn: process.env.SIGNED_URL_EXPIRY,
});
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache',
},
body: JSON.stringify({
signedUrl,
key,
method: 'PUT',
headers: {
'Content-Type': request.fileType,
'Content-Length': request.fileSize.toString(),
},
expiresAt: new Date(Date.now() + parseInt(process.env.SIGNED_URL_EXPIRY || '3600') * 1000).toISOString(),
}),
};
} catch (error) {
console.error('Signed URL generation hatası:', error);
if (error instanceof z.ZodError) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Geçersiz request',
details: error.errors,
}),
};
}
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: 'Signed URL generate edilemedi',
}),
};
}
};
Dosya İşleme Lambda’sı - Sadece Gerekince Çalışır#
Bu Lambda sadece dosyalar S3’e başarıyla yüklendiğinde çalışır:
// src/handlers/process-file.ts
import { S3Event } from 'aws-lambda';
import { S3Client, HeadObjectCommand, CopyObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
const s3Client = new S3Client({ region: process.env.AWS_REGION });
const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
const sesClient = new SESClient({ region: process.env.AWS_REGION });
export const handler = async (event: S3Event): Promise<void> => {
console.log('Yüklenen dosyalar işleniyor:', JSON.stringify(event, null, 2));
for (const record of event.Records) {
if (record.eventName?.startsWith('ObjectCreated')) {
await processUploadedFile(record);
}
}
};
async function processUploadedFile(record: any) {
const bucketName = record.s3.bucket.name;
const objectKey = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
const fileSize = record.s3.object.size;
console.log('Dosya işleniyor:', { bucketName, objectKey, fileSize });
try {
// Nesne gövdesini indirmeden metadata'yı oku
const headResponse = await s3Client.send(new HeadObjectCommand({
Bucket: bucketName,
Key: objectKey,
}));
const metadata = headResponse.Metadata || {};
const originalFilename = metadata['original-filename'] || objectKey;
const uploadId = metadata['upload-id'] || 'unknown';
// Dosya tipini ve işleme stratejisini belirle
const contentType = headResponse.ContentType || '';
let processingStatus = 'completed';
let processedKey = objectKey;
if (contentType.startsWith('video/')) {
// Video dosyaları transcoding gerektirebilir
processingStatus = 'processing';
// Production'da burada AWS MediaConvert tetiklenebilir
console.log('Video dosyası algılandı, transcoding tetiklenir');
// Demo için sadece processed klasörüne taşı
processedKey = objectKey.replace('uploads/', 'processed/videos/');
await s3Client.send(new CopyObjectCommand({
CopySource: `${bucketName}/${objectKey}`,
Bucket: bucketName,
Key: processedKey,
}));
processingStatus = 'completed';
} else if (contentType.startsWith('image/')) {
// Resim dosyaları yeniden boyutlandırma/optimizasyon gerektirebilir
console.log('Resim dosyası algılandı, işleme tetiklenir');
processedKey = objectKey.replace('uploads/', 'processed/images/');
await s3Client.send(new CopyObjectCommand({
CopySource: `${bucketName}/${objectKey}`,
Bucket: bucketName,
Key: processedKey,
}));
}
// İşleme sonucunu veritabanında sakla
await dynamoClient.send(new PutItemCommand({
TableName: process.env.FILES_TABLE || 'processed-files',
Item: {
fileId: { S: uploadId },
originalKey: { S: objectKey },
processedKey: { S: processedKey },
originalFilename: { S: originalFilename },
fileSize: { N: fileSize.toString() },
contentType: { S: contentType },
status: { S: processingStatus },
uploadedAt: { S: new Date().toISOString() },
processedAt: { S: new Date().toISOString() },
},
}));
// Opsiyonel: Bildirim e-postası gönder
if (metadata['notification-email']) {
await sesClient.send(new SendEmailCommand({
Source: 'noreply@yourapp.com',
Destination: {
ToAddresses: [metadata['notification-email']],
},
Message: {
Subject: {
Data: 'File Upload Processed Successfully',
},
Body: {
Text: {
Data: `Your file "${originalFilename}" has been successfully uploaded and processed.`,
},
},
},
}));
}
// Processed konuma taşındıysa orijinal upload'ı temizle
if (processedKey !== objectKey) {
await s3Client.send(new DeleteObjectCommand({
Bucket: bucketName,
Key: objectKey,
}));
}
console.log('Dosya işleme tamamlandı:', {
uploadId,
originalKey: objectKey,
processedKey,
status: processingStatus,
});
} catch (error) {
console.error('Dosya işleme başarısız:', error);
// Veritabanını hata statüsü ile güncelle
await dynamoClient.send(new PutItemCommand({
TableName: process.env.FILES_TABLE || 'processed-files',
Item: {
fileId: { S: record.s3.object.eTag },
originalKey: { S: objectKey },
status: { S: 'failed' },
errorMessage: { S: error instanceof Error ? error.message : 'Unknown error' },
uploadedAt: { S: new Date().toISOString() },
failedAt: { S: new Date().toISOString() },
},
}));
throw error; // Gerekirse retry tetiklemek için tekrar fırlat
}
}
Frontend Tarafı - React/TypeScript#
Client tarafı iki adımdan ibaret: URL iste, sonra o URL’e PUT et.
// hooks/useFileUpload.ts
import { useState, useCallback } from 'react';
interface UploadProgress {
loaded: number;
total: number;
percentage: number;
}
interface UseFileUploadReturn {
upload: (file: File) => Promise<string>;
progress: UploadProgress | null;
isUploading: boolean;
error: string | null;
}
export const useFileUpload = (): UseFileUploadReturn => {
const [progress, setProgress] = useState<UploadProgress | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const upload = useCallback(async (file: File): Promise<string> => {
setIsUploading(true);
setError(null);
setProgress(null);
try {
console.log('Dosya için upload başlatılıyor:', {
name: file.name,
size: file.size,
type: file.type,
});
// Adım 1: Signed URL iste
const signedUrlResponse = await fetch('/api/uploads/signed-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: file.name,
fileSize: file.size,
fileType: file.type,
uploadId: crypto.randomUUID(),
}),
});
if (!signedUrlResponse.ok) {
const errorData = await signedUrlResponse.json();
throw new Error(errorData.error || 'Signed URL alınamadı');
}
const { signedUrl, key, headers } = await signedUrlResponse.json();
console.log('Signed URL alındı, direkt S3 upload başlatılıyor');
// Adım 2: Progress tracking ile direkt S3'e upload
const uploadResponse = await fetch(signedUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type,
'Content-Length': file.size.toString(),
...headers,
},
body: file,
});
if (!uploadResponse.ok) {
throw new Error(`Upload başarısız: ${uploadResponse.status} ${uploadResponse.statusText}`);
}
console.log('Upload başarıyla tamamlandı');
return key; // Referans için S3 object key'i döndür
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Upload başarısız';
setError(errorMessage);
console.error('Upload hatası:', err);
throw err;
} finally {
setIsUploading(false);
setProgress(null);
}
}, []);
return {
upload,
progress,
isUploading,
error,
};
};
// fetch upload ilerlemesini raporlamaz; ilerleme çubuğu için XMLHttpRequest gerekir
export const useFileUploadWithProgress = (): UseFileUploadReturn => {
const [progress, setProgress] = useState<UploadProgress | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const upload = useCallback(async (file: File): Promise<string> => {
setIsUploading(true);
setError(null);
setProgress({ loaded: 0, total: file.size, percentage: 0 });
try {
// Signed URL al
const signedUrlResponse = await fetch('/api/uploads/signed-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
fileSize: file.size,
fileType: file.type,
uploadId: crypto.randomUUID(),
}),
});
if (!signedUrlResponse.ok) {
const errorData = await signedUrlResponse.json();
throw new Error(errorData.error || 'Signed URL alınamadı');
}
const { signedUrl, key } = await signedUrlResponse.json();
// Progress tracking ile upload
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentage = Math.round((event.loaded / event.total) * 100);
setProgress({
loaded: event.loaded,
total: event.total,
percentage,
});
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
setProgress({
loaded: file.size,
total: file.size,
percentage: 100,
});
resolve(key);
} else {
reject(new Error(`Upload başarısız: ${xhr.status} ${xhr.statusText}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Ağ hatası nedeniyle upload başarısız'));
});
xhr.addEventListener('abort', () => {
reject(new Error('Upload iptal edildi'));
});
xhr.open('PUT', signedUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Upload başarısız';
setError(errorMessage);
throw err;
} finally {
setIsUploading(false);
}
}, []);
return { upload, progress, isUploading, error };
};
React Upload Bileşeni#
// components/FileUploader.tsx
import React, { useCallback, useState } from 'react';
import { useFileUploadWithProgress } from '../hooks/useFileUpload';
interface FileUploaderProps {
onUploadComplete?: (key: string) => void;
onUploadError?: (error: string) => void;
acceptedTypes?: string[];
maxSize?: number;
}
export const FileUploader: React.FC<FileUploaderProps> = ({
onUploadComplete,
onUploadError,
acceptedTypes = ['video/*', 'image/*'],
maxSize = 10 * 1024 * 1024 * 1024, // 10GB default
}) => {
const { upload, progress, isUploading, error } = useFileUploadWithProgress();
const [dragOver, setDragOver] = useState(false);
const handleFileSelect = useCallback(async (files: FileList | null) => {
if (!files || files.length === 0) return;
const file = files[0];
// Dosya tipini doğrula
const isValidType = acceptedTypes.some(type => {
if (type.endsWith('/*')) {
return file.type.startsWith(type.slice(0, -1));
}
return file.type === type;
});
if (!isValidType) {
const errorMsg = `File type not allowed. Accepted types: ${acceptedTypes.join(', ')}`;
onUploadError?.(errorMsg);
return;
}
// Dosya boyutunu doğrula
if (file.size > maxSize) {
const errorMsg = `File too large. Maximum size: ${Math.round(maxSize / 1024 / 1024)}MB`;
onUploadError?.(errorMsg);
return;
}
try {
const key = await upload(file);
onUploadComplete?.(key);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Upload failed';
onUploadError?.(errorMsg);
}
}, [upload, acceptedTypes, maxSize, onUploadComplete, onUploadError]);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
handleFileSelect(e.dataTransfer.files);
}, [handleFileSelect]);
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
}, []);
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
return (
<div className="w-full max-w-xl mx-auto">
<div
className={`
border-2 border-dashed rounded-lg p-8 text-center transition-colors
${dragOver ? 'border-blue-400 bg-blue-50' : 'border-gray-300'}
${isUploading ? 'pointer-events-none opacity-60' : 'hover:border-gray-400'}
`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
{isUploading ? (
<div className="space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="text-sm text-gray-600">Uploading...</p>
{progress && (
<div className="space-y-2">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress.percentage}%` }}
></div>
</div>
<p className="text-xs text-gray-500">
{formatFileSize(progress.loaded)} / {formatFileSize(progress.total)} ({progress.percentage}%)
</p>
</div>
)}
</div>
) : (
<>
<svg
className="w-12 h-12 text-gray-400 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<p className="text-lg font-medium text-gray-900 mb-2">
Drop files here or click to browse
</p>
<p className="text-sm text-gray-500 mb-4">
Maximum file size: {formatFileSize(maxSize)}
</p>
<p className="text-xs text-gray-400">
Accepted types: {acceptedTypes.join(', ')}
</p>
</>
)}
<input
type="file"
className="hidden"
accept={acceptedTypes.join(',')}
onChange={(e) => handleFileSelect(e.target.files)}
disabled={isUploading}
id="file-input"
/>
{!isUploading && (
<label
htmlFor="file-input"
className="absolute inset-0 cursor-pointer"
/>
)}
</div>
{error && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
</div>
);
};
Güvenlik Hususları#
1. Dosya Tipi Validation (Hem Client hem Server)#
// Client-side validation'a asla tek başına güvenme
// mp4 işareti 4. byte'ta başlar; ilk dört byte değişken bir box uzunluğudur
const FILE_SIGNATURES = {
'image/jpeg': { offset: 0, bytes: [0xFF, 0xD8, 0xFF] },
'image/png': { offset: 0, bytes: [0x89, 0x50, 0x4E, 0x47] },
'video/mp4': { offset: 4, bytes: [0x66, 0x74, 0x79, 0x70] }, // "ftyp"
} as const;
function validateFileType(header: Buffer, declaredType: string): boolean {
const signature = FILE_SIGNATURES[declaredType as keyof typeof FILE_SIGNATURES];
if (!signature) return false;
return signature.bytes.every(
(byte, index) => header[signature.offset + index] === byte
);
}
// Processing Lambda'nızda
const ranged = await s3Client.send(new GetObjectCommand({
Bucket: bucketName,
Key: objectKey,
Range: 'bytes=0-15', // Nesnenin tamamı değil, sadece header byte'ları
}));
// SDK v3'te Body bir stream'dir; index'lemeden önce topla
const header = Buffer.from(await ranged.Body!.transformToByteArray());
if (!validateFileType(header, contentType)) {
throw new Error('Dosya tipi validation başarısız');
}
2. Boyut Limitleri ve Timeout Koruması#
// Signed URL generator'da
const generateSignedUrl = async (request: SignedUrlRequest) => {
// Kullanıcı tier'ına göre kademeli boyut limitleri uygula
const userTier = await getUserTier(request.userId);
const maxSize = SIZE_LIMITS[userTier] || SIZE_LIMITS.free;
if (request.fileSize > maxSize) {
throw new Error(`File size exceeds ${userTier} tier limit`);
}
// Dosya boyutuna göre uygun expiry ayarla
// Daha büyük dosyalar daha uzun upload penceresi alır. expiresIn ne olursa olsun
// URL, Lambda execution role'ünün geçici kimlik bilgileriyle birlikte ölür.
const expirySeconds = Math.min(
3600, // 1 saat max
Math.max(300, request.fileSize / 1024 / 1024 * 10) // MB başına 10 saniye
);
return getSignedUrl(s3Client, putCommand, { expiresIn: expirySeconds });
};
3. Erişim Kontrolü ve Audit Logging#
// Processing Lambda'da. Burada yapılandırılmış bir log satırı yeterli: CloudTrail
// S3 API çağrılarını tutar, ama uygulamanızın "kim neyi yükledi" görüşünü tutmaz.
const logFileUpload = async (uploadData: UploadData) => {
const event = {
eventTime: new Date().toISOString(),
eventName: 'FileUploaded',
eventSource: 'custom.fileupload',
userIdentity: {
type: 'Unknown',
principalId: uploadData.userId,
},
resources: [{
resourceName: uploadData.s3Key,
resourceType: 'AWS::S3::Object',
}],
requestParameters: {
bucketName: uploadData.bucketName,
key: uploadData.s3Key,
fileSize: uploadData.fileSize,
contentType: uploadData.contentType,
},
};
// Monitoring için CloudWatch'a logla
console.log('File upload audit log:', event);
// Opsiyonel olarak EventBridge'e veya kendi audit deponuza ilet
};
Maliyet Modelinde Ne Değişir#
İki tasarım farklı eksenlerde faturalanır; bu yüzden anlamlı karşılaştırma yapısaldır ve tek bir orana indirgenemez.
Proxy tasarımında Lambda, transfer penceresinin tamamı boyunca GB-saniye biriktirir. 3 GB’a boyutlandırılmış ve bağlantıyı on dakika tutan bir fonksiyon, ne kadar az CPU harcarsa harcasın tek bir upload için 1.800 GB-saniye maliyet çıkarır. Kullanıcınızın internet hızı sizin faturanıza yazılır. Presigned URL’de aynı upload, URL’i imzalayan kısa bir çağrı artı nesne yerine oturduktan sonraki bir çağrı kadar tutar; transferin kendisi bir S3 PUT’udur. İnternetten S3’e gelen veri transferi ise ücretlendirilmez.
Bir rakam telaffuz etmeden önce bunu kendi hacminizle AWS Pricing Calculator (yeni sekmede açılır) üzerinde modelleyin. Oran, upload’larınızın ne kadar sürdüğüne göre kayar; bunu belirleyen de kullanıcılarınızın bağlantısıdır, yani başkasının rakamları size taşınmaz.
Gecikme ve güvenilirlik de aynı yapısal nedenle değişir. İmzalama S3’e gidiş dönüş gerektirmediği için client’ın beklediği istek kısa ve öngörülebilirdir. Upload penceresi artık API Gateway integration timeout’u yerine presigned URL’in geçerlilik süresiyle sınırlıdır ve eşzamanlılığı hesabınızın Lambda limiti yerine S3 emer. Cold start’lar upload yolundan tamamen çıkar; çünkü o yoldaki tek fonksiyon, byte’lar hareket etmeye başlamadan önce döner.
Üretim için Gelişmiş Pattern’ler#
1. 100MB+ Dosyalar için Multipart Upload#
// Multipart upload'lar için genişletilmiş signed URL generator
import { CreateMultipartUploadCommand, UploadPartCommand } from '@aws-sdk/client-s3';
const generateMultipartUrls = async (request: LargeFileRequest) => {
const partSize = 100 * 1024 * 1024; // 100MB parça
const numParts = Math.ceil(request.fileSize / partSize);
// Multipart upload'ı başlat
const multipart = await s3Client.send(new CreateMultipartUploadCommand({
Bucket: process.env.UPLOAD_BUCKET!,
Key: request.key,
ContentType: request.fileType,
}));
// Her parça için signed URL generate et
const partUrls = await Promise.all(
Array.from({ length: numParts }, async (_, index) => {
const partNumber = index + 1;
const command = new UploadPartCommand({
Bucket: process.env.UPLOAD_BUCKET!,
Key: request.key,
PartNumber: partNumber,
UploadId: multipart.UploadId,
});
const signedUrl = await getSignedUrl(s3Client, command, {
expiresIn: 3600,
});
return {
partNumber,
signedUrl,
size: Math.min(partSize, request.fileSize - index * partSize),
};
})
);
return {
uploadId: multipart.UploadId,
parts: partUrls,
};
};
2. State Takibi ile Devam Ettirilebilir Upload’lar#
// Client-side resumable upload mantığı
export class ResumableUpload {
private uploadId: string;
private parts: UploadPart[];
private completedParts: CompletedPart[] = [];
async resumeUpload(file: File, uploadId?: string): Promise<string> {
if (uploadId) {
// Mevcut upload'a devam et
this.uploadId = uploadId;
this.completedParts = await this.getCompletedParts(uploadId);
} else {
// Yeni multipart upload başlat
const response = await this.initializeUpload(file);
this.uploadId = response.uploadId;
this.parts = response.parts;
}
// Kalan parçaları yükle
const pendingParts = this.parts.filter(
part => !this.completedParts.some(c => c.partNumber === part.partNumber)
);
for (const part of pendingParts) {
await this.uploadPart(file, part);
}
// Multipart upload'ı tamamla
return this.completeUpload();
}
private async uploadPart(file: File, part: UploadPart): Promise<void> {
const start = (part.partNumber - 1) * part.size;
const end = Math.min(start + part.size, file.size);
const chunk = file.slice(start, end);
const response = await fetch(part.signedUrl, {
method: 'PUT',
body: chunk,
});
if (response.ok) {
this.completedParts.push({
partNumber: part.partNumber,
etag: response.headers.get('ETag')!,
});
// Devam ettirme için ilerlemeyi localStorage'a kaydet
localStorage.setItem(`upload_${this.uploadId}`, JSON.stringify({
completedParts: this.completedParts,
totalParts: this.parts.length,
}));
}
}
}
3. Virus Tarama Entegrasyonu#
// Upload sonrası virus taraması
import { ClamAVClient } from 'clamav-js'; // Örnek kütüphane
const scanUploadedFile = async (s3Event: S3Event) => {
for (const record of s3Event.Records) {
const bucket = record.s3.bucket.name;
const key = record.s3.object.key;
// Tarama için dosyayı indir (büyük dosyalar için stream)
const fileStream = (await s3Client.send(new GetObjectCommand({
Bucket: bucket,
Key: key,
}))).Body as Readable;
// ClamAV veya benzeri ile tara
const scanResult = await clamav.scanStream(fileStream);
if (scanResult.isInfected) {
console.warn('Infected file detected:', { bucket, key, virus: scanResult.viruses });
// Dosyayı karantinaya al
await s3Client.send(new CopyObjectCommand({
CopySource: `${bucket}/${key}`,
Bucket: `${bucket}-quarantine`,
Key: key,
}));
// Orijinali sil
await s3Client.send(new DeleteObjectCommand({
Bucket: bucket,
Key: key,
}));
// Kullanıcıyı bilgilendir
await notifyUser(key, 'File rejected due to security scan');
} else {
// Dosya temiz, normal işleme devam et
await processCleanFile(bucket, key);
}
}
};
Monitoring ve Alerting#
CloudWatch Dashboard’ları#
// CDK monitoring stack
const dashboard = new cloudwatch.Dashboard(this, 'FileUploadDashboard', {
dashboardName: 'FileUploadMetrics',
widgets: [
[
new cloudwatch.GraphWidget({
title: 'Signed URL Generation',
left: [
signedUrlGenerator.metricDuration(),
signedUrlGenerator.metricErrors(),
],
right: [signedUrlGenerator.metricInvocations()],
}),
],
[
new cloudwatch.GraphWidget({
title: 'S3 Upload Metrics',
left: [
new cloudwatch.Metric({
namespace: 'AWS/S3',
metricName: 'NumberOfObjects',
dimensionsMap: { BucketName: uploadBucket.bucketName },
}),
],
}),
],
[
new cloudwatch.GraphWidget({
title: 'File Processing',
left: [
fileProcessor.metricDuration(),
fileProcessor.metricErrors(),
],
}),
],
],
});
// Production monitoring için alarm'lar
new cloudwatch.Alarm(this, 'HighErrorRate', {
metric: signedUrlGenerator.metricErrors(),
threshold: 10,
evaluationPeriods: 2,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
Form Verileri ve Dosya Metadata’sı#
Dosya Upload’larıyla Birlikte Ek Form Alanları İşleme#
Production’da genellikle yüklenen dosyalarla metadata ilişkilendirmeniz gerekir. Form verilerini signed URL upload’larla birlikte şöyle ele alırsınız:
// Metadata'lı genişletilmiş request şeması
const FileUploadWithMetadataSchema = z.object({
// Dosya bilgisi
fileName: z.string().min(1).max(255),
fileSize: z.number().int().min(1).max(10737418240),
fileType: z.string().regex(/^(video|image|audio)\/[a-zA-Z0-9][a-zA-Z0-9\!\-\_]*[a-zA-Z0-9]*$/),
// İş metadata'sı
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
category: z.enum(['education', 'entertainment', 'business', 'other']),
tags: z.array(z.string()).max(10),
isPublic: z.boolean(),
// Upload metadata'sı
uploadId: z.string().uuid(),
userId: z.string().uuid(),
organizationId: z.string().uuid().optional(),
});
// S3 objesine metadata dahil etmek için değiştirilmiş signed URL generator
const putObjectCommand = new PutObjectCommand({
Bucket: process.env.UPLOAD_BUCKET!,
Key: key,
ContentType: request.fileType,
ContentLength: request.fileSize,
Metadata: {
'original-filename': request.fileName,
'upload-id': request.uploadId,
'user-id': request.userId,
'title': request.title,
'description': request.description || '',
'category': request.category,
'tags': JSON.stringify(request.tags),
'is-public': request.isPublic.toString(),
'uploaded-at': new Date().toISOString(),
},
// Daha iyi organizasyon ve faturalandırma için object tag'leri ekle
Tagging: `Category=${request.category}&IsPublic=${request.isPublic}&UserId=${request.userId}`,
});
İki Aşamalı Upload Pattern’i#
Karmaşık formlar için iki aşamalı bir yaklaşım uygulayın:
// Aşama 1: Veritabanında upload kaydı oluştur
const createUploadRecord = async (metadata: FileMetadata) => {
const uploadRecord = {
id: metadata.uploadId,
userId: metadata.userId,
fileName: metadata.fileName,
fileSize: metadata.fileSize,
title: metadata.title,
description: metadata.description,
category: metadata.category,
tags: metadata.tags,
isPublic: metadata.isPublic,
status: 'pending', // pending -> uploading -> processing -> completed
createdAt: new Date().toISOString(),
};
await dynamoClient.send(new PutItemCommand({
TableName: process.env.UPLOAD_RECORDS_TABLE!,
Item: marshall(uploadRecord),
}));
return uploadRecord;
};
// Aşama 2: S3 upload tamamlandığında kaydı güncelle
const updateUploadStatus = async (uploadId: string, s3Key: string, status: string) => {
await dynamoClient.send(new UpdateItemCommand({
TableName: process.env.UPLOAD_RECORDS_TABLE!,
Key: marshall({ id: uploadId }),
UpdateExpression: 'SET #status = :status, #s3Key = :s3Key, #updatedAt = :updatedAt',
ExpressionAttributeNames: {
'#status': 'status',
'#s3Key': 's3Key',
'#updatedAt': 'updatedAt',
},
ExpressionAttributeValues: marshall({
':status': status,
':s3Key': s3Key,
':updatedAt': new Date().toISOString(),
}),
}));
};
Data Retention ve Lifecycle Management#
S3 Lifecycle Rules ile Otomatik Data Lifecycle#
// Kapsamlı lifecycle management ile genişletilmiş CDK stack
const uploadBucket = new s3.Bucket(this, 'UploadBucket', {
lifecycleRules: [
// Kural 1: Başarısız multipart upload'ları temizle
{
id: 'CleanupFailedUploads',
enabled: true,
abortIncompleteMultipartUploadsAfter: cdk.Duration.days(1),
},
// Kural 2: Erişim patterns'a göre transition
{
id: 'StorageClassTransitions',
enabled: true,
transitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30),
},
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
{
storageClass: s3.StorageClass.DEEP_ARCHIVE,
transitionAfter: cdk.Duration.days(365),
},
],
},
// Kural 3: Geçici/processing dosyalarını sil
{
id: 'CleanupTempFiles',
enabled: true,
prefix: 'temp/',
expiration: cdk.Duration.days(7),
},
// Kural 4: Kullanıcı-spesifik retention (örnek: ücretsiz tier kullanıcıları)
// Nesnenin bu tag'i taşıması gerekir; imzalama anında Tagging ile ekleyin
{
id: 'FreeTierRetention',
enabled: true,
tagFilters: { UserTier: 'free' },
expiration: cdk.Duration.days(90),
},
],
// Versioning'i yanlışlıkla silinme koruması için etkinleştir
versioned: true,
});
Kullanıcı Kontrollü Data Retention#
// Kullanıcı silme isteklerini işleyen Lambda
export const deleteFileHandler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
const { fileId } = JSON.parse(event.body || '{}');
const userId = getUserIdFromJWT(event.headers.authorization);
// Sahiplik doğrula
const fileRecord = await dynamoClient.send(new GetItemCommand({
TableName: process.env.UPLOAD_RECORDS_TABLE!,
Key: marshall({ id: fileId }),
}));
if (!fileRecord.Item) {
return { statusCode: 404, body: JSON.stringify({ error: 'Dosya bulunamadı' }) };
}
const file = unmarshall(fileRecord.Item);
if (file.userId !== userId) {
return { statusCode: 403, body: JSON.stringify({ error: 'Erişim reddedildi' }) };
}
// Önce soft delete (silinmiş olarak işaretle, ama S3'den hemen kaldırma)
await dynamoClient.send(new UpdateItemCommand({
TableName: process.env.UPLOAD_RECORDS_TABLE!,
Key: marshall({ id: fileId }),
UpdateExpression: 'SET #status = :status, #deletedAt = :deletedAt',
ExpressionAttributeNames: {
'#status': 'status',
'#deletedAt': 'deletedAt',
},
ExpressionAttributeValues: marshall({
':status': 'deleted',
':deletedAt': new Date().toISOString(),
}),
}));
// Grace period'dan sonra lifecycle cleanup için S3 delete tag'i ekle
await s3Client.send(new PutObjectTaggingCommand({
Bucket: process.env.UPLOAD_BUCKET!,
Key: file.s3Key,
Tagging: {
TagSet: [
{ Key: 'Status', Value: 'deleted' },
{ Key: 'DeletedAt', Value: new Date().toISOString() },
{ Key: 'GracePeriodDays', Value: '30' },
],
},
}));
return {
statusCode: 200,
body: JSON.stringify({
message: 'Dosya silme için planlandı',
gracePeriod: '30 gün',
}),
};
} catch (error) {
console.error('Dosya silme hatası:', error);
return { statusCode: 500, body: JSON.stringify({ error: 'Silme başarısız' }) };
}
};
GDPR Uyumluluk ve Data Portability#
// Kullanıcı veri export'u için Lambda (GDPR Madde 20)
export const exportUserDataHandler = async (event: APIGatewayProxyEvent) => {
const userId = getUserIdFromJWT(event.headers.authorization);
// Kullanıcının tüm dosyalarını al
const userFiles = await dynamoClient.send(new ScanCommand({
TableName: process.env.UPLOAD_RECORDS_TABLE!,
FilterExpression: '#userId = :userId',
ExpressionAttributeNames: { '#userId': 'userId' },
ExpressionAttributeValues: marshall({ ':userId': userId }),
}));
// Tüm dosyalar için download link'leri generate et
const fileExports = await Promise.all(
(userFiles.Items || []).map(async (item) => {
const file = unmarshall(item);
if (file.status !== 'completed') return null;
// Geçici download URL generate et
const downloadUrl = await getSignedUrl(
s3Client,
new GetObjectCommand({
Bucket: process.env.UPLOAD_BUCKET!,
Key: file.s3Key,
}),
{ expiresIn: 3600 * 24 } // 24 saat
);
return {
fileId: file.id,
originalFileName: file.fileName,
title: file.title,
description: file.description,
category: file.category,
tags: file.tags,
uploadedAt: file.createdAt,
fileSize: file.fileSize,
downloadUrl,
};
})
);
const exportData = {
userId,
exportedAt: new Date().toISOString(),
files: fileExports.filter(Boolean),
summary: {
totalFiles: fileExports.filter(Boolean).length,
totalSize: fileExports.reduce((sum, file) => sum + (file?.fileSize || 0), 0),
},
};
// Export'u geçici bir konuma yaz
const exportKey = `exports/${userId}/${Date.now()}.json`;
await s3Client.send(new PutObjectCommand({
Bucket: process.env.EXPORT_BUCKET!,
Key: exportKey,
Body: JSON.stringify(exportData, null, 2),
ContentType: 'application/json',
}));
// Export dosyası için download URL üret
const exportDownloadUrl = await getSignedUrl(
s3Client,
new GetObjectCommand({
Bucket: process.env.EXPORT_BUCKET!,
Key: exportKey,
}),
{ expiresIn: 3600 * 24 * 7 } // 7 gün
);
return {
statusCode: 200,
body: JSON.stringify({
exportUrl: exportDownloadUrl,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
summary: exportData.summary,
}),
};
};
Maliyet Optimizasyon Stratejileri#
Akıllı Storage Class Seçimi#
// Kullanım patterns'ı analiz edip storage class'ları optimize eden Lambda
export const optimizeStorageHandler = async (event: ScheduledEvent) => {
const s3Inventory = await getS3Inventory(); // S3 inventory report'larından
for (const object of s3Inventory) {
const lastAccessed = await getObjectAccessTime(object.key);
const daysSinceAccess = (Date.now() - lastAccessed) / (1000 * 60 * 60 * 24);
// Erişim patterns'a göre otomatik transition
if (daysSinceAccess > 90 && object.storageClass === 'STANDARD') {
await s3Client.send(new CopyObjectCommand({
CopySource: `${object.bucket}/${object.key}`,
Bucket: object.bucket,
Key: object.key,
StorageClass: 'GLACIER',
MetadataDirective: 'COPY',
}));
console.log('Glacier\'a geçirildi:', { key: object.key, daysSinceAccess });
}
// Çok eski dosyalar için deep archive
if (daysSinceAccess > 365 && object.storageClass === 'GLACIER') {
await s3Client.send(new CopyObjectCommand({
CopySource: `${object.bucket}/${object.key}`,
Bucket: object.bucket,
Key: object.key,
StorageClass: 'DEEP_ARCHIVE',
MetadataDirective: 'COPY',
}));
console.log('Deep Archive\'a geçirildi:', { key: object.key, daysSinceAccess });
}
}
};
Kullanım Bazlı Faturalandırma Entegrasyonu#
// Kullanım bazlı faturalandırma için dosya erişimini takip et
const trackFileAccess = async (userId: string, fileId: string, operation: string) => {
await dynamoClient.send(new PutItemCommand({
TableName: process.env.USAGE_TRACKING_TABLE!,
Item: marshall({
id: `${userId}#${Date.now()}`,
userId,
fileId,
operation, // 'upload', 'download', 'view', 'delete'
timestamp: new Date().toISOString(),
month: new Date().toISOString().substring(0, 7), // Faturalandırma için YYYY-MM
}),
}));
// Kullanıcının aylık kullanım sayacını güncelle
await dynamoClient.send(new UpdateItemCommand({
TableName: process.env.USER_USAGE_TABLE!,
Key: marshall({
userId,
month: new Date().toISOString().substring(0, 7),
}),
UpdateExpression: 'ADD #operation :increment',
ExpressionAttributeNames: { '#operation': operation },
ExpressionAttributeValues: marshall({ ':increment': 1 }),
}));
};
İki Tasarım Arasında Seçim#
Payload bir form alanından büyükse ve client S3’e doğrudan erişebiliyorsa varsayılan olarak presigned URL kullanın. Lambda’yı veri yolunda yalnızca içeriği depolanmadan önce görmesi gerektiğinde tutun: doğrulanmamış nesnelerin bucket’a düşmesini yasaklayan bir uyum kuralı ya da client’a bırakılamayacak bir dönüştürme. Bu durumların çoğu bile, karantina prefix’ine yazıp işleme fonksiyonu onayladıktan sonra nesneyi terfi ettirmekle daha iyi çözülür.
Kararı ters yöne çeviren iki kısıt var. Client S3’e hiç ulaşamıyorsa (kilitli bir kurumsal proxy ya da yalnızca sizin API’nize POST edecek bir partner entegrasyonu) presigned URL size bir şey kazandırmaz; yeniden proxy boyutlandırmaya dönersiniz. Upload’lar güvenilmeyen bir tarayıcıdan geliyorsa presigned URL’i ne ise o kabul edin: taşıyıcı bir kimlik bilgisi. İmza, URL’i tek bir nesne anahtarına bağlar; başka bir yere yazılamaz ama süresi dolana kadar aynı anahtara tekrar PUT edilebilir. ContentType ve ContentLength sabitlemek neyin yazılabileceğini daraltır, URL’i tek kullanımlık yapmaz. Beyan edilen tip ve boyut aynı kaldığı sürece ikinci bir PUT, işleme fonksiyonunuz nesneyi aldıktan sonra üzerine yazabilir. Geçerlilik süresini kısa tutun. Yazma işlemini beklediğiniz byte’lara imzalı bir ChecksumSHA256 ile bağlayın ya da IfNoneMatch: '*' ile yalnızca-oluşturma hâline getirin; böylece tekrarlanan PUT 412 ile reddedilir. Karantina prefix’ini devrede tutun ve upload’ın bittiğini S3 event’inden öğrenin.
Kaynaklar#
- Presigned URL ile nesne yükleme - Amazon S3 (yeni sekmede açılır) - İstemcilerin Lambda üzerinden geçmeden doğrudan S3’e yüklemesi için presigned PUT URL oluşturma rehberi
- Cross-Origin Resource Sharing (CORS) - Amazon S3 (yeni sekmede açılır) - Tarayıcı tabanlı doğrudan yüklemelere izin vermek için S3 bucket’ta CORS yapılandırması
- Çok parçalı yükleme - Amazon S3 (yeni sekmede açılır) - 100 MB’dan büyük dosyalar için çok parçalı yükleme, parça boyutlandırma ve abort politikaları
- Lambda kotaları - AWS Lambda (yeni sekmede açılır) - Senkron çağrı payload boyutu, bellek aralığı ve maksimum çalışma süresi için resmi limitler
- Amazon API Gateway kotaları ve önemli notlar (yeni sekmede açılır) - Proxy tasarımını kısıtlayan REST API payload boyutu limiti ve integration timeout aralığı
- AWS Lambda fonksiyonları için en iyi uygulamalar (yeni sekmede açılır) - Handler dışında istemci başlatma, bellek boyutlandırma ve zaman aşımı yapılandırması
- AWS CDK v2 Geliştirici Rehberi (yeni sekmede açılır) - S3 bucket, Lambda ve API Gateway kaynaklarını CDK construct olarak tanımlamak için referans
- TypeScript ile Lambda fonksiyonu oluşturma (yeni sekmede açılır) - TypeScript’i transpile ederek Lambda’ya dağıtma ve paketleme yapılandırması
- Amazon S3 çok parçalı yükleme limitleri (yeni sekmede açılır) - Parça boyutu, maksimum nesne boyutu ve maksimum parça sayısını kapsayan resmi limit tablosu
İlgili yazılar
Dev, staging ve production ortamlarında Lambda Layer versiyonlarını AWS CDK ile yönetmek için pratik yaklaşımlar: otomatik pipeline ve rollback dahil.
aws · lambda · aws-cdk +4
AWS API Gateway ve CDK ile yol tabanlı API versiyonlama: versiyon başına yaşam döngüsü kaydı, ayrı Lambda handler'ları, keşif endpoint'i ve sunset sinyalleri.
api-gateway · aws · aws-cdk +3
Lambda API'si ile OpenAPI sözleşmesini senkron tutmak için spec'i Zod şemalarından üretin ve bu üretimi CDK dağıtımına bağlayın.
api-gateway · aws-cdk · lambda +4
İç servis katmanı kurmadan önce kurup kurmayacağınıza karar verin. Katmanın çağrı başına maliyeti, VPC Lattice'in kazandığı hacim ve direct invoke'un hâlâ kazandığı an.
aws · aws-cdk · lambda +4
Private REST API gRPC’yi yapısal olarak taşıyamaz ve gRPC konuşan her AWS yüzeyi Lambda hedeflerini dışlar. gRPC’den neyi tutmalı, neyi bırakmalı.
aws · aws-cdk · lambda +4