From 8d62143bd46bcf60011a386f198655dfc8e9c225 Mon Sep 17 00:00:00 2001 From: Georg Hopp Date: Sat, 4 Jan 2025 15:22:28 +0100 Subject: [PATCH] Add partial content support --- src/client.rs | 55 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/src/client.rs b/src/client.rs index 14a3978..c138907 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,12 +1,12 @@ -use std::{path::Path, time::Duration}; +use std::{io::ErrorKind, path::Path, time::Duration}; use anyhow::anyhow; use futures_util::StreamExt as _; -use http::{header::{CONTENT_TYPE, RANGE}, uri::{Authority, Scheme}, Request, Response, Uri, request::Builder as RequestBuilder}; +use http::{header::{CONTENT_TYPE, RANGE}, request::Builder as RequestBuilder, uri::{Authority, Scheme}, Request, Response, StatusCode, Uri}; use http_body_util::BodyDataStream; use m3u8_rs::{MediaPlaylist, MediaSegment, Playlist}; use reqwest::{redirect::Policy, Body}; -use tokio::{fs::File, io::AsyncWriteExt as _, time::timeout}; +use tokio::{fs::{symlink_metadata, File}, io::AsyncWriteExt as _, time::timeout}; use tower::{ServiceBuilder, ServiceExt as _}; use tower_http_client::{client::BodyReader, ServiceExt as _}; use tower_reqwest::HttpClientLayer; @@ -96,7 +96,22 @@ impl State { pub(super) async fn get_m3u8_segment(&mut self, uri: &Uri) -> Result<(), DownloadError> { - let mut response = self.request(uri, 0).await + // I consider a missing path as fatal... there is absolutely nothing we can do about it + // and we need all files from the playlist. + let filename = Path::new(uri.path()) + . file_name() + . expect("no filename in path_and_query"); + let metadata = match symlink_metadata(filename).await { + Ok(metadata) => Some(metadata), + Err(error) => match error.kind() { + ErrorKind::PermissionDenied => panic!("Permission denied on: {:?}", filename), + _ => None, + } + }; + + let mut response = self.request( uri + , metadata.map_or(0, |m| m.len()) ) + . await . map_err(|e| DownloadError::new(uri.clone(), Some(e)))?; // We always need the content-type to be able to decide @@ -110,14 +125,23 @@ impl State { , Some(anyhow!(message)) )); } - // I consider a missing path as fatal... there is absolutely nothing we can do about it - // and we need all files from the playlist. - let path = uri.path(); - let filename = Path::new(path) - . file_name() - . expect("no filename in path_and_query"); - let mut file = File::create(filename).await - . expect("can not create file for writing"); + let mut file = match response.status() { + StatusCode::PARTIAL_CONTENT => + // Here we assume that this response only comes if the requested + // range was fullfillable and thus is the data range in the + // response. Thats why I do not check the content-range header. + // If that assumption does not hold this needs to be fixec. + File::options() + . create(true) + . append(true) + . open(filename) + . await + . expect("can not create file for writing"), + + _ => + File::create(filename).await + . expect("can not create file for writing"), + }; // read body into file as stream let mut body_stream = BodyDataStream::new(response.body_mut()); @@ -133,6 +157,9 @@ impl State { file.write_all(data.as_ref()).await . map_err(|e| DownloadError::new(uri.clone(), Some(e.into())) )?; + file.flush().await + . map_err(|e| + DownloadError::new(uri.clone(), Some(e.into())) )?; }, } }; @@ -179,11 +206,11 @@ impl State { Ok(filename.to_string()) } - async fn request(&mut self, uri: &Uri, from: usize) -> anyhow::Result> + async fn request(&mut self, uri: &Uri, from: u64) -> anyhow::Result> { let request = RequestBuilder::new() . uri(uri) - . header(RANGE, format!("{}-", from)) + . header(RANGE, format!("bytes={}-", from)) . body(Body::default())?; log!(Level::Debug, "{:?}", request);