Adding chunk file support

This commit is contained in:
2025-04-16 21:40:35 -04:00
parent f90cc7691c
commit d4be29386b
7 changed files with 710 additions and 3 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
tmp/ tmp/
uploads/ uploads/
file_metadata.json file_metadata.json
chunk_metadata.json
.env .env
# Python # Python

326
LARGE_UPLOAD.md Normal file
View File

@@ -0,0 +1,326 @@
# Large File Upload Analysis and Improvements
## Current Implementation Analysis
### Client Side (Android)
The SimpleLogUpload Android application is designed to upload large log files (up to 220MB) to a simple file upload server. The current implementation uses:
- OkHttp3 for HTTP requests
- A single monolithic file upload in one HTTP request
- Fixed network timeouts:
- Connection timeout: 60 seconds
- Write timeout: 180 seconds (3 minutes)
- Read timeout: 60 seconds
- Basic retry on connection failure
Behavior by network type:
- **WiFi**: Uploads work successfully
- **5G/Mobile**: Socket timeouts occur during upload (`java.net.SocketException: Socket closed`)
### Server Side (Python/Flask)
The server is a simple Flask application with:
- Standard file upload endpoint (`/upload`)
- No explicit chunked upload support
- Uses Gunicorn as WSGI server without specific timeout configurations
- No specific optimizations for large files
## Problem Root Causes
1. **Mobile Network Limitations**:
- Mobile carriers may have connection restrictions for large uploads
- 5G networks may have more aggressive connection management
- Network fluctuations are more common in mobile networks
2. **Timeout Configuration**:
- Current timeout settings may be insufficient for large files over mobile networks
- Server and client timeouts aren't aligned
3. **Single Upload Approach**:
- The entire 220MB file is uploaded in a single HTTP request
- No resume capability if connection drops
- No progress tracking or partial file recovery
4. **Missing Network Adaptability**:
- No differentiation between WiFi and mobile network settings
- No adaptive timeout or retry mechanisms
- No connection monitoring during upload
## Recommended Solutions
### 1. Implement Chunked Uploading
Develop a proper chunked upload system with these features:
```java
// Example chunked upload strategy
public class ChunkedUploadManager {
// Adjust these based on testing
private static final int DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
private static final int MAX_RETRY_ATTEMPTS = 3;
private static final int RETRY_DELAY_MS = 1000;
// File metadata
private File file;
private int chunkSize;
private int totalChunks;
private String uploadId;
// Network state
private boolean isWifiConnection;
private boolean isPaused;
// Tracking
private int currentChunkIndex;
private Set<Integer> uploadedChunks = new HashSet<>();
public ChunkedUploadManager(File file, boolean isWifiConnection) {
this.file = file;
this.isWifiConnection = isWifiConnection;
// Adjust chunk size based on network type
this.chunkSize = isWifiConnection ? DEFAULT_CHUNK_SIZE : (DEFAULT_CHUNK_SIZE / 2);
this.totalChunks = (int) Math.ceil((double) file.length() / chunkSize);
}
// Methods for initialization, chunk upload, tracking, etc.
}
```
**Server Changes Required**:
```python
# New endpoint for chunked uploads
@app.route('/upload/chunk', methods=['POST'])
@require_client_key
def upload_chunk():
# Get parameters
chunk_index = int(request.form.get('chunk_index'))
total_chunks = int(request.form.get('total_chunks'))
upload_id = request.form.get('upload_id')
filename = request.form.get('filename')
# Handle chunk file
chunk_file = request.files['chunk']
# Process chunk
# ...
return jsonify({'success': True})
# New endpoint to start chunked upload session
@app.route('/upload/start', methods=['POST'])
@require_client_key
def start_chunked_upload():
# Initialize a new upload session
upload_id = generate_unique_id()
# Store upload metadata
# ...
return jsonify({'upload_id': upload_id})
# New endpoint to complete chunked upload
@app.route('/upload/complete', methods=['POST'])
@require_client_key
def complete_chunked_upload():
# Combine chunks into final file
# ...
return jsonify({'code': result_code})
```
### 2. Adaptive Network Settings
Implement network-aware settings that adjust based on connection type:
```java
private void configureNetworkSettings() {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isWifi = activeNetwork != null && activeNetwork.getType() == ConnectivityManager.TYPE_WIFI;
// Adjust settings based on network type
if (isWifi) {
// WiFi settings - can be more aggressive
CONNECTION_TIMEOUT = 60; // seconds
WRITE_TIMEOUT = 180; // seconds
READ_TIMEOUT = 60; // seconds
CHUNK_SIZE = 10_485_760; // 10MB
} else {
// Mobile network settings - more conservative
CONNECTION_TIMEOUT = 30; // seconds
WRITE_TIMEOUT = 90; // seconds
READ_TIMEOUT = 30; // seconds
CHUNK_SIZE = 2_097_152; // 2MB
}
}
```
### 3. Network Monitoring and Handling
Implement network state monitoring to handle:
- Network type changes during upload
- Connection losses
- Bandwidth fluctuations
```java
private void registerNetworkCallback() {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkRequest.Builder builder = new NetworkRequest.Builder();
cm.registerNetworkCallback(builder.build(), new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
// Resume upload if paused
resumeUploadIfPaused();
}
@Override
public void onLost(Network network) {
// Pause upload
pauseUpload();
}
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
// Adjust strategies based on new network capabilities
boolean isUnmetered = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
adjustUploadParameters(isUnmetered);
}
});
}
```
### 4. Progressive Backoff and Retry Strategy
Implement a more sophisticated retry mechanism:
```java
private void uploadChunkWithRetry(int chunkIndex) {
int retryCount = 0;
int maxRetries = 5;
long backoffMs = 1000; // Start with 1 second
while (retryCount < maxRetries) {
try {
// Attempt to upload chunk
boolean success = uploadChunk(chunkIndex);
if (success) {
return; // Success, exit retry loop
}
} catch (IOException e) {
Log.e(TAG, "Chunk upload failed: " + e.getMessage());
}
retryCount++;
if (retryCount < maxRetries) {
// Calculate backoff with exponential increase and jitter
long jitter = (long) (Math.random() * 0.3 * backoffMs);
long sleepTime = backoffMs + jitter;
Log.d(TAG, "Retrying chunk " + chunkIndex + " after " + sleepTime + "ms (attempt " + retryCount + ")");
try {
Thread.sleep(sleepTime);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return;
}
// Increase backoff for next iteration
backoffMs = Math.min(backoffMs * 2, 30000); // Cap at 30 seconds
}
}
// If we get here, all retries failed
Log.e(TAG, "All retry attempts failed for chunk " + chunkIndex);
reportChunkFailed(chunkIndex);
}
```
### 5. Enhanced Progress Tracking and Resumability
Implement more granular progress tracking for better user experience and resumability:
```java
public class UploadProgress {
private long totalBytes;
private long uploadedBytes;
private int totalChunks;
private Set<Integer> completedChunks;
private boolean isComplete;
private String sessionId;
// Persist progress to disk for resumability
public void saveProgress() {
SharedPreferences prefs = context.getSharedPreferences("upload_progress", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
// Store progress data
editor.putString("session_" + sessionId + "_chunks", TextUtils.join(",", completedChunks));
editor.putLong("session_" + sessionId + "_bytes", uploadedBytes);
editor.putBoolean("session_" + sessionId + "_complete", isComplete);
editor.apply();
}
// Load progress for resuming uploads
public static UploadProgress loadProgress(Context context, String sessionId) {
// Implementation details
}
}
```
### 6. Server-Side Optimizations
Modify the server to better handle large file uploads:
1. Implement explicit chunked upload support
2. Configure Gunicorn timeouts appropriately:
```
gunicorn wsgi:app --bind 0.0.0.0:7777 --timeout 300 --workers 4
```
3. Add support for resumable uploads with progress tracking
4. Implement temporary file cleanup for abandoned uploads
## Implementation Priority
1. **First Phase**:
- Implement basic chunked uploading
- Add network type detection and adaptive settings
- Configure server for higher timeouts
2. **Second Phase**:
- Add upload resumability
- Implement network state monitoring
- Enhanced progress tracking
- Progressive retry mechanism
3. **Final Phase**:
- Server-side optimizations for chunked uploads
- Background upload functionality
- Bandwidth control options
## Testing Strategy
1. Test uploading the same 220MB file under various network conditions:
- Stable WiFi
- Congested WiFi
- 5G with strong signal
- 5G with weak signal
- 4G fallback
- Network transitions (WiFi → Mobile)
2. Measure and compare:
- Success rate of uploads
- Total upload time
- Battery consumption
- Data usage efficiency
## Conclusion
The socket timeout issues on 5G networks stem from trying to upload large files in a single HTTP request without adapting to the characteristics of mobile networks. By implementing chunked uploads with resume capability, network-aware configurations, and robust error handling, we can create a more reliable upload solution for all network types.
The most critical issue to address is the lack of chunking, which leaves the application vulnerable to connection interruptions, particularly on mobile networks where connections may be less stable than WiFi.

