From 021dc593827793375c13fee45585abf0dffed7c3 Mon Sep 17 00:00:00 2001 From: kieran Date: Mon, 16 Dec 2024 10:39:28 +0000 Subject: [PATCH] feat: range requests --- Cargo.toml | 8 +- index.html | 184 ++++++++++++++++++++++++++++++++++++++++++++++ src/routes/mod.rs | 99 ++++++++++++++++++------- 3 files changed, 261 insertions(+), 30 deletions(-) create mode 100644 index.html diff --git a/Cargo.toml b/Cargo.toml index 7b5f865..939d7c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ path = "src/bin/main.rs" name = "route96" [features] -default = ["nip96", "blossom", "analytics"] +default = ["nip96", "blossom", "analytics", "ranges"] media-compression = ["dep:ffmpeg-rs-raw", "dep:libc"] labels = ["nip96", "dep:candle-core", "dep:candle-nn", "dep:candle-transformers"] nip96 = ["media-compression"] @@ -24,6 +24,7 @@ bin-void-cat-migrate = ["dep:sqlx-postgres"] torrent-v2 = [] analytics = [] void-cat-redirects = ["dep:sqlx-postgres"] +ranges = ["dep:http-range-header"] [dependencies] log = "0.4.21" @@ -44,6 +45,7 @@ url = "2.5.0" serde_with = { version = "3.8.1", features = ["hex"] } reqwest = "0.12.8" clap = { version = "4.5.18", features = ["derive"] } +mime2ext = "0.1.53" libc = { version = "0.2.153", optional = true } ffmpeg-rs-raw = { git = "https://git.v0l.io/Kieran/ffmpeg-rs-raw.git", rev = "b358b3e4209da827e021d979c7d35876594d0285", optional = true } @@ -51,5 +53,5 @@ candle-core = { git = "https://git.v0l.io/huggingface/candle.git", tag = "0.8.1" candle-nn = { git = "https://git.v0l.io/huggingface/candle.git", tag = "0.8.1", optional = true } candle-transformers = { git = "https://git.v0l.io/huggingface/candle.git", tag = "0.8.1", optional = true } sqlx-postgres = { version = "0.8.2", optional = true, features = ["chrono", "uuid"] } -mime2ext = "0.1.53" -http-range-header = "0.4.2" +http-range-header = { version = "0.4.2", optional = true } + diff --git a/index.html b/index.html new file mode 100644 index 0000000..abab362 --- /dev/null +++ b/index.html @@ -0,0 +1,184 @@ + + + + + route96 + + + + +

+ Welcome to route96 +

+
+
+ + +
+
+ You must have a nostr extension for this to work +
+ +
+ + +
+
+ +
+ +
+ +
+
+

+
+
\ No newline at end of file
diff --git a/src/routes/mod.rs b/src/routes/mod.rs
index 250ec82..e351be5 100644
--- a/src/routes/mod.rs
+++ b/src/routes/mod.rs
@@ -12,6 +12,7 @@ use anyhow::Error;
 use http_range_header::{
     parse_range_header, EndPosition, StartPosition, SyntacticallyCorrectRange,
 };
+use log::{debug, warn};
 use nostr::Event;
 use rocket::fs::NamedFile;
 use rocket::http::{ContentType, Header, Status};
@@ -100,13 +101,27 @@ impl Nip94Event {
     }
 }
 
