OSW — Object Storage Wrapper
On this page
- Sekilas
- Yang dikerjakannya
- A. Membagikan tautan bertanda tangan, bukan membawa file
- B. Memberi tiap file siklus hidup
- C. Menyalin antar-bucket di sisi server
- D. Mengembalikan banyak tautan sekaligus
- E. Menjaga satu kontrak bagi setiap pemanggil
- Antarmuka
- Standar
- Di dalamnya
- Keputusan rancangan
- Spesifikasi
- Terkait
- Istilah
OSW — Object Storage Wrapper — adalah service HTTP kecil, ditulis dalam Rust, yang berdiri di antara aplikasi dan object storage yang kompatibel dengan S3. Aplikasi tak pernah memegang kredensial storage dan tak pernah mengalirkan byte file lewat server-nya sendiri: aplikasi meminta tautan bertanda tangan berumur pendek kepada OSW, browser meng-upload atau men-download langsung ke storage, dan OSW mengelola nasib file sesudahnya — meng-commit-nya, menghapusnya secara lunak, memulihkannya, atau menyalinnya antar-bucket.
Saya membangunnya sekali lalu memakainya ulang di berbagai project, termasuk backend situs ini, karena semuanya butuh beberapa kemampuan yang sama dari file storage, dan membuat kemampuan-kemampuan itu benar ternyata lebih sulit daripada kelihatannya. Berbicara langsung dengan storage menyebarkan kredensial ke setiap aplikasi, membuat setiap file menempuh jalan memutar lewat server aplikasi, dan membiarkan tiap aplikasi menciptakan ulang siklus hidup yang dibutuhkan file.
Sekilas
| Bahasa & framework | Rust, dengan actix-web dan client S3 murni Rust di atas rustls |
|---|---|
| Arsitektur | Clean Architecture |
| Versi | 1.1.0 |
| Status | Stabil, sedang dipakai |
| Bentuk | Service HTTP mandiri di belakang API gateway, dipakai ulang lintas project |
| Kode sumber | Privat |
Yang dikerjakannya
A. Membagikan tautan bertanda tangan, bukan membawa file
Presigned URL adalah tautan sementara yang ditandatangani dengan kredensial storage, yang mengizinkan satu upload atau download tertentu tanpa menyerahkan kredensialnya. OSW menerbitkannya untuk upload, download, atau keduanya, dan menamai setiap objek baru sendiri, jadi browser berbicara langsung dengan storage dan OSW tak pernah membawa satu byte pun dari file.
B. Memberi tiap file siklus hidup
File yang di-upload selagi user mengisi formulir belum menjadi bagian dari apa pun, dan file yang dihapus biasanya perlu bisa dipulihkan untuk sementara waktu. OSW memodelkan keduanya sebagai tempat pada path, di dalam bucket yang sama: upload baru mendarat di area sementara, commit memindahkannya ke path permanennya, soft delete memindahkannya ke area terhapus, dan restore memindahkannya kembali. Upload yang ditinggalkan disapu oleh storage server sendiri, lewat lifecycle rule yang mengkedaluwarsakan area sementara.
C. Menyalin antar-bucket di sisi server
Aplikasi bisa memindahkan file dari bucket privat ke bucket publik — misalnya ketika lampiran satu artikel ikut menjadi publik bersama artikelnya — atau menyalinnya di dalam satu bucket, sekaligus membuang sumbernya bila diminta. Penyalinan terjadi di dalam storage server; tak ada byte yang melewati OSW maupun aplikasi.
D. Mengembalikan banyak tautan sekaligus
Halaman yang mendaftar banyak file bisa meminta seluruh tautan download-nya dalam satu request, bila perlu dengan memeriksa dulu bahwa tiap file memang ada, lalu menerima satu baris per path yang diminta, dalam urutan yang sama.
E. Menjaga satu kontrak bagi setiap pemanggil
Setiap response, berhasil maupun gagal, datang dalam amplop JSON yang sama beserta request id, setiap kesalahan input dilaporkan sekaligus, dan pesan yang sudah dibaca aplikasi tetap sama kata per kata seperti dulu.
Antarmuka
| Kemampuan | Kegunaan |
|---|---|
| Presign | tautan upload dan/atau download bertanda tangan untuk objek baru, dengan nama yang dibangkitkan |
| Commit | memindahkan objek yang sudah di-upload ke path permanennya |
| Exists | apakah satu objek ada, beserta jenis, ukuran, tanggal, dan tag-nya |
| Download | tautan download bertanda tangan untuk satu objek |
| Download batch | tautan download bertanda tangan untuk banyak objek dalam satu request |
| Delete | menghapus objek, secara lunak bila tak ditentukan lain |
| Restore | mengembalikan objek yang terhapus lunak ke path asalnya |
| Copy | menyalin objek ke bucket atau key lain, bisa sekaligus membuang sumbernya |
| Liveness | untuk platform container |
OSW bisa dipasang di bawah route mana pun milik gateway. Request batch-lah yang membawa body JSON:
Standar
| Standar | Yang dijaminnya di sini |
|---|---|
| AWS Signature Version 4, autentikasi lewat query | tautan bertanda tangan yang diterima storage dan kedaluwarsa tepat waktu |
S3 API: HeadObject, CopyObject dengan x-amz-copy-source, DeleteObject | pemeriksaan keberadaan, copy di sisi server, dan penghapusan di server apa pun yang kompatibel S3 |
| Addressing path-style dan virtual-host | berjalan dengan MinIO di belakang proxy maupun dengan S3 ter-hosting |
| UUIDv7 | nama objek dan request id yang tak pernah bertabrakan dan terurut menurut waktu |
| RFC 3339 | waktu kedaluwarsa tautan yang bisa dibaca client mana pun |
| W3C Trace Context dan OpenTelemetry (OTLP, semantic conventions) | trace, log, dan metrics yang bersambung lintas service |
| OpenAPI 3.0.3 | kontrak yang terbaca mesin untuk setiap endpoint |
| TLS lewat rustls | koneksi terenkripsi tanpa OpenSSL di jalur kodenya |
Di dalamnya
OSW mengikuti Clean Architecture: aturan storage ada di domain, use case dikelompokkan per resource, dan S3, HTTP, konfigurasi, serta telemetry masing-masing berada di balik port atau terkurung di lapisannya sendiri. Saya membangunnya ulang ke bentuk itu lapis demi lapis dari versi sebelumnya, dalam sembilan fase, dan fase terakhir membuktikan spesifikasi API yang dipublikasikan identik byte demi byte dengan yang lama, jadi tak ada aplikasi yang perlu berubah. Jam dan pembangkit id juga berupa port, sehingga setiap timestamp dan nama di dalam test bisa diramalkan, dan ke-98 test-nya berjalan tanpa storage server karena tiap use case menerima storage in-memory yang mencatat setiap panggilan.
Keputusan rancangan
Hanya OSW yang memegang key, dan tautan ditandatangani untuk alamat yang akan dipakai browser. Tak ada aplikasi yang pernah melihat kredensial storage. Signature mencakup nama host, dan host yang dipakai OSW di dalam jaringan biasanya bukan host yang bisa dijangkau browser, jadi tautan ditandatangani terhadap public origin yang akan dipakai browser. Public origin diperiksa saat service menyala, jadi nilai yang keliru gagal saat boot, bukan pada upload pertama. Handle untuk public origin dibangun sekali per bucket saat start, karena membangunnya memuat trust store sistem — sekitar 305 mikrodetik kerja disk yang memblokir — sementara tanda tangannya sendiri hanya sekitar 14, dan batch berisi 100 akan membayar ongkos pertama itu 100 kali. Content type upload ditaruh di dalam query yang ditandatangani, bukan di header bertanda tangan, jadi browser tak perlu mengulangnya persis tapi juga tak bisa mengubahnya, dan addressing path-style ikut ke handle publik supaya tautan tetap benar di belakang proxy. Objek baru dinamai {direktori}/{uuidv7}.{ekstensi} oleh OSW sendiri, jadi nama tak pernah bertabrakan, terurut menurut waktu, dan tak bisa dipilih atau ditimpa client; ekstensinya diambil dari tabel content type dengan cadangan yang masuk akal. Flag permission hanya menerbitkan tautan yang dibutuhkan pemanggil — upload, download, atau keduanya — dan umur tautan bisa diatur, dengan waktu kedaluwarsa ikut dikembalikan supaya client tahu kapan harus meminta lagi.
Status file terlihat di path-nya, dan setiap perubahan memeriksa dulu. Upload bersifat sementara kecuali pemanggil menyatakan lain. Commit dan restore adalah copy di dalam storage server yang disusul penghapusan, jadi tak ada byte yang berpindah sebesar apa pun file-nya. Path divalidasi ketat dan objeknya dicari sebelum perubahan apa pun, jadi file yang hilang menjadi 404 yang bersih, bukan error storage. Penghapusan bersifat lunak secara bawaan dan bisa dibatalkan. OSW tak menyimpan state dan tak menjalankan penyapu: upload yang ditinggalkan adalah urusan storage server, lewat lifecycle rule-nya.
Nilai bawaan aman bila tak merugikan, dan sengaja ditiadakan bila akan menyembunyikan kesalahan. Bucket yang boleh disentuh OSW persis bucket yang dikonfigurasikan untuknya, dan ia menolak menyala bila tak ada satu pun. Bucket tak dikenal adalah kesalahan pemanggil, 400, bukan kesalahan server. Request satu-file jatuh ke bucket bawaan, tetapi copy wajib menyebut kedua bucket, karena jatuh diam-diam akan menyembunyikan kesalahan pemanggil.
Copy antar-bucket adalah copy di sisi server yang sungguhan, dan tak pernah menghilangkan sumbernya. Client S3 yang dipakai OSW hanya bisa menyalin di dalam satu bucket, jadi copy antar-bucket menandatangani PUT ke bucket tujuan dengan header x-amz-copy-source yang ikut ditandatangani, lalu mengirimnya lewat satu client HTTP yang dipakai ulang. Tautan internal itu tak pernah keluar dari proses. Header-nya dikirim sekaligus ditandatangani, kalau tidak signature-nya tak akan cocok; key sumber di-encode per segmen, dengan spasi sebagai %20 dan tak pernah +, yang akan dibaca storage sebagai tanda plus. Copy yang menjawab 200 tapi membawa error di body-nya dihitung gagal, karena S3 bisa melaporkan kegagalan copy yang terlambat dengan cara itu. Sumber tak pernah dihapus bila copy gagal; bila copy berhasil tapi penghapusan sumbernya tidak, jawabannya tetap 200 dengan source_deleted: false — tim backend yang meminta endpoint ini menginginkannya, supaya client tak mengulang copy yang sudah terjadi. Key tujuan secara bawaan sama dengan key sumber, copy di dalam satu bucket diizinkan, dan setiap pesan validasi menyebut sisi mana yang dimaksud. TLS-nya hanya rustls, dengan tipe HTTP dipatok ke versi yang diharapkan client S3.
Batch gagal hanya di tempat yang memang harus gagal. Endpoint batch ditambahkan di samping endpoint satu-file sesudah konsumen menyiasatinya dengan menembakkan satu request per file sekaligus, yang hanya memindahkan bebannya ke OSW. Persoalan yang mengenai setiap baris — bucket tak dikenal, path terlalu banyak, daftar kosong — menggagalkan seluruh request; persoalan pada satu baris hanya menggagalkan baris itu, dan setiap tautan lain tetap dikembalikan, dalam urutan saat diminta, dengan path ganda dipantulkan sekali per kemunculannya:
Setiap baris membawa empat key yang sama dengan null eksplisit, berisi tautan atau error dan tak pernah keduanya, dan memakai kosakata kecil milik domain: not_found, invalid_path, dan unavailable, yang terakhir sengaja dipisahkan supaya gangguan storage yang sesaat dicoba ulang, bukan dianggap file yang hilang. Perlu tidaknya tiap file diperiksa dulu tak punya nilai bawaan, karena memeriksa menelan satu request storage per file dan melewatinya menghilangkan sinyal "tidak ada". Pemeriksaan berjalan dengan konkurensi yang dibatasi, batas yang dipisahkan dari batas jumlah path karena keduanya ada dengan alasan berbeda, jadi satu pemanggil tak bisa menyingkirkan pemanggil lain. Satu waktu kedaluwarsa dihitung untuk seluruh batch, error tiap baris dicatat di tempat terjadinya, dan tiap baris memakai ulang aturan path satu-file.
Satu kontrak, dijaga persis. Setiap response dibungkus satu amplop, yang menggantikan selusin helper yang nyaris kembar. Batas body JSON diatur dengan sengaja, dan JSON yang rusak tetap menerima amplopnya, bukan error teks polos bawaan framework. Validasi mengumpulkan setiap persoalan sebelum menjawab. Error use case berupa tipe yang dipetakan lapisan HTTP ke status code, menggantikan trik lama yang membaca awalan string error, dan 500 hanya menyebut operasi mana yang gagal, dengan penyebabnya di log. Body 401 adalah satu-satunya pengecualian yang disengaja, dibiarkan dalam bentuk lamanya demi client yang sudah membacanya, begitu pula pesan-pesan yang di-parse client itu. Objek hilang yang dilaporkan client S3 dengan caranya sendiri yang tak konsisten tetap dikenali sebagai 404.
Identitas datang dari gateway, dan setiap request bisa ditelusuri. OSW berdiri di belakang API gateway yang mengautentikasi setiap pemanggil dan hanya meneruskan identitas yang sudah terverifikasi. Request id diterima bila pemanggil mengirimnya dan dibangkitkan bila tidak, lalu dipantulkan kembali. Telemetry beralih dari menulis langsung ke Elasticsearch menjadi OpenTelemetry lewat collector: trace yang masuk diteruskan, metrics memakai delta temporality karena exporter Elasticsearch milik collector membuang histogram dan counter bila tidak, metrics HTTP diberi label pola route, bukan path mentah, alamat client diambil dari header yang diteruskan hanya bila datang dari trusted proxy, key sensitif disamarkan di log, dan telemetry bisa dimatikan sepenuhnya, dengan flush saat shutdown ketika menyala. Konfigurasi dibaca sekali, saat start.
Ia dikirim kecil dan berjalan tanpa hak istimewa. Image-nya di-build dalam dua tahap, berjalan sebagai user non-root, dan membaca konfigurasinya dari file yang di-mount.
Spesifikasi
| Masukan | Request dari aplikasi di belakang API gateway, membawa identitas yang terverifikasi |
|---|---|
| Keluaran | Tautan bertanda tangan beserta waktu kedaluwarsanya, metadata objek, hasil siklus hidup dan copy, semuanya dalam satu amplop JSON |
| Storage | Server apa pun yang kompatibel S3, MinIO di deployment saya, dengan lifecycle rule yang mengkedaluwarsakan upload yang ditinggalkan |
| Observability | Trace, log, dan metrics OpenTelemetry lewat OTLP, atau standard output saja |
| Test | 98, tanpa storage server |
| Berjalan sebagai | Image container yang di-build dua tahap, sebagai user non-root, dengan konfigurasi di-mount dari file |
Terkait
Sebelum OSW dipasang di depan satu storage server, server itu diperiksa dengan script ini.
Membuktikan storage di belakang OSW sudah siap sebelum apa pun bergantung padanya.
OSW menjadi studi kasus artikel ini, tentang kode yang bisa diikuti pembaca berikutnya.
Template yang menyalakan object storage server bersama OSW untuk project baru.
Ke mana trace, log, dan metrics OSW pergi begitu meninggalkan service-nya.
Istilah
- Object storage
- Storage yang menyimpan file sebagai objek utuh di dalam bucket bernama, dicapai lewat HTTP.
- Presigned URL
- Tautan sementara yang ditandatangani dengan kredensial storage, yang mengizinkan satu upload atau download tertentu tanpa memberikan kredensial itu kepada client.
- Soft delete
- Memindahkan file ke area terhapus, bukan memusnahkannya, sehingga bisa dipulihkan.
- Lifecycle rule
- Aturan yang dipasang di storage server untuk mengkedaluwarsakan atau memindahkan objek secara otomatis, misalnya semua yang ada di bawah satu prefix sesudah satu hari.
- API gateway
- Service di depan service lain yang mengautentikasi pemanggil dan hanya meneruskan identitas yang sudah terverifikasi kepada mereka.
- Port
- Antarmuka yang ditetapkan lapisan dalam dan diimplementasikan lapisan luar, sehingga aturan tak pernah bergantung pada tool tertentu.
Technologies covered
- Rust
- MinIO
- S3