View File

@@ -8,6 +8,7 @@ The Android mobile app for this server can be found at: https://github.com/matti
- Clean, modern UI for file downloads - Clean, modern UI for file downloads
- Secure API authentication for uploads - Secure API authentication for uploads
- Mobile-friendly upload endpoint - Mobile-friendly upload endpoint
- Chunked upload support for large files
- Specifically designed for the simplefileupload-android app - Specifically designed for the simplefileupload-android app
- Secure 6-digit alphanumeric code system - Secure 6-digit alphanumeric code system
- Automatic file cleanup - Automatic file cleanup
@@ -133,6 +134,100 @@ curl -X POST https://localhost:7777/upload \
} }
``` ```
### POST /upload/start
Start a chunked upload session for large files.
**Authentication:**
- Required: `X-Client-Key` header with your API key
**Form Fields:**
- `filename`: Name of the file being uploaded (required)
- `total_chunks`: Total number of chunks to expect (required)
- `max_downloads`: Maximum download count (optional, default: 1)
- `expiry_hours`: Hours until expiry (optional, default: 24)
**Example using curl:**
```bash
# Start a chunked upload session
curl -X POST http://localhost:7777/upload/start \
-H "X-Client-Key: your-secret-key" \
-F "filename=largefile.zip" \
-F "total_chunks=10" \
-F "max_downloads=5" \
-F "expiry_hours=48"
```
**Response:**
```json
{
"upload_id": "abC123dEf456gHij"
}
```
### POST /upload/chunk
Upload a single chunk of a file.
**Authentication:**
- Required: `X-Client-Key` header with your API key
**Form Fields:**
- `upload_id`: Upload session ID from /upload/start (required)
- `chunk_index`: Index of this chunk, starting from 0 (required)
- `chunk`: The file chunk data (required)
**Example using curl:**
```bash
# Upload chunk 0
curl -X POST http://localhost:7777/upload/chunk \
-H "X-Client-Key: your-secret-key" \
-F "upload_id=abC123dEf456gHij" \
-F "chunk_index=0" \
-F "chunk=@chunk_0.bin"
```
**Response:**
```json
{
"success": true,
"received_chunks": 5,
"total_chunks": 10
}
```
### POST /upload/complete
Complete a chunked upload session.
**Authentication:**
- Required: `X-Client-Key` header with your API key
**Form Fields:**
- `upload_id`: Upload session ID from /upload/start (required)
**Example using curl:**
```bash
# Complete the chunked upload
curl -X POST http://localhost:7777/upload/complete \
-H "X-Client-Key: your-secret-key" \
-F "upload_id=abC123dEf456gHij"
```
**Response:**
```json
{
"code": "ABC123"
}
```
**Error Responses for Chunked Upload Endpoints:**
- 401: Invalid or missing client key
- 400: Missing required parameters or invalid request
- 404: Invalid upload session ID
```json
{
"error": "Error message here"
}
```
### GET /download/{code} ### GET /download/{code}
Download a file using a 6-character code. Download a file using a 6-character code.

