Penyimpanan S3
Container di Cloud Run, Fargate, Kubernetes, atau Fly.io kehilangan disk-nya
setiap kali redeploy, dan dua replika tidak pernah melihat file satu sama
lain. daravel_s3 memindahkan facade Storage ke sebuah bucket: AWS S3
atau penyimpanan apa pun yang kompatibel dengan S3 (Cloudflare R2, MinIO,
DigitalOcean Spaces, Backblaze B2, Wasabi, Google Cloud Storage lewat
interoperability API-nya). Request ditandatangani dengan AWS Signature
Version 4 dalam Dart murni, sehingga paket ini tidak menambah AWS SDK dan
tetap ter-compile ke satu binary AOT yang sama.
dart pub add daravel_s3
// bootstrap/app.dart
import 'package:daravel_s3/daravel_s3.dart';
Application createApp() => Application.configure(basePath: basePath)
.withS3()
// ...
.create();
Nama variabelnya sama dengan yang dibaca config/filesystems.php di
Laravel, jadi aplikasi Laravel dan Daravel bisa berbagi satu .env dan satu
bucket:
FILESYSTEM_DISK=s3 # semua pemanggilan Storage menuju bucket
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_DEFAULT_REGION=ap-southeast-1
AWS_BUCKET=my-app-files
AWS_URL=https://cdn.example.com # opsional: base URL publik (CDN)
AWS_VISIBILITY=private # atau public: objek mendapat x-amz-acl public-read
# AWS_SESSION_TOKEN=... # kredensial sementara
Dengan FILESYSTEM_DISK=s3, boot gagal selama key, secret, atau bucket
masih kosong. Tanpa variabel itu, bucket tetap tersedia sebagai
Storage.disk('s3') di samping local dan public. withS3(disk: 'media')
mendaftarkannya dengan nama lain.
Provider lain#
| Provider | AWS_ENDPOINT |
AWS_DEFAULT_REGION |
AWS_USE_PATH_STYLE_ENDPOINT |
|---|---|---|---|
| AWS S3 | (kosong) | region bucket | false |
| Cloudflare R2 | https://<account-id>.r2.cloudflarestorage.com |
auto |
false |
| MinIO | http://127.0.0.1:9000 |
us-east-1 |
true |
| DigitalOcean Spaces | https://<region>.digitaloceanspaces.com |
region-nya (sgp1) |
false |
| Google Cloud Storage | https://storage.googleapis.com |
auto |
true |
Pengalamatan path-style (endpoint/bucket/key) juga merupakan cara
menjangkau bucket yang namanya mengandung titik lewat HTTPS.
Memakai disk#
Semua method Disk bekerja pada bucket, dengan hasil yang sama seperti pada
disk lokal (paket ini menjalankan satu suite tes kontrak terhadap
LocalDisk, MemoryDisk, dan S3Disk):
await Storage.disk('s3').put('avatars/${user.id}.png', bytes);
final png = await Storage.disk('s3').getBytes('avatars/${user.id}.png');
await Storage.disk('s3').copy('a.pdf', 'archive/a.pdf'); // disalin di dalam S3
await Storage.disk('s3').delete(['old/1.txt', 'old/2.txt']); // satu request per 1000
final files = await Storage.disk('s3').allFiles('reports');
Objek disimpan dengan Content-Type sesuai ekstensinya, sehingga browser
yang membuka URL-nya langsung menampilkannya. Direktori adalah prefix key;
makeDirectory menulis objek kosong dir/ seperti yang ditulis Laravel,
dan kedua framework melihat pohon yang sama. append membaca objek lalu
menulisnya kembali, karena S3 tidak bisa menambahkan isi ke objek.
url(path) bernilai AWS_URL/key bila AWS_URL di-set, dan URL bucket itu
sendiri bila tidak. URL ini hanya bisa diakses bila objeknya publik (bucket
policy atau AWS_VISIBILITY=public); untuk objek privat gunakan temporary
URL.
Streaming file besar#
putStream menyimpan paling banyak satu part (8 MiB) di memori. Stream yang
muat dalam satu part dikirim sebagai satu PUT; stream yang lebih panjang
menjadi multipart upload, yang dibatalkan bila stream-nya gagal, sehingga
rekaman 2 GB tidak pernah menumpuk di RAM. File yang di-upload disimpan
dengan cara ini, langsung dari file sementaranya:
router.post('/recordings', (request) async {
final video = await request.file('video');
final path = await video!.store('recordings', disk: 's3');
return {'path': path};
});
readStream meneruskan unduhan ke response sambil datanya tiba:
return Response.stream(
Storage.disk('s3').readStream('recordings/$id.mp4'),
contentType: 'video/mp4',
);
Temporary URL#
Temporary URL (presigned URL) memungkinkan client mengunduh objek privat langsung dari bucket sampai URL itu kedaluwarsa (paling lama tujuh hari), tanpa byte-nya melewati server Anda:
final url = Storage.disk('s3').temporaryUrl(
'invoices/1029.pdf',
expiration: 30.minutes.fromNow,
responseContentDisposition: 'attachment; filename="invoice-1029.pdf"',
);
Waktu kedaluwarsanya mengikuti clock Daravel, sehingga Clock.fake() di
dalam tes ikut menetapkannya.
Upload langsung#
Untuk upload berukuran besar, client meminta presigned PUT ke server Anda
lalu mengirim file-nya langsung ke bucket:
router.post('/api/uploads', (request) async {
final path = 'materials/${Str.uuid()}.mp4';
final upload = Storage.disk('s3').temporaryUploadUrl(
path,
expiration: 15.minutes.fromNow,
contentType: 'video/mp4',
);
return {'path': path, ...upload.toJson()}; // method, url, headers
});
Client mengirim file dengan headers persis seperti yang dikembalikan:
// Flutter, package:http
await http.put(Uri.parse(json['url']), headers: json['headers'], body: bytes);
contentType dan contentLength termasuk dalam tanda tangan: S3 menolak
upload dengan tipe lain, atau dengan ukuran lain bila contentLength
diberikan. Presigned PUT tidak bisa membatasi ukuran, hanya menetapkan
ukuran yang tepat.
Membatasi ukuran: form POST#
Agar client bisa meng-upload sampai ukuran tertentu, berikan form
presigned POST (POST policy dari S3). Policy-nya ditandatangani, dan S3
memeriksa kondisinya saat upload: file yang melebihi maxSize, content
type yang tidak diawali contentTypePrefix, atau field yang diubah akan
ditolak.
router.post('/api/uploads', (request) async {
final path = 'materials/${Str.uuid()}.mp4';
final form = Storage.disk('s3').temporaryUploadForm(
path,
expiration: 15.minutes.fromNow,
maxSize: 500 * 1024 * 1024, // 500 MB
contentTypePrefix: 'video/', // atau contentType: 'video/mp4', persis
);
return {'path': path, ...form.toJson()}; // method, url, fields
});
Client mengirim fields apa adanya sebagai multipart/form-data ke url,
menambahkan Content-Type bila yang diberikan berupa prefix, dan menaruh
file-nya paling akhir, di field bernama file:
// Flutter, package:http
final upload = http.MultipartRequest('POST', Uri.parse(json['url']))
..fields.addAll({...json['fields'], 'Content-Type': 'video/mp4'})
..files.add(await http.MultipartFile.fromPath('file', video.path));
final response = await upload.send(); // 204 bila S3 sudah menyimpannya
Browser melakukan hal yang sama dengan
<form method="post" enctype="multipart/form-data"> atau FormData.
Testing#
Storage.fake('s3') mengganti bucket dengan disk in-memory, sama seperti
disk lainnya. temporaryUrl, temporaryUploadUrl, dan temporaryUploadForm
juga bekerja pada fake dan mengembalikan placeholder
(/storage/s3/<path>?expiration=<unix time>), sehingga route yang
membagikan URL bisa diuji tanpa jaringan:
final disk = Storage.fake('s3');
final response = (await client.post('/api/uploads')).assertOk();
final body = response.json! as Map<String, Object?>;
expect(body['url'], startsWith('/storage/s3/materials/'));
disk.assertMissing(body['path']! as String); // client yang meng-upload-nya
Terhadap server sungguhan, suite integrasi paket ini berjalan di RustFS
pada CI, atau di server apa pun yang kompatibel dengan S3:
DARAVEL_TEST_S3=http://127.0.0.1:9000 dart test --tags integration di
packages/daravel_s3.
Di jaringan#
Sebuah request boleh memakai timeout (5 menit) untuk mengirim body dan
menerima header response-nya, dan body response boleh diam selama itu;
lewat dari itu request dibatalkan atau body-nya diputus dengan
ConnectionException (isTimeout), dan socket-nya tidak dikembalikan ke
pool. Pemanggilan yang dijawab S3 dengan 500, 502, 503 (SlowDown)
atau 504, atau yang koneksinya gagal sebelum ada response, dikirim ulang
paling banyak retries (3) kali lagi setelah retryDelay (100 ms) yang
digandakan tiap kali dengan jitter, seperti SDK AWS; setiap pemanggilan
membawa seluruh body-nya, sehingga pengiriman ulang persis sama.
Batasan#
copydanmovememakai server-side copy dari S3, yang menerima objek hingga 5 GB.- Kredensial dibaca dari
.env(atauAWS_SESSION_TOKENuntuk kredensial sementara); instance role dan web identity token belum dibaca. - Setiap part multipart upload, kecuali yang terakhir, minimal 5 MiB;
S3Disk(..., partSize: ...)mengubah default 8 MiB.
Padanan Laravel#
Bagian lain dari Laravel yang ikut didukung, beserta API yang bisa dicari di pub.dev.
| Laravel | Daravel | API | Catatan |
|---|---|---|---|
Storage read-through driver |
ReadThroughDisk(primary: a, fallback: b) |
ReadThroughDisk |