multipart ranges

This commit is contained in:
JMARyA 2025-01-13 19:00:44 +01:00
parent 37f65b6353
commit a5ecf145a9
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
4 changed files with 75 additions and 26 deletions

View file

@ -1,6 +1,3 @@
use data_encoding::HEXUPPER;
use rand::RngCore;
pub mod csrf; pub mod csrf;
pub mod profile_pic; pub mod profile_pic;
mod session; mod session;
@ -13,14 +10,6 @@ pub use user::MaybeUser;
pub use user::User; pub use user::User;
pub use user::UserRole; pub use user::UserRole;
fn gen_token(token_length: usize) -> String {
let mut token_bytes = vec![0u8; token_length];
rand::thread_rng().fill_bytes(&mut token_bytes);
HEXUPPER.encode(&token_bytes)
}
/// A macro to check if a user has admin privileges. /// A macro to check if a user has admin privileges.
/// ///
/// This macro checks whether the provided user has admin privileges by calling the `is_admin` method on it. /// This macro checks whether the provided user has admin privileges by calling the `is_admin` method on it.

View file

@ -2,9 +2,9 @@ use chrono::Utc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::FromRow; use sqlx::FromRow;
use crate::get_pg; use crate::{gen_random, get_pg};
use super::{User, UserRole, gen_token}; use super::{User, UserRole};
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] #[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Session { pub struct Session {
@ -50,7 +50,7 @@ impl Sessions for User {
sqlx::query_as( sqlx::query_as(
"INSERT INTO user_session (token, \"user\", kind, name) VALUES ($1, $2, $3, $4) RETURNING *", "INSERT INTO user_session (token, \"user\", kind, name) VALUES ($1, $2, $3, $4) RETURNING *",
) )
.bind(gen_token(64)) .bind(gen_random(64))
.bind(&self.username) .bind(&self.username)
.bind(SessionKind::API) .bind(SessionKind::API)
.bind(name) .bind(name)
@ -108,7 +108,7 @@ impl Sessions for User {
sqlx::query_as( sqlx::query_as(
"INSERT INTO user_session (token, \"user\", kind) VALUES ($1, $2, $3) RETURNING *", "INSERT INTO user_session (token, \"user\", kind) VALUES ($1, $2, $3) RETURNING *",
) )
.bind(gen_token(64)) .bind(gen_random(64))
.bind(&self.username) .bind(&self.username)
.bind(SessionKind::USER) .bind(SessionKind::USER)
.fetch_one(get_pg!()) .fetch_one(get_pg!())

View file

@ -1,3 +1,5 @@
use data_encoding::HEXUPPER;
use rand::RngCore;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
pub mod auth; pub mod auth;
@ -41,3 +43,11 @@ macro_rules! get_pg {
} }
}; };
} }
pub fn gen_random(token_length: usize) -> String {
let mut token_bytes = vec![0u8; token_length];
rand::thread_rng().fill_bytes(&mut token_bytes);
HEXUPPER.encode(&token_bytes)
}

View file

@ -5,6 +5,8 @@ use rocket::http::Status;
use rocket::response::Responder; use rocket::response::Responder;
use std::io::Cursor; use std::io::Cursor;
use crate::gen_random;
// TODO: Implement file based response // TODO: Implement file based response
pub struct DataResponse { pub struct DataResponse {
@ -29,20 +31,62 @@ impl<'r> Responder<'r, 'static> for DataResponse {
fn respond_to(self, req: &'r Request<'_>) -> rocket::response::Result<'static> { fn respond_to(self, req: &'r Request<'_>) -> rocket::response::Result<'static> {
// Handle Range requests // Handle Range requests
if let Some(range) = req.headers().get_one("Range") { if let Some(range) = req.headers().get_one("Range") {
if let Some((start, end)) = parse_range_header(range, self.data.len()) { let ranges = range.split(",").collect::<Vec<_>>();
// TODO : Reject invalid ranges if ranges.len() == 1 {
// TODO : Multiple ranges? if let Some((start, end)) = parse_range_header(range, self.data.len()) {
let sliced_data = &self.data[start..=end];
return Ok(Response::build()
.header(Header::new(
"Content-Range",
format!("bytes {}-{}/{}", start, end, self.data.len()),
))
.header(Header::new("Accept-Ranges", "bytes"))
.header(Header::new("Content-Type", self.content_type.clone()))
.status(Status::PartialContent)
.streamed_body(Cursor::new(sliced_data.to_vec()))
.finalize());
}
} else {
let mut multipart_body: Vec<u8> = Vec::new();
let boundary = gen_random(32);
for range in ranges {
if let Some((start, end)) = parse_range_header(range, self.data.len()) {
let sliced_data = &self.data[start..=end];
let mut body: Vec<u8> = Vec::new();
body.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
body.extend_from_slice(
format!(
"Content-Range: bytes {}-{}/{}\r\n",
start,
end,
self.data.len()
)
.as_bytes(),
);
body.extend_from_slice(
format!("Content-Type: {}\r\n\r\n", self.content_type.clone())
.as_bytes(),
);
body.extend_from_slice(sliced_data);
body.extend_from_slice("\r\n".as_bytes());
multipart_body.extend_from_slice(&body);
}
}
multipart_body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
let sliced_data = &self.data[start..=end];
return Ok(Response::build() return Ok(Response::build()
.header(Header::new(
"Content-Range",
format!("bytes {}-{}/{}", start, end, self.data.len()),
))
.header(Header::new("Accept-Ranges", "bytes")) .header(Header::new("Accept-Ranges", "bytes"))
.header(Header::new("Content-Type", self.content_type.clone())) .header(Header::new(
"Content-Type",
format!("multipart/byteranges; boundary={boundary}"),
))
.status(Status::PartialContent) .status(Status::PartialContent)
.streamed_body(Cursor::new(sliced_data.to_vec())) .streamed_body(Cursor::new(multipart_body.to_vec()))
.finalize()); .finalize());
} }
} }
@ -69,10 +113,16 @@ fn parse_range_header(range: &str, total_len: usize) -> Option<(usize, usize)> {
} }
let range = &range[6..]; let range = &range[6..];
if range.starts_with('-') {
let neg: usize = range.trim_start_matches('-').parse().ok()?;
return Some((total_len - neg, total_len));
}
let parts: Vec<&str> = range.split('-').collect(); let parts: Vec<&str> = range.split('-').collect();
if parts.len() != 2 { if parts.len() != 2 {
return None; return Some((parts[0].parse().ok()?, total_len));
} }
let start = parts[0].parse::<usize>().ok(); let start = parts[0].parse::<usize>().ok();