implement file downloads
This commit is contained in:
parent
f8a13f095b
commit
fb025fa4cc
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -143,7 +143,7 @@ dependencies = [
|
|||||||
"problem_details",
|
"problem_details",
|
||||||
"serde",
|
"serde",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-util",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@ -837,12 +837,14 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-util"
|
||||||
version = "0.1.17"
|
version = "0.7.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
|
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -8,7 +8,7 @@ repository = "https://git.nifni.eu/nif/casket"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8.1", features = ["multipart", "macros"] }
|
axum = { version = "0.8.1", features = ["multipart", "macros"] }
|
||||||
tokio = { version = "1.42.0", features = ["full"] }
|
tokio = { version = "1.42.0", features = ["full"] }
|
||||||
tokio-stream = "0.1.17"
|
tokio-util = { version = "0.7.13", features = ["io"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
figment = { version = "0.10.19", features = ["toml", "env"] }
|
figment = { version = "0.10.19", features = ["toml", "env"] }
|
||||||
|
|||||||
67
casket-backend/src/files/download.rs
Normal file
67
casket-backend/src/files/download.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
use crate::errors::to_internal_error;
|
||||||
|
use crate::extractor_helper::WithProblemDetails;
|
||||||
|
use crate::files::build_system_path;
|
||||||
|
use crate::AppState;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::extract::{Query, State};
|
||||||
|
use axum::http::{header, StatusCode};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use problem_details::ProblemDetails;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio_util::io::ReaderStream;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
pub async fn handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
axum::extract::Path(user_id): axum::extract::Path<String>,
|
||||||
|
WithProblemDetails(Query(query)): WithProblemDetails<Query<SingleFileQuery>>,
|
||||||
|
) -> Result<impl IntoResponse, ProblemDetails> {
|
||||||
|
let filesystem_path = build_system_path(&state.config.files.directory, &user_id, &query.path)?;
|
||||||
|
|
||||||
|
let file_name = get_file_name(&filesystem_path)?;
|
||||||
|
let file = open_file(&filesystem_path).await?;
|
||||||
|
|
||||||
|
let metadata = file.metadata().await.map_err(to_internal_error)?;
|
||||||
|
if !metadata.is_file() {
|
||||||
|
return Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
|
||||||
|
.with_detail(format!("Path {:?} is not a file", query.path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = ReaderStream::new(file);
|
||||||
|
let body = Body::from_stream(stream);
|
||||||
|
Ok((
|
||||||
|
[(
|
||||||
|
header::CONTENT_DISPOSITION,
|
||||||
|
format!("attachment; filename=\"{file_name}\""),
|
||||||
|
)],
|
||||||
|
body,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_file(filesystem_path: &Path) -> Result<File, ProblemDetails> {
|
||||||
|
File::open(filesystem_path).await.map_err(|err| {
|
||||||
|
ProblemDetails::from_status_code(StatusCode::NOT_FOUND)
|
||||||
|
.with_detail(format!("Failed to open file: {err}"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_file_name(filesystem_path: &PathBuf) -> Result<Cow<str>, ProblemDetails> {
|
||||||
|
filesystem_path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error!("Failed to get file name for {:?}", filesystem_path);
|
||||||
|
ProblemDetails::from_status_code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
})
|
||||||
|
.map(OsStr::to_string_lossy)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query to download a file
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SingleFileQuery {
|
||||||
|
/// Path which is used as base path to query the tree.
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
@ -3,10 +3,11 @@ use axum::http::StatusCode;
|
|||||||
use problem_details::ProblemDetails;
|
use problem_details::ProblemDetails;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
|
pub mod download;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod upload;
|
pub mod upload;
|
||||||
|
|
||||||
fn build_system_path(
|
pub fn build_system_path(
|
||||||
base_directory: &Path,
|
base_directory: &Path,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
user_path: &PathBuf,
|
user_path: &PathBuf,
|
||||||
@ -23,7 +24,7 @@ fn build_system_path(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sanitize_path(path: &Path) -> Result<PathBuf, ProblemDetails> {
|
fn sanitize_path(path: &Path) -> Result<PathBuf, ProblemDetails> {
|
||||||
let mut ret = PathBuf::new();
|
let mut ret = PathBuf::new();
|
||||||
for component in path.components().peekable() {
|
for component in path.components().peekable() {
|
||||||
match component {
|
match component {
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
use crate::files;
|
use crate::files;
|
||||||
|
use crate::files::download;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::routing::post;
|
use axum::routing::post;
|
||||||
use axum::{response::Html, routing::get, Router};
|
use axum::{response::Html, routing::get, Router};
|
||||||
use files::list::query_file_tree;
|
use files::list::query_file_tree;
|
||||||
|
|
||||||
pub fn routes() -> Router<AppState> {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new().route("/", get(handler)).route(
|
Router::new()
|
||||||
"/api/v1/user/{:user_id}/files",
|
.route("/", get(handler))
|
||||||
post(files::upload::handle_file_uploads).get(query_file_tree),
|
.route(
|
||||||
)
|
"/api/v1/user/{:user_id}/files",
|
||||||
|
post(files::upload::handle_file_uploads).get(query_file_tree),
|
||||||
|
)
|
||||||
|
.route("/api/v1/user/{:user_id}/download", get(download::handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handler() -> Html<&'static str> {
|
async fn handler() -> Html<&'static str> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user