View File

@@ -64,6 +64,15 @@
- [ ] Create backup system - [ ] Create backup system
- [ ] Add health check endpoints - [ ] Add health check endpoints
### Chunked Upload Support
- [ ] Implement new API endpoints for chunked uploads (/upload/chunk, /upload/start, /upload/complete)
- [ ] Add server-side chunk management and reassembly
- [ ] Configure Gunicorn with appropriate timeouts for large uploads
- [ ] Implement temporary storage for in-progress chunked uploads
- [ ] Add automatic cleanup for abandoned uploads
- [ ] Create progress tracking for partially complete uploads
- [ ] Test with large files (>100MB) over various connection types
### UI/UX ### UI/UX
- [ ] Dark mode support - [ ] Dark mode support
- [ ] Accessibility improvements - [ ] Accessibility improvements

View File

@@ -3,7 +3,11 @@ from functools import wraps
from flask import Flask, request, jsonify, send_from_directory from flask import Flask, request, jsonify, send_from_directory
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import utils.mLogger as log import utils.mLogger as log
from controllers.file_controller import upload_file, download_file, delete_file from controllers.file_controller import (
upload_file, download_file, delete_file,
start_chunked_upload, upload_chunk, verify_chunks, complete_chunked_upload,
cleanup_expired_files, cleanup_abandoned_uploads
)
from views import views from views import views
app = Flask(__name__) app = Flask(__name__)
@@ -13,6 +17,10 @@ app.config['CLIENT_KEY'] = os.environ.get('CLIENT_KEY', 'default-secret-key')
if not os.path.exists(app.config['UPLOAD_FOLDER']): if not os.path.exists(app.config['UPLOAD_FOLDER']):
os.makedirs(app.config['UPLOAD_FOLDER']) os.makedirs(app.config['UPLOAD_FOLDER'])
# Run cleanup on startup
cleanup_expired_files()
cleanup_abandoned_uploads()
def require_client_key(f): def require_client_key(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
@@ -33,12 +41,19 @@ def require_client_key(f):
# Route for file uploads # Route for file uploads
app.route('/upload', methods=['POST'])(require_client_key(upload_file)) app.route('/upload', methods=['POST'])(require_client_key(upload_file))
# Routes for chunked uploads
app.route('/upload/start', methods=['POST'])(require_client_key(start_chunked_upload))
app.route('/upload/chunk', methods=['POST'])(require_client_key(upload_chunk))
app.route('/upload/verify', methods=['POST'])(require_client_key(verify_chunks))
app.route('/upload/complete', methods=['POST'])(require_client_key(complete_chunked_upload))
# Route for downloading the file # Route for downloading the file
app.route('/download/<code>', methods=['GET'])(download_file) app.route('/download/<code>', methods=['GET'])(download_file)
# Route for deleting the file # Route for deleting the file
app.route('/delete/<code>', methods=['DELETE'])(delete_file) app.route('/delete/<code>', methods=['DELETE'])(delete_file)
# Register frontend views
app.register_blueprint(views) app.register_blueprint(views)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -2,6 +2,7 @@ import os
import random import random
import string import string
import json import json
import shutil
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import request, jsonify, send_from_directory, render_template from flask import request, jsonify, send_from_directory, render_template
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -12,9 +13,17 @@ from models.file_metadata import FileMetadata
UPLOAD_FOLDER = os.path.abspath('tmp/uploads') UPLOAD_FOLDER = os.path.abspath('tmp/uploads')
METADATA_FILE = 'file_metadata.json' METADATA_FILE = 'file_metadata.json'
# Constants for chunked uploads
CHUNKS_FOLDER = os.path.join(UPLOAD_FOLDER, 'chunks')
CHUNK_METADATA_FILE = 'chunk_metadata.json'
if not os.path.exists(UPLOAD_FOLDER): if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER) os.makedirs(UPLOAD_FOLDER)
# Ensure chunks directory exists
if not os.path.exists(CHUNKS_FOLDER):
os.makedirs(CHUNKS_FOLDER)
def load_metadata(): def load_metadata():
if os.path.exists(METADATA_FILE): if os.path.exists(METADATA_FILE):
with open(METADATA_FILE, 'r') as f: with open(METADATA_FILE, 'r') as f:
@@ -25,9 +34,22 @@ def save_metadata(metadata):
with open(METADATA_FILE, 'w') as f: with open(METADATA_FILE, 'w') as f:
json.dump(metadata, f) json.dump(metadata, f)
def load_chunk_metadata():
if os.path.exists(CHUNK_METADATA_FILE):
with open(CHUNK_METADATA_FILE, 'r') as f:
return json.load(f)
return {}
def save_chunk_metadata(metadata):
with open(CHUNK_METADATA_FILE, 'w') as f:
json.dump(metadata, f)
def generate_unique_code(): def generate_unique_code():
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
def generate_upload_id():
return ''.join(random.choices(string.ascii_uppercase + string.digits + string.ascii_lowercase, k=16))
def upload_file(): def upload_file():
if 'file' not in request.files: if 'file' not in request.files:
log.e("No File Part") log.e("No File Part")
@@ -171,3 +193,242 @@ def cleanup_expired_files():
save_metadata(updated_metadata) save_metadata(updated_metadata)
log.i("Cleanup completed: Expired and overused files removed.") log.i("Cleanup completed: Expired and overused files removed.")
def start_chunked_upload():
# Get metadata from request
filename = request.form.get('filename')
if not filename:
return jsonify({'error': 'Filename is required'}), 400
total_chunks = request.form.get('total_chunks')
if not total_chunks or not total_chunks.isdigit():
return jsonify({'error': 'Invalid total_chunks parameter'}), 400
# Generate unique upload ID and create chunk directory
upload_id = generate_upload_id()
chunk_dir = os.path.join(CHUNKS_FOLDER, upload_id)
os.makedirs(chunk_dir)
# Store upload session metadata
metadata = load_chunk_metadata()
metadata[upload_id] = {
'filename': secure_filename(filename),
'total_chunks': int(total_chunks),
'received_chunks': [],
'created_at': datetime.now().isoformat(),
'chunk_dir': chunk_dir,
'max_downloads': int(request.form.get('max_downloads', 1)),
'expiry_hours': int(request.form.get('expiry_hours', 24))
}
save_chunk_metadata(metadata)
log.i(f"Started chunked upload session: {upload_id}")
return jsonify({'upload_id': upload_id}), 200
def upload_chunk():
# Validate parameters
upload_id = request.form.get('upload_id')
chunk_index = request.form.get('chunk_index')
total_chunks = request.form.get('total_chunks')
if not upload_id or not chunk_index or not chunk_index.isdigit():
log.e(f"Invalid chunk upload parameters: upload_id={upload_id}, chunk_index={chunk_index}")
return jsonify({'error': 'Invalid parameters'}), 400
chunk_index = int(chunk_index)
# Get upload session metadata
metadata = load_chunk_metadata()
if upload_id not in metadata:
log.e(f"Invalid upload session ID: {upload_id}")
return jsonify({'error': 'Invalid upload session'}), 404
session = metadata[upload_id]
# Validate chunk index
if chunk_index >= session['total_chunks']:
log.e(f"Invalid chunk index: {chunk_index}, total_chunks: {session['total_chunks']}")
return jsonify({'error': 'Invalid chunk index'}), 400
# Handle the chunk file
if 'chunk' not in request.files:
log.e(f"No chunk file provided for chunk {chunk_index} of upload {upload_id}")
return jsonify({'error': 'No chunk file provided'}), 400
chunk_file = request.files['chunk']
if chunk_file.filename == '':
log.e(f"Empty chunk file for chunk {chunk_index} of upload {upload_id}")
return jsonify({'error': 'Empty chunk file'}), 400
# Save the chunk
chunk_path = os.path.join(session['chunk_dir'], f'chunk_{chunk_index}')
chunk_file.save(chunk_path)
# Verify the chunk was saved successfully
if not os.path.exists(chunk_path) or os.path.getsize(chunk_path) == 0:
log.e(f"Failed to save chunk {chunk_index} for upload {upload_id}")
return jsonify({'error': 'Failed to save chunk'}), 500
# Update metadata
if chunk_index not in session['received_chunks']:
session['received_chunks'].append(chunk_index)
log.i(f"Added chunk {chunk_index} to received_chunks list for upload {upload_id}")
else:
log.w(f"Chunk {chunk_index} already in received_chunks list for upload {upload_id}, possible duplicate")
metadata[upload_id] = session
save_chunk_metadata(metadata)
# If client sent total_chunks, validate it matches what we have
if total_chunks and total_chunks.isdigit() and int(total_chunks) != session['total_chunks']:
log.w(f"Client reported total_chunks ({total_chunks}) doesn't match server ({session['total_chunks']})")
log.i(f"Received chunk {chunk_index} for upload {upload_id}, now have {len(session['received_chunks'])}/{session['total_chunks']}")
return jsonify({
'success': True,
'received_chunks': len(session['received_chunks']),
'total_chunks': session['total_chunks'],
'chunk_index': chunk_index
}), 200
def complete_chunked_upload():
upload_id = request.form.get('upload_id')
if not upload_id:
return jsonify({'error': 'Upload ID is required'}), 400
# Get upload session metadata
metadata = load_chunk_metadata()
if upload_id not in metadata:
return jsonify({'error': 'Invalid upload session'}), 404
session = metadata[upload_id]
# Verify all chunks are received by checking if every expected chunk index is in received_chunks
expected_chunks = set(range(session['total_chunks']))
received_chunks = set(session['received_chunks'])
if expected_chunks != received_chunks:
missing_chunks = list(expected_chunks - received_chunks)
log.e(f"Missing chunks for upload {upload_id}: {missing_chunks}")
return jsonify({
'error': 'Not all chunks received',
'received': len(session['received_chunks']),
'total': session['total_chunks'],
'missing_chunks': missing_chunks
}), 400
# Combine chunks into final file
final_filename = session['filename']
final_path = os.path.join(UPLOAD_FOLDER, final_filename)
# Verify all chunk files actually exist on disk
missing_files = []
for i in range(session['total_chunks']):
chunk_path = os.path.join(session['chunk_dir'], f'chunk_{i}')
if not os.path.exists(chunk_path):
missing_files.append(i)
if missing_files:
log.e(f"Missing chunk files for upload {upload_id} despite being in received_chunks: {missing_files}")
return jsonify({
'error': 'Chunk files missing on server',
'missing_files': missing_files
}), 500
with open(final_path, 'wb') as outfile:
for i in range(session['total_chunks']):
chunk_path = os.path.join(session['chunk_dir'], f'chunk_{i}')
with open(chunk_path, 'rb') as infile:
outfile.write(infile.read())
# Clean up chunks
import shutil
shutil.rmtree(session['chunk_dir'])
del metadata[upload_id]
save_chunk_metadata(metadata)
# Create regular file metadata
code = generate_unique_code()
expiry_time = datetime.now() + timedelta(hours=session['expiry_hours'])
file_metadata = load_metadata()
file_metadata[code] = {
'filename': final_filename,
'path': final_path,
'max_downloads': session['max_downloads'],
'downloads': 0,
'expiry_time': expiry_time.isoformat()
}
save_metadata(file_metadata)
log.i(f"Completed chunked upload {upload_id}, assigned code {code}")
return jsonify({'code': code}), 200
def cleanup_abandoned_uploads():
metadata = load_chunk_metadata()
current_time = datetime.now()
expired_uploads = []
for upload_id, session in metadata.items():
created_at = datetime.fromisoformat(session['created_at'])
# Remove uploads older than 24 hours
if (current_time - created_at).total_seconds() > 86400:
if os.path.exists(session['chunk_dir']):
shutil.rmtree(session['chunk_dir'])
expired_uploads.append(upload_id)
for upload_id in expired_uploads:
del metadata[upload_id]
if expired_uploads:
save_chunk_metadata(metadata)
log.i(f"Cleaned up {len(expired_uploads)} abandoned uploads")
def verify_chunks():
"""Verify which chunks have been received for an upload session"""
# Get upload ID from request
upload_id = request.form.get('upload_id')
if not upload_id:
return jsonify({'error': 'Upload ID is required'}), 400
# Get upload session metadata
metadata = load_chunk_metadata()
if upload_id not in metadata:
return jsonify({'error': 'Invalid upload session'}), 404
session = metadata[upload_id]
# Calculate missing chunks
received_chunks = set(session['received_chunks'])
all_chunks = set(range(session['total_chunks']))
missing_chunks = list(all_chunks - received_chunks)
# Check for each chunk file on disk to verify it actually exists
disk_missing_chunks = []
for chunk_index in range(session['total_chunks']):
chunk_path = os.path.join(session['chunk_dir'], f'chunk_{chunk_index}')
if not os.path.exists(chunk_path):
disk_missing_chunks.append(chunk_index)
# If it's in received_chunks but not on disk, there was a problem
if chunk_index in received_chunks:
log.w(f"Chunk {chunk_index} marked as received but file is missing for upload {upload_id}")
# Remove from received_chunks since the file is missing
if chunk_index in session['received_chunks']:
session['received_chunks'].remove(chunk_index)
# Update metadata if any changes were made
if len(received_chunks) != len(session['received_chunks']):
metadata[upload_id] = session
save_chunk_metadata(metadata)
# Return missing chunks information
log.i(f"Verified chunks for upload {upload_id}: {len(session['received_chunks'])}/{session['total_chunks']} received")
# Final verification
missing_chunks = list(all_chunks - set(session['received_chunks']))
return jsonify({
'total_chunks': session['total_chunks'],
'received_chunks': len(session['received_chunks']),
'missing_chunks': missing_chunks
}), 200

View File

@@ -7,7 +7,7 @@ key = os.getenv("SSL_KEY", "key.pem")
use_ssl = os.path.exists(cert) and os.path.exists(key) use_ssl = os.path.exists(cert) and os.path.exists(key)
base_cmd = [ base_cmd = [
"gunicorn", "wsgi:app", "--bind", "0.0.0.0:7777" "gunicorn", "wsgi:app", "--bind", "0.0.0.0:7777", "--workers", "4", "--timeout", "300",
] ]
if use_ssl: if use_ssl: