style: cargo fmt --all
This commit is contained in:
@@ -157,8 +157,8 @@ impl UploadSession {
|
||||
/// Persist the full session metadata once (on create).
|
||||
async fn persist_metadata(&self) -> Result<(), String> {
|
||||
let path = self.temp_dir.join(SESSION_META_FILE);
|
||||
let json = serde_json::to_vec(self)
|
||||
.map_err(|e| format!("Failed to serialise session: {e}"))?;
|
||||
let json =
|
||||
serde_json::to_vec(self).map_err(|e| format!("Failed to serialise session: {e}"))?;
|
||||
// Atomic write: write to .tmp then rename
|
||||
let tmp = self.temp_dir.join("session.json.tmp");
|
||||
fs::write(&tmp, &json)
|
||||
@@ -213,9 +213,7 @@ impl ChunkedUploadService {
|
||||
};
|
||||
|
||||
if recovered_count > 0 {
|
||||
tracing::info!(
|
||||
"♻️ Recovered {recovered_count} chunked-upload session(s) from disk"
|
||||
);
|
||||
tracing::info!("♻️ Recovered {recovered_count} chunked-upload session(s) from disk");
|
||||
}
|
||||
|
||||
// Start cleanup task
|
||||
@@ -325,10 +323,7 @@ impl ChunkedUploadService {
|
||||
// ── Cleanup ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Background task to clean expired sessions
|
||||
async fn cleanup_loop(
|
||||
sessions: Arc<DashMap<String, UploadSession>>,
|
||||
temp_base_dir: PathBuf,
|
||||
) {
|
||||
async fn cleanup_loop(sessions: Arc<DashMap<String, UploadSession>>, temp_base_dir: PathBuf) {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(3600)); // Every hour
|
||||
|
||||
loop {
|
||||
@@ -463,7 +458,8 @@ impl ChunkedUploadService {
|
||||
) -> Result<ChunkUploadResponseDto, String> {
|
||||
// Validate session exists and chunk index is valid
|
||||
let (chunk_path, expected_size) = {
|
||||
let session = self.sessions
|
||||
let session = self
|
||||
.sessions
|
||||
.get(upload_id)
|
||||
.ok_or_else(|| format!("Upload session not found: {}", upload_id))?;
|
||||
|
||||
@@ -500,11 +496,10 @@ impl ChunkedUploadService {
|
||||
// worker free for other connections.
|
||||
if let Some(ref expected_checksum) = checksum {
|
||||
let data_clone = data.clone(); // Bytes::clone is O(1) — just an Arc increment
|
||||
let actual_checksum = tokio::task::spawn_blocking(move || {
|
||||
format!("{:x}", md5::compute(&data_clone))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("MD5 checksum task failed: {e}"))?;
|
||||
let actual_checksum =
|
||||
tokio::task::spawn_blocking(move || format!("{:x}", md5::compute(&data_clone)))
|
||||
.await
|
||||
.map_err(|e| format!("MD5 checksum task failed: {e}"))?;
|
||||
|
||||
if actual_checksum != *expected_checksum {
|
||||
return Err(format!(
|
||||
@@ -527,7 +522,8 @@ impl ChunkedUploadService {
|
||||
// Disk I/O (persist_progress) is done AFTER the ref is dropped so
|
||||
// concurrent uploads to other sessions are never blocked.
|
||||
let (bytes_received, progress, is_complete, persist_path, persist_bitmask) = {
|
||||
let mut session = self.sessions
|
||||
let mut session = self
|
||||
.sessions
|
||||
.get_mut(upload_id)
|
||||
.ok_or_else(|| "Session disappeared".to_string())?;
|
||||
|
||||
@@ -571,11 +567,9 @@ impl ChunkedUploadService {
|
||||
}
|
||||
|
||||
/// Get upload status
|
||||
async fn get_status_inner(
|
||||
&self,
|
||||
upload_id: &str,
|
||||
) -> Result<UploadStatusResponseDto, String> {
|
||||
let session = self.sessions
|
||||
async fn get_status_inner(&self, upload_id: &str) -> Result<UploadStatusResponseDto, String> {
|
||||
let session = self
|
||||
.sessions
|
||||
.get(upload_id)
|
||||
.ok_or_else(|| format!("Upload session not found: {}", upload_id))?;
|
||||
|
||||
@@ -613,7 +607,8 @@ impl ChunkedUploadService {
|
||||
// Clone the session data and drop the DashMap ref immediately
|
||||
// so the shard is not held during the expensive assembly step.
|
||||
let session = {
|
||||
let entry = self.sessions
|
||||
let entry = self
|
||||
.sessions
|
||||
.get(upload_id)
|
||||
.ok_or_else(|| format!("Upload session not found: {}", upload_id))?;
|
||||
|
||||
@@ -640,12 +635,17 @@ impl ChunkedUploadService {
|
||||
let chunks_meta: Vec<(usize, PathBuf)> = session
|
||||
.chunks
|
||||
.iter()
|
||||
.map(|c| (c.index, session.temp_dir.join(format!("chunk_{:06}", c.index))))
|
||||
.map(|c| {
|
||||
(
|
||||
c.index,
|
||||
session.temp_dir.join(format!("chunk_{:06}", c.index)),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let total_size = session.total_size;
|
||||
|
||||
let hash = tokio::task::spawn_blocking(move || -> Result<String, String> {
|
||||
use std::io::{Read, Write, BufWriter as StdBufWriter};
|
||||
use std::io::{BufWriter as StdBufWriter, Read, Write};
|
||||
|
||||
let raw_output = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
@@ -920,8 +920,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let json = serde_json::to_vec(&session).expect("serialise");
|
||||
let restored: UploadSession =
|
||||
serde_json::from_slice(&json).expect("deserialise");
|
||||
let restored: UploadSession = serde_json::from_slice(&json).expect("deserialise");
|
||||
|
||||
assert_eq!(restored.id, session.id);
|
||||
assert_eq!(restored.filename, session.filename);
|
||||
@@ -996,7 +995,9 @@ mod tests {
|
||||
|
||||
let recovered = ChunkedUploadService::recover_sessions(&base).await;
|
||||
assert_eq!(recovered.len(), 1);
|
||||
let session = recovered.get(&upload_id).expect("session must be recovered");
|
||||
let session = recovered
|
||||
.get(&upload_id)
|
||||
.expect("session must be recovered");
|
||||
assert_eq!(session.filename, "bigfile.bin");
|
||||
assert_eq!(session.folder_id, Some("folder-x".into()));
|
||||
assert_eq!(session.chunks[0].status, ChunkStatus::Complete);
|
||||
@@ -1051,10 +1052,8 @@ mod tests {
|
||||
assert!(status.pending_chunks.is_empty());
|
||||
|
||||
// 4. Complete (assemble)
|
||||
let (path, filename, _folder, _ct, size, hash) = service
|
||||
.complete_upload_inner(&id)
|
||||
.await
|
||||
.expect("complete");
|
||||
let (path, filename, _folder, _ct, size, hash) =
|
||||
service.complete_upload_inner(&id).await.expect("complete");
|
||||
assert_eq!(filename, "test.txt");
|
||||
assert_eq!(size, 1024);
|
||||
assert!(!hash.is_empty());
|
||||
@@ -1078,7 +1077,13 @@ mod tests {
|
||||
let service = ChunkedUploadService::new(base.clone()).await;
|
||||
|
||||
let resp = service
|
||||
.create_session_inner("x.bin".into(), None, "application/octet-stream".into(), 512, Some(512))
|
||||
.create_session_inner(
|
||||
"x.bin".into(),
|
||||
None,
|
||||
"application/octet-stream".into(),
|
||||
512,
|
||||
Some(512),
|
||||
)
|
||||
.await
|
||||
.expect("create");
|
||||
|
||||
@@ -1156,12 +1161,18 @@ mod tests {
|
||||
chunk_size: 512,
|
||||
chunks: vec![
|
||||
ChunkInfo {
|
||||
index: 0, offset: 0, size: 512,
|
||||
status: ChunkStatus::Pending, checksum: None,
|
||||
index: 0,
|
||||
offset: 0,
|
||||
size: 512,
|
||||
status: ChunkStatus::Pending,
|
||||
checksum: None,
|
||||
},
|
||||
ChunkInfo {
|
||||
index: 1, offset: 512, size: 512,
|
||||
status: ChunkStatus::Pending, checksum: None,
|
||||
index: 1,
|
||||
offset: 512,
|
||||
size: 512,
|
||||
status: ChunkStatus::Pending,
|
||||
checksum: None,
|
||||
},
|
||||
],
|
||||
created_at: Utc::now(),
|
||||
@@ -1172,14 +1183,20 @@ mod tests {
|
||||
|
||||
// Write metadata
|
||||
let json = serde_json::to_vec(&session).unwrap();
|
||||
fs::write(session_dir.join(SESSION_META_FILE), &json).await.unwrap();
|
||||
fs::write(session_dir.join(SESSION_META_FILE), &json)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Write progress marking both chunks complete
|
||||
let bitmask = vec![0b00000011u8]; // bits 0 and 1
|
||||
fs::write(session_dir.join(PROGRESS_FILE), &bitmask).await.unwrap();
|
||||
fs::write(session_dir.join(PROGRESS_FILE), &bitmask)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// But only create chunk_000000 on disk — chunk_000001 is "missing"
|
||||
fs::write(session_dir.join("chunk_000000"), &[0u8; 512]).await.unwrap();
|
||||
fs::write(session_dir.join("chunk_000000"), &[0u8; 512])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let recovered = ChunkedUploadService::recover_sessions(&base).await;
|
||||
let s = recovered.get("partial-session").expect("must be recovered");
|
||||
|
||||
@@ -769,9 +769,7 @@ impl DedupService {
|
||||
.bind(BATCH_SIZE)
|
||||
.fetch_all(self.maintenance_pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error("Dedup", format!("GC batch failed: {e}"))
|
||||
})?;
|
||||
.map_err(|e| DomainError::internal_error("Dedup", format!("GC batch failed: {e}")))?;
|
||||
|
||||
if batch.is_empty() {
|
||||
break;
|
||||
|
||||
@@ -192,11 +192,7 @@ impl ThumbnailService {
|
||||
}
|
||||
|
||||
// 2. Generate thumbnail (CPU-bound, runs in spawn_blocking)
|
||||
tracing::info!(
|
||||
"🎨 Generating thumbnail: {} {:?}",
|
||||
file_id_owned,
|
||||
size
|
||||
);
|
||||
tracing::info!("🎨 Generating thumbnail: {} {:?}", file_id_owned, size);
|
||||
match self.generate_thumbnail(&original_owned, size).await {
|
||||
Ok(bytes) => {
|
||||
// Save to disk (best-effort — don't fail the request)
|
||||
@@ -245,14 +241,17 @@ impl ThumbnailService {
|
||||
let max_dim = size.max_dimension();
|
||||
|
||||
// Acquire semaphore permit — bounds peak RAM from concurrent decodes
|
||||
let _permit = self.decode_semaphore.acquire().await
|
||||
let _permit = self
|
||||
.decode_semaphore
|
||||
.acquire()
|
||||
.await
|
||||
.map_err(|_| ThumbnailError::TaskError("Decode semaphore closed".into()))?;
|
||||
|
||||
// Run image processing in blocking thread pool
|
||||
let result = tokio::task::spawn_blocking(move || -> Result<Vec<u8>, ThumbnailError> {
|
||||
// Single read: load file once into memory, then work from the buffer
|
||||
let data = std::fs::read(&path)
|
||||
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||
let data =
|
||||
std::fs::read(&path).map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||
|
||||
// Safety check: read dimensions from in-memory buffer (no 2nd I/O)
|
||||
let (w, h) = image::ImageReader::new(std::io::Cursor::new(&data))
|
||||
@@ -284,9 +283,9 @@ impl ThumbnailService {
|
||||
// Adaptive filter: faster filters for smaller sizes where
|
||||
// quality difference vs Lanczos3 is imperceptible
|
||||
let filter = match size {
|
||||
ThumbnailSize::Icon => FilterType::Triangle, // 150px — max speed
|
||||
ThumbnailSize::Icon => FilterType::Triangle, // 150px — max speed
|
||||
ThumbnailSize::Preview => FilterType::CatmullRom, // 400px — good balance
|
||||
ThumbnailSize::Large => FilterType::CatmullRom, // 800px — sufficient quality
|
||||
ThumbnailSize::Large => FilterType::CatmullRom, // 800px — sufficient quality
|
||||
};
|
||||
let thumbnail = img.resize(new_width, new_height, filter);
|
||||
|
||||
@@ -318,7 +317,10 @@ impl ThumbnailService {
|
||||
let _permit = match self.decode_semaphore.acquire().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
tracing::warn!("Decode semaphore closed, skipping thumbnails for {}", file_id);
|
||||
tracing::warn!(
|
||||
"Decode semaphore closed, skipping thumbnails for {}",
|
||||
file_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -328,8 +330,8 @@ impl ThumbnailService {
|
||||
// Single spawn_blocking: 1 read + 1 decode + 3 resize + 3 encode
|
||||
let results = tokio::task::spawn_blocking(move || {
|
||||
// Single read: load file once into memory
|
||||
let data = std::fs::read(&path)
|
||||
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||
let data =
|
||||
std::fs::read(&path).map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||
|
||||
// Safety check: read dimensions from in-memory buffer (no 2nd I/O)
|
||||
let (w, h) = image::ImageReader::new(std::io::Cursor::new(&data))
|
||||
@@ -364,18 +366,15 @@ impl ThumbnailService {
|
||||
};
|
||||
|
||||
let filter = match size {
|
||||
ThumbnailSize::Icon => FilterType::Triangle,
|
||||
ThumbnailSize::Icon => FilterType::Triangle,
|
||||
ThumbnailSize::Preview => FilterType::CatmullRom,
|
||||
ThumbnailSize::Large => FilterType::CatmullRom,
|
||||
ThumbnailSize::Large => FilterType::CatmullRom,
|
||||
};
|
||||
let thumb = img.resize(new_w, new_h, filter);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
thumb
|
||||
.write_to(
|
||||
&mut std::io::Cursor::new(&mut buf),
|
||||
ImageFormat::WebP,
|
||||
)
|
||||
.write_to(&mut std::io::Cursor::new(&mut buf), ImageFormat::WebP)
|
||||
.map_err(|e| ThumbnailError::ImageError(e.to_string()))?;
|
||||
|
||||
Ok((size, Bytes::from(buf)))
|
||||
@@ -388,15 +387,11 @@ impl ThumbnailService {
|
||||
let thumbnails = match results {
|
||||
Ok(Ok(t)) => t,
|
||||
Ok(Err(e)) => {
|
||||
tracing::warn!(
|
||||
"Thumbnail generation failed for {}: {}", file_id, e
|
||||
);
|
||||
tracing::warn!("Thumbnail generation failed for {}: {}", file_id, e);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"Thumbnail task panicked for {}: {}", file_id, e
|
||||
);
|
||||
tracing::warn!("Thumbnail task panicked for {}: {}", file_id, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,10 +17,7 @@ pub struct TrashCleanupService {
|
||||
}
|
||||
|
||||
impl TrashCleanupService {
|
||||
pub fn new(
|
||||
trash_repository: Arc<dyn TrashRepository>,
|
||||
cleanup_interval_hours: u64,
|
||||
) -> Self {
|
||||
pub fn new(trash_repository: Arc<dyn TrashRepository>, cleanup_interval_hours: u64) -> Self {
|
||||
Self {
|
||||
trash_repository,
|
||||
cleanup_interval_hours: cleanup_interval_hours.max(1), // Minimum 1 hour
|
||||
@@ -60,9 +57,7 @@ impl TrashCleanupService {
|
||||
|
||||
/// Bulk-delete all expired trash items in a single transaction.
|
||||
#[instrument(skip(trash_repository))]
|
||||
async fn cleanup_expired_items(
|
||||
trash_repository: Arc<dyn TrashRepository>,
|
||||
) -> Result<()> {
|
||||
async fn cleanup_expired_items(trash_repository: Arc<dyn TrashRepository>) -> Result<()> {
|
||||
debug!("Starting bulk cleanup of expired trash items");
|
||||
|
||||
let (files, folders) = trash_repository.delete_expired_bulk().await?;
|
||||
|
||||
Reference in New Issue
Block a user