The fallback branch discarded range.start and streamed the file from byte zero while the response advertised the requested window, so a client asking for bytes 5-9 got bytes 0-4 and stored them at the wrong offset. readFileRangeFallback windows the stream and calls the native operation when there is one. Fixes #177 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
2.8 KiB
TypeScript
82 lines
2.8 KiB
TypeScript
import type { Context } from "@webnet/http/server"
|
|
import type { AsyncVFS } from "@webnet/vfs"
|
|
import { readFileRangeFallback } from "@webnet/vfs/fallback"
|
|
import type { DAVServerOptions } from "../types.js"
|
|
import { parseRange, formatHttpDate } from "../../common/utils.js"
|
|
|
|
export async function handleGet(
|
|
ctx: Context,
|
|
vfs: AsyncVFS,
|
|
vfsPath: string,
|
|
opts: DAVServerOptions = {},
|
|
): Promise<void> {
|
|
const stat = await vfs.stat(vfsPath)
|
|
if (stat.isDirectory) {
|
|
if (opts.onCollectionGet) {
|
|
await opts.onCollectionGet(ctx, vfs, vfsPath, stat)
|
|
} else {
|
|
ctx.res.setStatus(405, "Method Not Allowed")
|
|
ctx.res.setHeader("Content-Length", "0")
|
|
ctx.res.body = null
|
|
}
|
|
return
|
|
}
|
|
|
|
const range = parseRange(ctx.req.getHeader("range"))
|
|
|
|
if (range) {
|
|
const end = range.end ?? stat.size - 1n
|
|
if (range.start >= stat.size || end >= stat.size) {
|
|
ctx.res.setStatus(416, "Range Not Satisfiable")
|
|
ctx.res.setHeader("Content-Range", `bytes */${stat.size}`)
|
|
ctx.res.body = null
|
|
return
|
|
}
|
|
const length = end - range.start + 1n
|
|
const stream = await readFileRangeFallback(vfs, vfsPath, range.start, end)
|
|
|
|
ctx.res.setStatus(206, "Partial Content")
|
|
ctx.res.setHeader("Content-Range", `bytes ${range.start}-${end}/${stat.size}`)
|
|
ctx.res.setHeader("Content-Length", String(length))
|
|
if (stat.contentType) ctx.res.setHeader("Content-Type", stat.contentType)
|
|
if (stat.etag) ctx.res.setHeader("ETag", stat.etag)
|
|
if (stat.modifiedAt) ctx.res.setHeader("Last-Modified", formatHttpDate(stat.modifiedAt))
|
|
ctx.res.body = stream
|
|
return
|
|
}
|
|
|
|
const stream = await vfs.readFile(vfsPath)
|
|
ctx.res.setStatus(200, "OK")
|
|
ctx.res.setHeader("Content-Length", String(stat.size))
|
|
ctx.res.setHeader("Accept-Ranges", "bytes")
|
|
if (stat.contentType) ctx.res.setHeader("Content-Type", stat.contentType)
|
|
if (stat.etag) ctx.res.setHeader("ETag", stat.etag)
|
|
if (stat.modifiedAt) ctx.res.setHeader("Last-Modified", formatHttpDate(stat.modifiedAt))
|
|
ctx.res.body = stream
|
|
}
|
|
|
|
export async function handleHead(
|
|
ctx: Context,
|
|
vfs: AsyncVFS,
|
|
vfsPath: string,
|
|
opts: DAVServerOptions = {},
|
|
): Promise<void> {
|
|
const stat = await vfs.stat(vfsPath)
|
|
if (stat.isDirectory) {
|
|
if (opts.onCollectionGet) {
|
|
await opts.onCollectionGet(ctx, vfs, vfsPath, stat)
|
|
} else {
|
|
ctx.res.setStatus(405, "Method Not Allowed")
|
|
ctx.res.body = null
|
|
}
|
|
return
|
|
}
|
|
ctx.res.setStatus(200, "OK")
|
|
ctx.res.setHeader("Content-Length", String(stat.size))
|
|
ctx.res.setHeader("Accept-Ranges", "bytes")
|
|
if (stat.contentType) ctx.res.setHeader("Content-Type", stat.contentType)
|
|
if (stat.etag) ctx.res.setHeader("ETag", stat.etag)
|
|
if (stat.modifiedAt) ctx.res.setHeader("Last-Modified", formatHttpDate(stat.modifiedAt))
|
|
ctx.res.body = null
|
|
}
|