Skip to content

Commit 44cd973

Browse files
committed
feat: add folder upload support and error handler
1 parent 0a54e77 commit 44cd973

8 files changed

Lines changed: 472 additions & 137 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ Transfer files between two devices via web browser
77

88
```shell
99
# Linux
10-
wget -c https://github.com/debugdoctor/FileFlow/releases/download/v0.1.1/FileFlow-linux-x86_64 -O FileFlow
10+
wget -c https://github.com/debugdoctor/FileFlow/releases/download/v0.2.0/FileFlow-linux-x86_64 -O FileFlow
1111

1212
# Windows
13-
wget -c https://github.com/debugdoctor/FileFlow/releases/download/v0.1.1/FileFlow-windows-x86_64.exe -O FileFlow.exe
13+
wget -c https://github.com/debugdoctor/FileFlow/releases/download/v0.2.0/FileFlow-windows-x86_64.exe -O FileFlow.exe
1414
```
1515

1616
### Usage

server/src/dao/db.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct MetaInfo {
1111
pub block_size: u32,
1212
pub file_name: String,
1313
pub file_size: u64,
14+
pub done: bool,
1415
}
1516

1617
impl MetaInfo {
@@ -29,6 +30,7 @@ impl MetaInfo {
2930
block_size: 1024 * 1024,
3031
file_name: file_name,
3132
file_size: file_size,
33+
done: false,
3234
}
3335
}
3436
}

server/src/dao/memdb.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{collections::HashMap, sync::Arc, time::Instant};
22
use tokio::sync::RwLock;
33
use tokio::time::Duration;
4-
use tracing::{event, instrument, Level};
4+
use tracing::{event, Level};
55