+/// Range request handler over file handle
 struct RangeBody {
-    pub file: File,
-    pub file_size: u64,
-    pub ranges: Vec,
-
+    file: File,
+    file_size: u64,
+    ranges: Vec,
     current_range_index: usize,
     current_offset: u64,
+    poll_complete: bool,
+}
+
+impl RangeBody {
+    pub fn new(file: File, file_size: u64, ranges: Vec) -> Self {
+        Self {
+            file,
+            file_size,
+            ranges,
+            current_offset: 0,
+            current_range_index: 0,
+            poll_complete: false,
+        }
+    }
 }
 
 impl AsyncRead for RangeBody {
@@ -138,23 +153,32 @@ impl AsyncRead for RangeBody {
             return self.poll_read(cx, buf);
         }
 
-        let pinned = pin!(&mut self.file);
-        pinned.start_seek(SeekFrom::Start(range_start))?;
+        if !self.poll_complete {
+            // start seeking to our read position
+            let pinned = pin!(&mut self.file);
+            pinned.start_seek(SeekFrom::Start(range_start))?;
+            self.poll_complete = true;
+        }
 
-        let pinned = pin!(&mut self.file);
-        match pinned.poll_complete(cx) {
-            Poll::Ready(Ok(_)) => {}
-            Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
-            Poll::Pending => return Poll::Pending,
+        if self.poll_complete {
+            let pinned = pin!(&mut self.file);
+            match pinned.poll_complete(cx) {
+                Poll::Ready(Ok(_)) => {
+                    self.poll_complete = false;
+                }
+                Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
+                Poll::Pending => return Poll::Pending,
+            }
         }
 
         // Read data from the file
         let pinned = pin!(&mut self.file);
-        let n = pinned.poll_read(cx, &mut buf.take(bytes_to_read as usize));
+        let n = pinned.poll_read(cx, buf);
         if let Poll::Ready(Ok(())) = n {
             self.current_offset += bytes_to_read;
             Poll::Ready(Ok(()))
         } else {
+            self.poll_complete = true;
             Poll::Pending
         }
     }
@@ -170,30 +194,51 @@ impl<'r> Responder<'r, 'static> for FilePayload {
             response.set_header(Header::new("accept-ranges", "bytes"));
             if let Some(r) = request.headers().get("range").next() {
                 if let Ok(ranges) = parse_range_header(r) {
-                    let r_body = RangeBody {
-                        file_size: self.info.size, // TODO: handle filesize mismatch
-                        file: self.file,
-                        ranges: ranges.ranges,
-                        current_range_index: 0,
-                        current_offset: 0,
-                    };
-                    response.set_streamed_body(Box::pin(r_body));
+                    if ranges.ranges.len() > 1 {
+                        warn!("Multipart ranges are not supported, fallback to non-range request");
+                        response.set_streamed_body(self.file);
+                    } else {
+                        let single_range = ranges.ranges.first().unwrap();
+                        let range_start = match single_range.start {
+                            StartPosition::Index(i) => i,
+                            StartPosition::FromLast(i) => self.info.size - i,
+                        };
+                        let range_end = match single_range.end {
+                            EndPosition::Index(i) => i,
+                            EndPosition::LastByte => self.info.size,
+                        };
+                        debug!("Range: {:?} {:?}", range_start..range_end, single_range);
+                        let r_len = range_end - range_start;
+                        let r_body = RangeBody::new(self.file, self.info.size, ranges.ranges);
+
+                        response.set_status(Status::PartialContent);
+                        response.set_header(Header::new("content-length", r_len.to_string()));
+                        response.set_header(Header::new(
+                            "content-range",
+                            format!("bytes {}-{}/{}", range_start, range_end, self.info.size),
+                        ));
+                        response.set_streamed_body(Box::pin(r_body));
+                    }
                 }
             } else {
                 response.set_streamed_body(self.file);
             }
         }
         #[cfg(not(feature = "ranges"))]
-        response.set_streamed_body(self.file);
-        response.set_header(Header::new("content-length", self.info.size.to_string()));
+        {
+            response.set_streamed_body(self.file);
+            response.set_header(Header::new("content-length", self.info.size.to_string()));
+        }
 
         if let Ok(ct) = ContentType::from_str(&self.info.mime_type) {
             response.set_header(ct);
         }
-        response.set_header(Header::new(
-            "content-disposition",
-            format!("inline; filename=\"{}\"", self.info.name),
-        ));
+        if !self.info.name.is_empty() {
+            response.set_header(Header::new(
+                "content-disposition",
+                format!("inline; filename=\"{}\"", self.info.name),
+            ));
+        }
         Ok(response)
     }
 }
@@ -247,7 +292,7 @@ async fn delete_file(
 #[rocket::get("/")]
 pub async fn root() -> Result {
     #[cfg(debug_assertions)]
-    let index = "./ui_src/dist/index.html";
+    let index = "./index.html";
     #[cfg(not(debug_assertions))]
     let index = "./ui/index.html";
     if let Ok(f) = NamedFile::open(index).await {