Files
Oxicloud/src/interfaces/range_requests.rs
T
Claude 82ee7da0d2 perf: serve ranges from RAM cache, stream ZIPs, overlap ingest settle, O(1) chunk gate
Round 2 of benchmark-gated optimizations (benches/ROUND2.md; every change
gated by a before/after in examples/bench_round2.rs — an AFTER that did
not beat its BEFORE was to be rolled back; none needed it):

- Range requests (REST/DAV/shares) answered from the moka content cache
  for sub-10MB files: PG resolve + open/seek/read -> Bytes::slice.
  256KiB seeks: 1,730/s -> 3.7M/s (p50 552us -> 0.15us).
- Streaming folder/share ZIPs via tokio duplex: TTFB no longer scales
  with archive size (326ms -> 0.4ms on 192MiB corpus; total also faster).
  Content-Length dropped (size unknown up front).
- NC chunked-upload per-PUT gate: O(k) directory scan+stat -> in-RAM
  per-session counter (lazy rebuild on cold start). 1,000-chunk upload
  gate cost: 33.1s -> 0.09s cumulative.
- Delta download + commit-verify now use the CDC path's
  buffered(read_prefetch) read-ahead: 64-chunk drain at 5ms open
  latency 440ms -> 51ms; order preserved.
- CDC ingest settles batches on a spawned task (depth-1 pipeline) so
  the source stream keeps flowing during PG pin + backend writes;
  rollback ledger shared + lock-serialized so compensation stays exact
  on cancellation. 512MiB paced ingest: 60-69 -> 74-75 MB/s.
  OXICLOUD_INGEST_OVERLAP=0 restores inline settling (ops/bench hatch).
- Frontend: instant-upload BLAKE3 hashing moved off the main thread to
  a bounded Web Worker pool (File handles by reference); vitest gate
  asserts the pool beats sequential (first gate draft posting buffers
  was 2.6x slower and was rewritten — copies dominated).

Validation: cargo fmt + clippy -D warnings clean; 514 unit + 544
integration tests green; 270 frontend tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBK1RdtzyP6759Muqe1K1w
2026-07-16 16:50:07 +00:00

106 lines
4.1 KiB
Rust

//! Conditional-request (`If-None-Match` → 304) and `Range` (→ 206/416)
//! helpers shared by the native WebDAV and NextCloud GET handlers.
//!
//! Mount-style WebDAV clients (rclone, davfs2, Finder, video players)
//! read by ranges; without 206 support every seek or resume transfers
//! the whole file. The REST download handler keeps its own richer
//! variant in `file_handler.rs` (Content-Disposition, compression,
//! permission-scoped streams) but follows the same header semantics.
use axum::body::Body;
use axum::http::{HeaderMap, Response, StatusCode, header};
use http_range_header::parse_range_header;
use std::sync::Arc;
use crate::application::dtos::file_dto::FileDto;
use crate::application::ports::file_ports::RangeContent;
use crate::application::services::file_retrieval_service::FileRetrievalService;
/// `If-None-Match` short-circuit: returns a `304 Not Modified` response
/// when the client already holds the current representation. `etag` must
/// be the quoted form the GET would emit (same comparison as the REST
/// download endpoint: exact match or `*`).
pub fn not_modified_response(headers: &HeaderMap, etag: &str) -> Option<Response<Body>> {
let client_etag = headers.get(header::IF_NONE_MATCH)?.to_str().ok()?;
if client_etag == etag || client_etag == "*" {
return Some(
Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header(header::ETAG, etag)
.body(Body::empty())
.unwrap(),
);
}
None
}
/// `Range` short-circuit for a streaming download.
///
/// Returns `Some(206)` with the requested byte range, `Some(416)` when
/// the range cannot be satisfied, or `None` when no (parseable) range
/// was requested or the range stream could not be created — callers
/// fall through to the full-body response, mirroring the REST handler.
///
/// The caller has already resolved access to the file (path-resolver /
/// ownership), so this uses the unscoped range stream — same contract
/// as the `get_file_stream` call in the surrounding handlers.
pub async fn range_response(
headers: &HeaderMap,
file: &FileDto,
etag: &str,
retrieval: &Arc<FileRetrievalService>,
) -> Option<Response<Body>> {
let range_str = headers.get(header::RANGE)?.to_str().ok()?;
let ranges = parse_range_header(range_str).ok()?;
let valid_ranges = match ranges.validate(file.size) {
Ok(v) => v,
Err(_) => {
return Some(
Response::builder()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(header::CONTENT_RANGE, format!("bytes */{}", file.size))
.body(Body::empty())
.unwrap(),
);
}
};
let range = valid_ranges.first()?;
let start = *range.start();
let end = *range.end();
let range_length = end - start + 1;
// Cache-aware: sub-threshold files already in the RAM content cache are
// answered with a zero-copy Bytes slice — no PG, no disk (benches/RANGE-CACHE.md).
match retrieval
.get_file_range_preloaded(file, start, Some(end + 1))
.await
{
Ok(content) => {
let body = match content {
RangeContent::Bytes(b) => Body::from(b),
RangeContent::Stream(s) => Body::from_stream(Box::into_pin(s)),
};
Some(
Response::builder()
.status(StatusCode::PARTIAL_CONTENT)
.header(header::CONTENT_TYPE, &*file.mime_type)
.header(header::CONTENT_LENGTH, range_length)
.header(
header::CONTENT_RANGE,
format!("bytes {}-{}/{}", start, end, file.size),
)
.header(header::ACCEPT_RANGES, "bytes")
.header(header::ETAG, etag)
.body(body)
.unwrap(),
)
}
Err(err) => {
tracing::error!("Error creating range stream: {}", err);
None // fall through to the full download
}
}
}