Lewati ke konten

Object Storage

Static serving bawaan membaca dari filesystem lokal, jadi router.static() saja tidak bisa menjangkau bucket di S3, Cloudflare R2, atau Google Cloud Storage. Jembatannya adalah opsi staticHandler, sebuah hook yang mempertahankan route static yang familiar sambil menukar pembacaan berkas dengan fetch ke object storage. Route tetap terdaftar lewat router.static(), dan handler menjawab tiap request dari bucket alih-alih dari disk.

Kenapa Hook dan Bukan Path

Opsi path pada static serving memetakan prefix URL ke folder yang bisa diresolusi Deno.stat dan Deno.realPath, yang merupakan kontrak disk lokal. Object storage tidak punya path nyata di disk, jadi pemeriksaan traversal yang aman dan streaming lewat file handle tidak berlaku. Hook staticHandler menyerahkan seluruh langkah serve, jadi bucket menjadi sumber kebenaran sementara permukaan route tetap sama.

Menyajikan Dari Bucket

Sebagian besar object store mengekspos endpoint HTTPS per objek, jadi fetch ke ${endpoint}/${key} menarik byte-nya. Handler memotong prefix URL dari ctx.pathname untuk memulihkan kunci objek, lalu mengalirkan body response langsung lewat ctx.send.stream:

typescript
import { 
Router
, type
Context
, type ServeOptions } from '@neabyte/deserve'
// Endpoint dasar bucket const
endpoint
= 'https://my-bucket.s3.amazonaws.com'
const
router
= new
Router
({
routesDir
: 'routes',
staticHandler
: {
// Sajikan tiap objek dari bucket async
serve
(
ctx
:
Context
,
options
: ServeOptions,
urlPath
: string) {
// Pulihkan kunci objek dari path const
key
=
ctx
.
pathname
.
slice
(
urlPath
.
length
).
replace
(/^\//, '')
const
object
= await
fetch
(`${
endpoint
}/${
key
}`)
if (!
object
.
ok
|| !
object
.
body
) {
return
ctx
.
handleError
(404, new Deno.
errors
.
NotFound
('Object not found'))
} // Alirkan body bucket ke klien const
contentType
=
object
.
headers
.
get
('content-type') ?? 'application/octet-stream'
return
ctx
.
send
.
stream
(
object
.
body
,
undefined
,
contentType
)
} } }) // Daftarkan route yang dipenuhi handler
router
.
static
(
'/assets', {
path
: 's3'
} ) await
router
.
serve
(8000)

Nilai path tetap harus diset pada router.static() karena wajib, namun handler mengabaikannya di sini karena bucket menggantikan folder lokal. Request ke /assets/logo.png menjadi fetch untuk kunci logo.png.

Meneruskan Byte Range

Static serving menjawab byte range sendiri, tapi handler kustom kini memegang tugas itu. Meneruskan header Range yang masuk ke bucket membiarkan store mengembalikan konten parsial, dan meneruskan kembali status serta header range menjaga penggeser video atau unduhan yang bisa dilanjutkan tetap bekerja:

typescript
async function 
serve
(
ctx
:
Context
,
options
: ServeOptions,
urlPath
: string) {
const
key
=
ctx
.
pathname
.
slice
(
urlPath
.
length
).
replace
(/^\//, '')
const
range
=
ctx
.
header
('range')
// Teruskan header Range bila ada const
object
= await
fetch
(`${
endpoint
}/${
key
}`, {
headers
:
range
? {
Range
:
range
} : {}
}) if (!
object
.
ok
|| !
object
.
body
) {
return
ctx
.
handleError
(404, new Deno.
errors
.
NotFound
('Object not found'))
} // Cerminkan header range ke klien const
contentType
=
object
.
headers
.
get
('content-type') ?? 'application/octet-stream'
const
contentRange
=
object
.
headers
.
get
('content-range')
if (
contentRange
) {
ctx
.
setHeader
('Content-Range',
contentRange
)
ctx
.
setHeader
('Accept-Ranges', 'bytes')
} return
ctx
.
send
.
custom
(
object
.
body
, {
status
:
object
.
status
,
headers
: {
'Content-Type':
contentType
} }) }

Sebuah 206 Partial Content dari bucket mengalir balik tanpa berubah, karena ctx.send.custom mempertahankan status yang dipilih bucket.

Memakai Route Handler Sebagai Ganti

Hook staticHandler mencakup satu prefix URL utuh, yang cocok untuk folder aset publik. Satu unduhan di balik auth atau logika bisnis lebih cocok dengan route handler biasa, tempat middleware berjalan lebih dulu dan kunci datang dari route param:

typescript
// routes/files/[key].ts
export async function 
GET
(
ctx
:
Context
):
Promise
<Response> {
const
key
=
ctx
.
param
('key')
const
object
= await
fetch
(`${
endpoint
}/${
key
}`)
if (!
object
.
ok
|| !
object
.
body
) {
return
ctx
.
handleError
(404, new Deno.
errors
.
NotFound
('Object not found'))
} // Alirkan objek langsung apa adanya const
contentType
=
object
.
headers
.
get
('content-type') ?? 'application/octet-stream'
return
ctx
.
send
.
stream
(
object
.
body
,
undefined
,
contentType
)
}

Jalur ini menjalankan rantai middleware penuh, jadi menjaganya dengan basic auth atau pemeriksaan session terjadi sebelum bucket disentuh sama sekali.

Menandatangani Request

Bucket privat butuh request yang ditandatangani, bukan fetch polos. Dua jalur cocok:

  • URL presigned - SDK menandatangani URL berumur pendek, dan handler bisa mengalihkan dengan ctx.redirect atau mengambilnya sisi server.
  • SDK sisi server - klien resmi menandatangani tiap request, misalnya AWS SDK for JavaScript untuk S3 atau binding Cloudflare R2 untuk Workers.

Jalur mana pun yang menandatangani request, body response tetap mengalir lewat ctx.send.stream, jadi bentuk penyajiannya tetap sama.

Menangani Kegagalan

Object storage menambah panggilan jaringan yang bisa timeout atau ditolak, jadi tiap fetch meneruskan kegagalannya ke penanganan error terpusat alih-alih membocorkan error mentah. Objek yang hilang dipetakan ke 404, sementara gangguan upstream dipetakan ke 502 agar penyebabnya tetap terbaca:

typescript
export async function 
GET
(
ctx
:
Context
):
Promise
<Response> {
const
key
=
ctx
.
param
('key')
try { const
object
= await
fetch
(`${
endpoint
}/${
key
}`)
if (
object
.
status
=== 404) {
return await
ctx
.
handleError
(404, new Deno.
errors
.
NotFound
('Object not found'))
} // Petakan kegagalan upstream ke 502 if (!
object
.
ok
|| !
object
.
body
) {
return await
ctx
.
handleError
(502, new
Error
('Object storage unavailable'))
} return
ctx
.
send
.
stream
(
object
.
body
,
undefined
, 'application/octet-stream')
} catch (
error
) {
// Rutekan tiap gangguan jaringan ke error handling return await
ctx
.
handleError
(502,
error
as Error)
} }

Membentuk ini menjadi satu response klien tinggal di Penanganan Error, dan menangkapnya untuk log tinggal di Pelaporan Error.

Dirilis di bawah Lisensi MIT.