feat: update chunked upload endpoints and enhance upload resilience with improved logging and HMAC validation

This commit is contained in:
2025-07-17 19:09:22 +02:00
parent 59679edbce
commit dc88f9f6fb
18 changed files with 181 additions and 43 deletions

View File

@ -55,9 +55,9 @@ The network resilience features have been successfully implemented and are ready
| Endpoint | Method | Purpose | | Endpoint | Method | Purpose |
|----------|--------|---------| |----------|--------|---------|
| `/upload/chunked` | POST | Start new chunked upload session | | `/chunked-upload` | POST | Start new chunked upload session |
| `/upload/chunked` | PUT | Upload individual chunks | | `/chunked-upload` | PUT | Upload individual chunks |
| `/upload/status` | GET | Check upload progress | | `/upload-status` | GET | Check upload progress |
| `/upload` | POST | Traditional uploads (unchanged) | | `/upload` | POST | Traditional uploads (unchanged) |
## 📱 Network Switching Benefits ## 📱 Network Switching Benefits
@ -89,17 +89,17 @@ curl -X POST \
-H "X-Filename: large_video.mp4" \ -H "X-Filename: large_video.mp4" \
-H "X-Total-Size: 104857600" \ -H "X-Total-Size: 104857600" \
-H "X-Signature: HMAC" \ -H "X-Signature: HMAC" \
http://localhost:8080/upload/chunked http://localhost:8080/chunked-upload
# 2. Upload chunks (automatically handles network switches) # 2. Upload chunks (automatically handles network switches)
curl -X PUT \ curl -X PUT \
-H "X-Upload-Session-ID: session_123" \ -H "X-Upload-Session-ID: session_123" \
-H "X-Chunk-Number: 0" \ -H "X-Chunk-Number: 0" \
--data-binary @chunk_0.bin \ --data-binary @chunk_0.bin \
http://localhost:8080/upload/chunked http://localhost:8080/chunked-upload
# 3. Check progress # 3. Check progress
curl "http://localhost:8080/upload/status?session_id=session_123" curl "http://localhost:8080/upload-status?session_id=session_123"
``` ```
## ⚙️ Configuration ## ⚙️ Configuration

BIN
chunk_0.bin Normal file

Binary file not shown.

View File

@ -30,6 +30,25 @@ func handleChunkedUpload(w http.ResponseWriter, r *http.Request) {
return return
} }
// BASIC HMAC VALIDATION - same as original handleUpload
if conf.Security.EnableJWT {
_, err := validateJWTFromRequest(r, conf.Security.JWTSecret)
if err != nil {
http.Error(w, fmt.Sprintf("JWT Authentication failed: %v", err), http.StatusUnauthorized)
uploadErrorsTotal.Inc()
return
}
log.Debugf("JWT authentication successful for chunked upload: %s", r.URL.Path)
} else {
err := validateHMAC(r, conf.Security.Secret)
if err != nil {
http.Error(w, fmt.Sprintf("HMAC Authentication failed: %v", err), http.StatusUnauthorized)
uploadErrorsTotal.Inc()
return
}
log.Debugf("HMAC authentication successful for chunked upload: %s", r.URL.Path)
}
// Extract headers for chunked upload // Extract headers for chunked upload
sessionID := r.Header.Get("X-Upload-Session-ID") sessionID := r.Header.Get("X-Upload-Session-ID")
chunkNumberStr := r.Header.Get("X-Chunk-Number") chunkNumberStr := r.Header.Get("X-Chunk-Number")
@ -122,8 +141,10 @@ func handleChunkedUpload(w http.ResponseWriter, r *http.Request) {
defer chunkFile.Close() defer chunkFile.Close()
// Copy chunk data with progress tracking // Copy chunk data with progress tracking
log.Printf("DEBUG: Processing chunk %d for session %s (content-length: %d)", chunkNumber, sessionID, r.ContentLength)
written, err := copyChunkWithResilience(chunkFile, r.Body, r.ContentLength, sessionID, chunkNumber) written, err := copyChunkWithResilience(chunkFile, r.Body, r.ContentLength, sessionID, chunkNumber)
if err != nil { if err != nil {
log.Printf("ERROR: Failed to save chunk %d for session %s: %v", chunkNumber, sessionID, err)
http.Error(w, fmt.Sprintf("Error saving chunk: %v", err), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("Error saving chunk: %v", err), http.StatusInternalServerError)
uploadErrorsTotal.Inc() uploadErrorsTotal.Inc()
os.Remove(chunkPath) // Clean up failed chunk os.Remove(chunkPath) // Clean up failed chunk
@ -138,15 +159,32 @@ func handleChunkedUpload(w http.ResponseWriter, r *http.Request) {
return return
} }
// Get updated session for completion check
session, _ = uploadSessionStore.GetSession(sessionID)
progress := float64(session.UploadedBytes) / float64(session.TotalSize)
// Debug logging for large files
if session.TotalSize > 50*1024*1024 { // Log for files > 50MB
log.Debugf("Chunk %d uploaded for %s: %d/%d bytes (%.1f%%)",
chunkNumber, session.Filename, session.UploadedBytes, session.TotalSize, progress*100)
}
// Check if upload is complete // Check if upload is complete
if uploadSessionStore.IsSessionComplete(sessionID) { isComplete := uploadSessionStore.IsSessionComplete(sessionID)
log.Printf("DEBUG: Session %s completion check: %v (uploaded: %d, total: %d, progress: %.1f%%)",
sessionID, isComplete, session.UploadedBytes, session.TotalSize, progress*100)
if isComplete {
log.Printf("DEBUG: Starting file assembly for session %s", sessionID)
// Assemble final file // Assemble final file
finalPath, err := uploadSessionStore.AssembleFile(sessionID) finalPath, err := uploadSessionStore.AssembleFile(sessionID)
if err != nil { if err != nil {
log.Printf("ERROR: File assembly failed for session %s: %v", sessionID, err)
http.Error(w, fmt.Sprintf("Error assembling file: %v", err), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("Error assembling file: %v", err), http.StatusInternalServerError)
uploadErrorsTotal.Inc() uploadErrorsTotal.Inc()
return return
} }
log.Printf("DEBUG: File assembly completed for session %s: %s", sessionID, finalPath)
// Handle deduplication if enabled (reuse existing logic) // Handle deduplication if enabled (reuse existing logic)
if conf.Server.DeduplicationEnabled { if conf.Server.DeduplicationEnabled {

View File

@ -603,11 +603,6 @@ func setupRouter() *http.ServeMux {
log.Info("HTTP router configured successfully with full protocol support (v, v2, token, v3)") log.Info("HTTP router configured successfully with full protocol support (v, v2, token, v3)")
// Enhance router with network resilience features (non-intrusive)
if conf.Uploads.ChunkedUploadsEnabled {
EnhanceExistingRouter(mux)
}
return mux return mux
} }

View File

@ -8,6 +8,21 @@ import (
"time" "time"
) )
// Global flag to prevent duplicate route registration
var routesEnhanced bool
// InitializeEnhancements initializes all new features and enhances the router
func InitializeEnhancements(router *http.ServeMux) {
// Initialize upload resilience system
InitializeUploadResilience()
// Enhance the existing router with new endpoints (only once)
if !routesEnhanced {
EnhanceExistingRouter(router)
routesEnhanced = true
}
}
// InitializeUploadResilience initializes the upload resilience system // InitializeUploadResilience initializes the upload resilience system
func InitializeUploadResilience() { func InitializeUploadResilience() {
// Initialize upload session store // Initialize upload session store
@ -22,16 +37,13 @@ func InitializeUploadResilience() {
// EnhanceExistingRouter adds new routes without modifying existing setupRouter function // EnhanceExistingRouter adds new routes without modifying existing setupRouter function
func EnhanceExistingRouter(mux *http.ServeMux) { func EnhanceExistingRouter(mux *http.ServeMux) {
// Add chunked upload endpoints // BASIC FUNCTION: Add chunked upload endpoints directly without wrappers
mux.HandleFunc("/upload/chunked", ResilientHTTPHandler(handleChunkedUpload, networkManager)) mux.HandleFunc("/chunked-upload", handleChunkedUpload)
mux.HandleFunc("/upload/status", handleUploadStatus) mux.HandleFunc("/upload-status", handleUploadStatus)
// Wrap existing upload handlers with resilience (optional) log.Info("Enhanced upload endpoints added:")
if conf.Uploads.ChunkedUploadsEnabled { log.Info(" POST/PUT /chunked-upload - Chunked/resumable uploads")
log.Info("Enhanced upload endpoints added:") log.Info(" GET /upload-status - Upload status check")
log.Info(" POST/PUT /upload/chunked - Chunked/resumable uploads")
log.Info(" GET /upload/status - Upload status check")
}
} }
// UpdateConfigurationDefaults suggests better defaults without forcing changes // UpdateConfigurationDefaults suggests better defaults without forcing changes
@ -116,19 +128,3 @@ func GetResilienceStatus() map[string]interface{} {
return status return status
} }
// Non-intrusive initialization function to be called from main()
func InitializeEnhancements() {
// Only initialize if chunked uploads are enabled
if conf.Uploads.ChunkedUploadsEnabled {
InitializeUploadResilience()
// Start performance monitoring
go MonitorUploadPerformance()
// Log configuration recommendations
UpdateConfigurationDefaults()
} else {
log.Info("Chunked uploads disabled. Enable 'chunkeduploadsenabled = true' for network resilience features")
}
}

View File

@ -698,6 +698,9 @@ func main() {
router := setupRouter() // Assuming setupRouter is defined (likely in this file or router.go router := setupRouter() // Assuming setupRouter is defined (likely in this file or router.go
// Initialize enhancements and enhance the router
InitializeEnhancements(router)
go handleFileCleanup(&conf) // Directly call handleFileCleanup go handleFileCleanup(&conf) // Directly call handleFileCleanup
readTimeout, err := time.ParseDuration(conf.Timeouts.Read) // Corrected field name readTimeout, err := time.ParseDuration(conf.Timeouts.Read) // Corrected field name
@ -766,9 +769,6 @@ func main() {
} }
log.Infof("Running version: %s", versionString) log.Infof("Running version: %s", versionString)
// Initialize network resilience features (non-intrusive)
InitializeEnhancements()
log.Infof("Starting HMAC file server %s...", versionString) log.Infof("Starting HMAC file server %s...", versionString)
if conf.Server.UnixSocket { if conf.Server.UnixSocket {
socketPath := "/tmp/hmac-file-server.sock" // Use a default socket path since ListenAddress is now a port socketPath := "/tmp/hmac-file-server.sock" // Use a default socket path since ListenAddress is now a port

View File

@ -6,7 +6,7 @@ bind_ip = "0.0.0.0"
listenport = "8080" listenport = "8080"
unixsocket = false unixsocket = false
storagepath = "./uploads" storagepath = "./uploads"
metricsenabled = true metricsenabled = false
metricsport = "9090" metricsport = "9090"
deduplicationenabled = true deduplicationenabled = true
networkevents = true # Enable network change detection networkevents = true # Enable network change detection

105
debug_upload.sh Executable file
View File

@ -0,0 +1,105 @@
#!/bin/bash
# Simple test to debug the 49% upload stop issue
set -e
echo "[DEBUG-TEST] Starting server..."
./hmac-file-server --config config-network-resilience.toml > debug_server.log 2>&1 &
SERVER_PID=$!
# Wait for server to start
sleep 3
# Check if server is running
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "[ERROR] Server failed to start"
cat debug_server.log
exit 1
fi
cleanup() {
echo "[DEBUG-TEST] Cleaning up..."
kill $SERVER_PID 2>/dev/null || true
rm -f debug_server.log
}
trap cleanup EXIT
echo "[DEBUG-TEST] Testing 50MB chunked upload..."
# Calculate HMAC signature
SECRET="your-super-secret-hmac-key-minimum-32-characters-long"
MESSAGE="/chunked-upload"
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
# Start session
echo "[DEBUG-TEST] Creating session..."
SESSION_RESPONSE=$(curl -s -X POST \
-H "X-Filename: test_50mb.bin" \
-H "X-Total-Size: 52428800" \
-H "X-Signature: $SIGNATURE" \
http://localhost:8080/chunked-upload)
echo "[DEBUG-TEST] Session response: $SESSION_RESPONSE"
SESSION_ID=$(echo "$SESSION_RESPONSE" | grep -o '"session_id":"[^"]*"' | cut -d'"' -f4)
if [ -z "$SESSION_ID" ]; then
echo "[ERROR] Failed to get session ID"
exit 1
fi
echo "[DEBUG-TEST] Session ID: $SESSION_ID"
# Upload first few chunks to see what happens
CHUNK_SIZE=5242880 # 5MB
for i in {0..12}; do # Upload first 13 chunks (65MB worth, should trigger completion)
OFFSET=$((i * CHUNK_SIZE))
echo "[DEBUG-TEST] Creating chunk $i..."
dd if=test_50mb.bin of=chunk_$i.bin bs=$CHUNK_SIZE skip=$i count=1 2>/dev/null || {
# Handle the last chunk
REMAINING=$((52428800 - OFFSET))
if [ $REMAINING -gt 0 ]; then
dd if=test_50mb.bin of=chunk_$i.bin bs=1 skip=$OFFSET count=$REMAINING 2>/dev/null
else
echo "[DEBUG-TEST] No more data for chunk $i"
break
fi
}
CHUNK_SIZE_ACTUAL=$(stat -f%z chunk_$i.bin 2>/dev/null || stat -c%s chunk_$i.bin 2>/dev/null)
echo "[DEBUG-TEST] Uploading chunk $i (size: $CHUNK_SIZE_ACTUAL bytes)..."
UPLOAD_RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
-H "X-Upload-Session-ID: $SESSION_ID" \
-H "X-Chunk-Number: $i" \
--data-binary @chunk_$i.bin \
http://localhost:8080/chunked-upload)
echo "[DEBUG-TEST] Upload response for chunk $i:"
echo "$UPLOAD_RESPONSE"
echo "---"
# Check server logs for debug output
echo "[DEBUG-TEST] Recent server logs:"
tail -5 debug_server.log
echo "---"
# Check if complete
if echo "$UPLOAD_RESPONSE" | grep -q '"complete":true'; then
echo "[DEBUG-TEST] ✅ Upload completed at chunk $i"
rm -f chunk_*.bin
exit 0
fi
rm -f chunk_$i.bin
sleep 1
done
echo "[DEBUG-TEST] Upload did not complete. Checking status..."
STATUS_RESPONSE=$(curl -s "http://localhost:8080/upload-status?session_id=$SESSION_ID")
echo "[DEBUG-TEST] Final status: $STATUS_RESPONSE"
echo "[DEBUG-TEST] Full server logs:"
cat debug_server.log

View File

@ -0,0 +1 @@
Hello, HMAC File Server! Do 17. Jul 18:59:11 CEST 2025

View File

@ -0,0 +1 @@
Hello, HMAC File Server! Do 17. Jul 18:59:11 CEST 2025

View File

@ -1 +1 @@
566111 619742

BIN
test_1mb.bin Normal file

Binary file not shown.

1
test_1mb.txt Normal file
View File

@ -0,0 +1 @@
Hello, HMAC File Server! Do 17. Jul 18:59:11 CEST 2025

BIN
test_215mb.bin Normal file

Binary file not shown.

BIN
test_4gb.bin Normal file

Binary file not shown.

1
test_4gb.txt Normal file
View File

@ -0,0 +1 @@
Hello, HMAC File Server! Do 17. Jul 18:59:11 CEST 2025

BIN
test_50mb.bin Normal file

Binary file not shown.

View File

@ -1 +1 @@
Hello, HMAC File Server! Do 17. Jul 18:18:58 CEST 2025 Hello, HMAC File Server! Do 17. Jul 18:59:11 CEST 2025