66
pub struct MemDB<T> {
77
pub store: Arc<RwLock<HashMap<String, CacheEntry<T>>>>,

server/src/router.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,26 @@ use tower_http::timeout::TimeoutLayer;
44
use tracing::{event, instrument, Level};
55
use std::time::Duration;
66

7-
use crate::service::handler::{download, get_assets, get_file, get_id, get_status, upload, upload_file};
7+
use crate::service::handler::{download, get_assets, get_file, get_id, get_status, upload, upload_file, done};
88
use tower_http::services::ServeDir;
99

1010
fn api_router() -> Router {
1111
Router::new()
12-
.route("/hello", get(|| async {
12+
.route("/hello", get(|| async {
1313
// Changed from DEBUG to TRACE to reduce log verbosity
1414
event!(Level::TRACE, "Hello endpoint accessed");
15-
"Hi!"
15+
"Hi!"
1616
}))
1717
.route("/get_id", get(get_id))
1818
.route("/{id}/status", get(get_status))
1919
// Add timeout layer specifically for upload api
2020
.route("/{id}/upload", post(upload_file))
21-
.layer(TimeoutLayer::new(Duration::from_secs(20)))
21+
.layer(TimeoutLayer::new(Duration::from_secs(20)))
2222
// Add timeout layer specifically for download api
2323
.route("/{id}/file", get(get_file))
24-
.layer(TimeoutLayer::new(Duration::from_secs(20)))
24+
.layer(TimeoutLayer::new(Duration::from_secs(20)))
25+
.route("/{id}/done", put(done))
26+
.layer(TimeoutLayer::new(Duration::from_secs(20)))
2527
}
2628

2729
fn assets_router() -> Router {

server/src/service/handler.rs

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ use serde::Deserialize;
1313
use serde_json::json;
1414
use tracing::{event, instrument, Level};
1515

16-
const MAX_BLOCK_SIZE: u64 = 1024 * 1024; // 1MB
17-
const MAX_BLOCKS_PER_FILE: usize = 8;
16+
/// Maximum size of each file block in bytes (1MB)
17+
const MAX_BLOCK_SIZE: u64 = 1024 * 1024;
18+
/// Maximum number of blocks allowed per file (increased to support larger zip files)
19+
const MAX_BLOCKS_PER_FILE: usize = 16;
20+
/// Maximum number of retry attempts for database operations
1821
const MAX_RETRIES: u32 = 5;
19-
const RETRY_INTERVAL: u64 = 250; // milliseconds
22+
/// Interval between retry attempts in milliseconds
23+
const RETRY_INTERVAL: u64 = 250;
2024

21-
// dto
25+
/// Data transfer object for file information
2226
#[derive(Debug, Deserialize)]
2327
struct FileInfo {
2428
pub filename: String,
@@ -27,6 +31,8 @@ struct FileInfo {
2731
pub total: u64,
2832
}
2933

34+
/// Handler for serving the upload page
35+
/// Returns the upload HTML page or 404 if not found
3036
#[instrument]
3137
pub async fn upload() -> impl IntoResponse {
3238
match StaticFiles::get("upload/index.html") {
@@ -41,6 +47,8 @@ pub async fn upload() -> impl IntoResponse {
4147
}
4248
}
4349

50+
/// Handler for serving the download page
51+
/// Returns the download HTML page or 404 if not found
4452
pub async fn download() -> impl IntoResponse {
4553
match StaticFiles::get("download/index.html") {
4654
Some(content) => {
@@ -54,6 +62,10 @@ pub async fn download() -> impl IntoResponse {
5462
}
5563
}
5664

65+
/// Handler for generating a unique file ID
66+
/// Accepts file name and size as query parameters
67+
/// Returns a unique ID that can be used for file transfer
68+
#[instrument]
5769
pub async fn get_id(
5870
Query(query): Query<HashMap<String, String>>
5971
) -> impl IntoResponse {
@@ -96,6 +108,9 @@ pub async fn get_id(
96108
.into_response()
97109
}
98110

111+
/// Handler for checking the status of a file transfer
112+
/// Returns file metadata and transfer status
113+
#[instrument]
99114
pub async fn get_status(Path(id): Path<String>) -> impl IntoResponse {
100115
// Changed from DEBUG to TRACE to reduce log verbosity
101116
event!(Level::TRACE, "Checking status for ID: {}", id);
@@ -112,6 +127,7 @@ pub async fn get_status(Path(id): Path<String>) -> impl IntoResponse {
112127
"file_name": meta_info.value.file_name,
113128
"file_size": meta_info.value.file_size,
114129
"is_using": meta_info.value.is_using,
130+
"done": meta_info.value.done,
115131
}
116132

117133
}))
@@ -127,38 +143,41 @@ pub async fn get_status(Path(id): Path<String>) -> impl IntoResponse {
127143
}
128144
}
129145

146+
/// Handler for downloading file chunks
147+
/// Supports range requests for chunked file transfer
148+
/// Includes retry logic and atomic operations for concurrent access
130149
#[instrument(skip_all)]
131150
pub async fn get_file(
132151
Path(id): Path<String>,
133152
Query(query): Query<HashMap<String, String>>,
134-
) -> Result<impl IntoResponse, (StatusCode, String)> {
153+
) -> impl IntoResponse {
135154
let receive_id = match query.get("rid") {
136155
Some(receive_id) => receive_id.to_string(),
137156
None => {
138157
event!(Level::WARN, "Missing Parameter: rid");
139-
return Ok((
158+
return (
140159
StatusCode::BAD_REQUEST,
141160
Json(json!({
142161
"code": 400,
143162
"success": false,
144163
"message": "Missing Parameter: rid"
145164
})))
146-
.into_response());
165+
.into_response();
147166
}
148167
};
149168

150169
let start = match query.get("start") {
151170
Some(start) => start.parse::<u64>().unwrap(),
152171
None => {
153172
event!(Level::WARN, "Missing Parameter: start");
154-
return Ok((
173+
return (
155174
StatusCode::BAD_REQUEST,
156175
Json(json!({
157176
"code": 400,
158177
"success": false,
159178
"message": "Missing Parameter: start"
160179
})))
161-
.into_response());
180+
.into_response();
162181
}
163182
};
164183

@@ -176,14 +195,14 @@ pub async fn get_file(
176195
// Check if already in use
177196
if current_meta.value.is_using {
178197
event!(Level::WARN, "File already in use for ID: {}", id);
179-
return Ok((
198+
return (
180199
StatusCode::BAD_REQUEST,
181200
Json(json!({
182201
"code": 400,
183202
"success": false,
184203
"message": "Bad Request"
185204
})))
186-
.into_response());
205+
.into_response();
187206
}
188207

189208
// Atomically update the metadata
@@ -202,22 +221,22 @@ pub async fn get_file(
202221
retries += 1;
203222
if retries >= MAX_RETRIES {
204223
event!(Level::ERROR, "Failed to update metadata after {} retries for ID: {}", MAX_RETRIES, id);
205-
return Ok((
224+
return (
206225
StatusCode::INTERNAL_SERVER_ERROR,
207226
Json(json!({
208227
"code": 500,
209228
"success": false,
210229
"message": "Internal Server Error"
211230
})))
212-
.into_response());
231+
.into_response();
213232
}
214233
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
215234
}
216235
}
217236
}
218237
None => {
219238
event!(Level::WARN, "Access ID Not Found: {}", id);
220-
return Ok((StatusCode::NOT_FOUND, "Access ID Not Found").into_response());
239+
return (StatusCode::NOT_FOUND, "Access ID Not Found").into_response();
221240
}
222241
}
223242
}
@@ -230,18 +249,18 @@ pub async fn get_file(
230249
Some(meta_info) => {
231250
if meta_info.value.used_by != receive_id {
232251
event!(Level::WARN, "Wrong Receive ID for ID: {}", id);
233-
return Ok((
252+
return (
234253
StatusCode::BAD_REQUEST,
235254
Json(json!({
236255
"code": 400,
237256
"success": false,
238257
"message": "Wrong Receive ID"
239-
}))).into_response());
258+
}))).into_response();
240259
}
241260
},
242261
None => {
243262
event!(Level::WARN, "Access ID Not Found during verification: {}", id);
244-
return Ok((StatusCode::NOT_FOUND, "Access ID Not Found").into_response());
263+
return (StatusCode::NOT_FOUND, "Access ID Not Found").into_response();
245264
}
246265
};
247266

@@ -253,14 +272,14 @@ pub async fn get_file(
253272
Some(file_block) => {
254273
if file_block.value.start > start {
255274
event!(Level::WARN, "Wrong start position for ID: {} and start: {}", id.clone(), start);
256-
return Ok((
275+
return (
257276
StatusCode::BAD_REQUEST,
258277
Json(json!({
259278
"code": 400,
260279
"success": false,
261280
"message": "Wrong start position"
262281
})))
263-
.into_response());
282+
.into_response();
264283
}
265284
// Changed from DEBUG to TRACE to reduce log verbosity
266285
event!(Level::TRACE, "Retrieved block for ID: {} and start: {}", id.clone(), start);
@@ -269,7 +288,7 @@ pub async fn get_file(
269288
None => {
270289
if retries >= MAX_RETRIES {
271290
event!(Level::ERROR, "Block {}:{:012} Not Found after {} retries", &id, start, MAX_RETRIES);
272-
return Ok((StatusCode::NOT_FOUND, format!("Block {}:{:012} Not Found", &id, start)).into_response());
291+
return (StatusCode::NOT_FOUND, format!("Block {}:{:012} Not Found", &id, start)).into_response();
273292
}
274293
retries += 1;
275294
tokio::time::sleep(tokio::time::Duration::from_millis(RETRY_INTERVAL)).await;
@@ -304,14 +323,18 @@ pub async fn get_file(
304323
// Changed from DEBUG to TRACE to reduce log verbosity
305324
event!(Level::TRACE, "Sending file block for ID: {} range: {}-{}", id, block_start, block_end);
306325

307-
Ok((
326+
(
308327
StatusCode::PARTIAL_CONTENT,
309328
AppendHeaders(headers),
310329
Body::from(block_data)
311-
).into_response())
330+
).into_response()
312331
}
313332

314333

334+
/// Handler for uploading file chunks
335+
/// Processes multipart form data with file info and chunk data
336+
/// Includes validation for block size and file limits
337+
#[instrument]
315338
pub async fn upload_file(Path(id): Path<String>, multipart: Multipart) -> impl IntoResponse {
316339
// Changed from INFO to DEBUG to reduce log verbosity for large files
317340
event!(Level::DEBUG, "Starting file upload for ID: {}", id);
@@ -518,7 +541,7 @@ pub async fn upload_file(Path(id): Path<String>, multipart: Multipart) -> impl I
518541
return Json(json!({
519542
"code": 400,
520543
"success": false,
521-
"message": "Maximum number of blocks per file reached (8)"
544+
"message": format!("Maximum number of blocks per file reached ({})", MAX_BLOCKS_PER_FILE)
522545
}))
523546
.into_response();
524547
}
@@ -559,8 +582,48 @@ pub async fn upload_file(Path(id): Path<String>, multipart: Multipart) -> impl I
559582
.into_response()
560583
}
561584

585+
/// Handler for marking file download as complete
586+
/// Updates the metadata to indicate successful download
587+
#[instrument]
588+
pub async fn done(Path(id): Path<String>, Json(_payload): Json<serde_json::Value>) -> impl IntoResponse {
589+
// Mark download as complete for the given ID
590+
match MetaInfo::get_db().get(&id).await {
591+
Some(mut meta_info) => {
592+
meta_info.value.done = true;
593+
match MetaInfo::get_db().update(&id, meta_info.value, meta_info.exp).await {
594+
Ok(_) => {
595+
event!(Level::DEBUG, "Download marked as complete for ID: {}", id);
596+
Json(json!({
597+
"code": 200,
598+
"success": true,
599+
"message": "Download completion marked successfully"
600+
}))
601+
},
602+
Err(e) => {
603+
event!(Level::ERROR, "Failed to update download completion status: {}", e);
604+
Json(json!({
605+
"code": 500,
606+
"success": false,
607+
"message": "Internal Server Error"
608+
}))
609+
}
610+
}
611+
},
612+
None => {
613+
event!(Level::WARN, "ID not found for download completion: {}", id);
614+
Json(json!({
615+
"code": 404,
616+
"success": false,
617+
"message": "Not Found"
618+
}))
619+
}
620+
}
621+
}
622+
623+
/// Handler for serving static assets
624+
/// Returns CSS, JS, and other static files with appropriate MIME types
562625
#[instrument(skip_all)]
563-
pub async fn get_assets(Path(file): Path<String>) -> impl IntoResponse {
626+
pub async fn get_assets(Path(file): Path<String>) -> impl IntoResponse {
564627
match StaticFiles::get(format!("assets/{}", file).as_str()) {
565628
Some(f) => {
566629
let mime = mime_guess::from_path(&file).first_or_octet_stream();

server/src/service/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
pub mod handler;
22
pub mod static_files;
3-
mod entity;

0 commit comments

Comments
 (0)