Daravelv1.25.0
ENID GitHub

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#

  • copy dan move memakai server-side copy dari S3, yang menerima objek hingga 5 GB.
  • Kredensial dibaca dari .env (atau AWS_SESSION_TOKEN untuk 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