webdav_server/api/
assets.rs1use crate::{
2 Error::ApiError,
3 api::Error::{BadRequest, InvalidHeader, NotFound},
4 app::State as AppState,
5 error, info,
6 logger::Module,
7 model::asset::{Asset, AssetMetadataRow, AssetTag},
8 storage::Payload,
9};
10use axum::{
11 Extension, Json,
12 body::Body,
13 extract::{Path, Query, State},
14 response::IntoResponse,
15};
16use hyper::{HeaderMap, StatusCode};
17use serde::{Deserialize, Serialize};
18use tokio_util::io::ReaderStream;
19
20use crate::Result;
21
22pub async fn delete(
32 State(state): State<AppState>,
33 Extension(user_id): Extension<i64>,
34 Path(hash_str): Path<String>,
35) -> Result<impl IntoResponse> {
36 let hash_bytes = hex::decode(&hash_str)
37 .map_err(|_| BadRequest("Invalid Hash Format".into()))?;
38
39 Asset::delete(&state.db, user_id, &hash_bytes).await?;
40
41 state.storage.delete_blob(&hash_str).await?;
42
43 info!(
44 Module::Asset,
45 "deleted ownership over blob \"{hash_str}\" with success"
46 );
47 Ok(StatusCode::NO_CONTENT)
48}
49
50pub async fn get(
63 State(state): State<AppState>,
64 Extension(user_id): Extension<i64>,
65 Path(hash_str): Path<String>,
66) -> Result<impl IntoResponse> {
67 let hash_bytes = hex::decode(&hash_str)
68 .map_err(|_| BadRequest("Invalid Hash Format".into()))?;
69
70 let owns_file = Asset::owned_by(&state.db, user_id, &hash_bytes).await?;
71
72 if !owns_file {
73 error!(
74 Module::Asset,
75 "Attempt to access unauthorized blob '{hash_str}' by user {user_id}"
76 );
77 return Err(ApiError(NotFound(
78 "Asset not found or Unauthorized".into(),
79 )));
80 }
81
82 let file = state
83 .storage
84 .get_blob(&hash_str)
85 .await
86 .map_err(|_| NotFound("File Missing".into()))?;
87
88 let stream = ReaderStream::new(file);
89 let body = Body::from_stream(stream);
90
91 let mut headers = HeaderMap::new();
92 headers.insert(
93 "Content-Type",
94 "application/octet-stream"
95 .parse()
96 .map_err(|e| ApiError(InvalidHeader(e)))?,
97 );
98
99 Ok((StatusCode::OK, headers, body))
100}
101
102#[derive(Serialize)]
103#[serde(untagged)]
104enum FileStatus {
105 Success { file_name: String, hash: String },
106 Failure { file_name: String, error: String },
107}
108
109impl FileStatus {
110 const fn success(file_name: String, hash: String) -> Self {
111 Self::Success { file_name, hash }
112 }
113
114 fn failure(file_name: String, error: &impl ToString) -> Self {
115 Self::Failure {
116 file_name,
117 error: error.to_string(),
118 }
119 }
120}
121
122pub async fn upload(
133 State(state): State<AppState>,
134 Path(tag_str): Path<String>,
135 headers: HeaderMap,
136 Extension(user_id): Extension<i64>,
137 body: Body,
138) -> crate::Result<impl IntoResponse> {
139 let tag = AssetTag::try_from(tag_str.as_str())
140 .map_err(|()| BadRequest("Invalid Tag".into()))?;
141
142 let file_name = headers
143 .get("X-File-Name")
144 .and_then(|v| v.to_str().ok())
145 .ok_or_else(|| BadRequest("Missing X-File-Name header".into()))?;
146
147 let expected_size: u64 = headers
148 .get("Content-Length")
149 .and_then(|x| x.to_str().ok())
150 .and_then(|x| x.parse().ok())
151 .ok_or_else(|| BadRequest("Missing content length in header".into()))?;
152
153 if expected_size > 10_000_000_000 {
154 return Err(BadRequest("Payload too Large".into()).into());
155 }
156
157 let f_stream = body.into_data_stream();
158
159 let status = match state
160 .storage
161 .try_save(file_name, Payload::new(expected_size, f_stream))
162 .await
163 {
164 Ok(metadata) => {
165 match Asset::create(&state.db, user_id, tag, &metadata).await {
166 Ok(()) => {
167 info!(
168 Module::Asset,
169 "upload success, user owns {}",
170 metadata.hash.to_string()
171 );
172 FileStatus::success(
173 file_name.into(),
174 metadata.hash.to_string(),
175 )
176 }
177 Err(e) => {
178 info!(
179 Module::Asset,
180 "user {user_id} failed to register asset ownership to database '{file_name}': {e}"
181 );
182 FileStatus::failure(file_name.into(), &e)
183 }
184 }
185 }
186
187 Err(e) => {
188 info!(
189 Module::Asset,
190 "user {user_id} failed to uplaod '{file_name}'. Failed to write this blob to disk with error {e}"
191 );
192 FileStatus::failure(file_name.into(), &e)
193 }
194 };
195
196 Ok(Json(status))
197}
198
199#[derive(Deserialize)]
200pub struct ListQuery {
201 pub tag: Option<i16>,
202}
203
204#[derive(Serialize)]
205pub struct ListResponse {
206 pub assets: Vec<AssetMetadataRow>,
207}
208
209pub async fn list(
210 State(state): State<AppState>,
211 Extension(user_id): Extension<i64>,
212 Query(query): Query<ListQuery>,
213) -> Result<impl IntoResponse> {
214 let assets = Asset::list(&state.db, user_id, query.tag).await?;
215
216 Ok(Json(ListResponse { assets }))
217}