Skip to content

Commit 4aa51fa

Browse files
committed
feat: (WIP) range requests
1 parent 43df594 commit 4aa51fa

File tree

3 files changed

+105
-8
lines changed

3 files changed

+105
-8
lines changed

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ candle-nn = { git = "https://git.v0l.io/huggingface/candle.git", tag = "0.8.1",
5252
candle-transformers = { git = "https://git.v0l.io/huggingface/candle.git", tag = "0.8.1", optional = true }
5353
sqlx-postgres = { version = "0.8.2", optional = true, features = ["chrono", "uuid"] }
5454
mime2ext = "0.1.53"
55+
http-range-header = "0.4.2"

src/routes/mod.rs

+97-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
use std::fs;
2-
use std::fs::File;
3-
use std::str::FromStr;
4-
51
use crate::db::{Database, FileUpload};
62
use crate::filesystem::FileStore;
73
pub use crate::routes::admin::admin_routes;
@@ -13,14 +9,23 @@ use crate::settings::Settings;
139
#[cfg(feature = "void-cat-redirects")]
1410
use crate::void_db::VoidCatDb;
1511
use anyhow::Error;
12+
use http_range_header::{
13+
parse_range_header, EndPosition, StartPosition, SyntacticallyCorrectRange,
14+
};
1615
use nostr::Event;
1716
use rocket::fs::NamedFile;
1817
use rocket::http::{ContentType, Header, Status};
1918
#[cfg(feature = "void-cat-redirects")]
2019
use rocket::response::Redirect;
2120
use rocket::response::Responder;
2221
use rocket::serde::Serialize;
23-
use rocket::{Request, State};
22+
use rocket::{Request, Response, State};
23+
use std::io::SeekFrom;
24+
use std::pin::{pin, Pin};
25+
use std::str::FromStr;
26+
use std::task::{Context, Poll};
27+
use tokio::fs::File;
28+
use tokio::io::{AsyncRead, AsyncSeek, ReadBuf};
2429

2530
#[cfg(feature = "blossom")]
2631
mod blossom;
@@ -95,9 +100,93 @@ impl Nip94Event {
95100
}
96101
}
97102

103+
struct RangeBody {
104+
pub file: File,
105+
pub file_size: u64,
106+
pub ranges: Vec<SyntacticallyCorrectRange>,
107+
108+
current_range_index: usize,
109+
current_offset: u64,
110+
}
111+
112+
impl AsyncRead for RangeBody {
113+
fn poll_read(
114+
mut self: Pin<&mut Self>,
115+
cx: &mut Context<'_>,
116+
buf: &mut ReadBuf<'_>,
117+
) -> Poll<std::io::Result<()>> {
118+
if self.current_range_index >= self.ranges.len() {
119+
return Poll::Ready(Ok(()));
120+
}
121+
122+
let current_range = &self.ranges[self.current_range_index];
123+
let start_pos = match current_range.start {
124+
StartPosition::Index(i) => i,
125+
StartPosition::FromLast(i) => self.file_size - i,
126+
};
127+
let end_pos = match current_range.end {
128+
EndPosition::Index(i) => i,
129+
EndPosition::LastByte => self.file_size,
130+
};
131+
let range_start = start_pos + self.current_offset;
132+
let range_len = end_pos - range_start;
133+
let bytes_to_read = buf.remaining().min(range_len as usize) as u64;
134+
135+
if bytes_to_read == 0 {
136+
self.current_offset = 0;
137+
self.current_range_index += 1;
138+
return self.poll_read(cx, buf);
139+
}
140+
141+
let pinned = pin!(&mut self.file);
142+
pinned.start_seek(SeekFrom::Start(range_start))?;
143+
144+
let pinned = pin!(&mut self.file);
145+
match pinned.poll_complete(cx) {
146+
Poll::Ready(Ok(_)) => {}
147+
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
148+
Poll::Pending => return Poll::Pending,
149+
}
150+
151+
// Read data from the file
152+
let pinned = pin!(&mut self.file);
153+
let n = pinned.poll_read(cx, &mut buf.take(bytes_to_read as usize));
154+
if let Poll::Ready(Ok(())) = n {
155+
self.current_offset += bytes_to_read;
156+
Poll::Ready(Ok(()))
157+
} else {
158+
Poll::Pending
159+
}
160+
}
161+
}
162+
98163
impl<'r> Responder<'r, 'static> for FilePayload {
99164
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'static> {
100-
let mut response = self.file.respond_to(request)?;
165+
let mut response = Response::new();
166+
167+
// handle ranges
168+
#[cfg(feature = "ranges")]
169+
{
170+
response.set_header(Header::new("accept-ranges", "bytes"));
171+
if let Some(r) = request.headers().get("range").next() {
172+
if let Ok(ranges) = parse_range_header(r) {
173+
let r_body = RangeBody {
174+
file_size: self.info.size, // TODO: handle filesize mismatch
175+
file: self.file,
176+
ranges: ranges.ranges,
177+
current_range_index: 0,
178+
current_offset: 0,
179+
};
180+
response.set_streamed_body(Box::pin(r_body));
181+
}
182+
} else {
183+
response.set_streamed_body(self.file);
184+
}
185+
}
186+
#[cfg(not(feature = "ranges"))]
187+
response.set_streamed_body(self.file);
188+
response.set_header(Header::new("content-length", self.info.size.to_string()));
189+
101190
if let Ok(ct) = ContentType::from_str(&self.info.mime_type) {
102191
response.set_header(ct);
103192
}
@@ -145,7 +234,7 @@ async fn delete_file(
145234
if let Err(e) = db.delete_file(&id).await {
146235
return Err(Error::msg(format!("Failed to delete (fs): {}", e)));
147236
}
148-
if let Err(e) = fs::remove_file(fs.get(&id)) {
237+
if let Err(e) = tokio::fs::remove_file(fs.get(&id)).await {
149238
return Err(Error::msg(format!("Failed to delete (fs): {}", e)));
150239
}
151240
}
@@ -189,7 +278,7 @@ pub async fn get_blob(
189278
return Err(Status::NotFound);
190279
}
191280
if let Ok(Some(info)) = db.get_file(&id).await {
192-
if let Ok(f) = File::open(fs.get(&id)) {
281+
if let Ok(f) = File::open(fs.get(&id)).await {
193282
return Ok(FilePayload { file: f, info });
194283
}
195284
}

0 commit comments

Comments
 (0)