-
Couldn't load subscription status.
- Fork 0
Resume transfers #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Resume transfers #12
Conversation
- Switched from Redis lists to Redis Streams for chunk tracking
- Added upload/download progress persistence and resumption
- Implemented WebSocket /resume/{uid} endpoint for upload resumption
- Added HTTP Range header support for partial downloads (206 responses)
- Created resumption handler for managing transfer state
- Updated JavaScript client to save/restore upload progress via localStorage
- Added connection management for handling disconnections gracefully
- Transfers now wait for peer reconnection within timeout window
- All transfers are resumable by default (no configuration needed)
Key components:
- lib/store.py: Refactored to use Redis Streams with progress tracking
- lib/stream_store.py: New module for stream operations (deprecated)
- lib/transfer.py: Enhanced with resume_from and start_byte parameters
- lib/resume.py: Handles resumption logic and state validation
- lib/range_utils.py: Parses and validates HTTP Range headers
- lib/connection.py: Manages peer states and reconnection windows
- views/websockets.py: Added /resume endpoint for upload resumption
- views/http.py: Enhanced with Range/partial content support
- static/js/file-transfer.js: Added localStorage progress persistence
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The stream_store.py module was created but never used - all functionality was integrated directly into store.py. Also removed backup file. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Major changes: - Created Pydantic models (UploadProgress, DownloadProgress, TransferStates, ResumeInfo) for type safety and validation - Refactored Store class with specific methods instead of generic keyword-based approach - Simplified FileTransfer class by breaking down large methods into smaller, focused ones - Merged resume.py and connection.py functionality into new TransferManager class - Updated all references to use the new cleaner API Improvements: - Replaced enums with simple class constants for TransferStates - Added specific Redis methods (get_upload_progress, get_sender_state, etc.) - Improved code readability by removing deep nesting and complex conditionals - Removed unnecessary comments - code is now self-explanatory - Simplified test cases to focus on core functionality Note: Some range download tests need further work but core functionality is intact. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- 416 in case of invalid Range header - Fast hash in upload key instead of ID - Fixing tests
62f27fc to
65cee30
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
views/websockets.py:1
- The exception handler for waiting for receiver connection was removed, but the timeout handling remains. This could cause unhandled exceptions if errors other than TimeoutError occur during wait_for_client_connected().
import string
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| else: | ||
| await transfer.set_client_connected() |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The else clause after the return statement is unnecessary. The code after the if block will only execute when the condition is false, making the else clause redundant.
| else: | |
| await transfer.set_client_connected() | |
| await transfer.set_client_connected() |
| const BUFFER_THRESHOLD_MOBILE = CHUNK_SIZE_MOBILE * 16; | ||
| const BUFFER_THRESHOLD_DESKTOP = CHUNK_SIZE_DESKTOP * 16; | ||
| const MAX_HASH_SAMPLING = 2 * 1024**2; | ||
| const DEBUG_LOGS = false; |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DEBUG_LOGS is set to false in production code. Consider using an environment variable or build-time flag to control debug logging instead of hardcoding it.
| const DEBUG_LOGS = false; | |
| const DEBUG_LOGS = typeof window !== 'undefined' && window.DEBUG_LOGS === true; |
| except (ClientDisconnect, WebSocketDisconnect) as e: | ||
| self.error(f"△ Unexpected upload error: {e}") | ||
| await self.store.put_in_queue(self.DEAD_FLAG) | ||
| if chunk_count % 1 == 0 or bytes_uploaded % (16 * 1024) == 0: |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition chunk_count % 1 == 0 will always be true since any integer modulo 1 equals 0. This makes the entire condition equivalent to just the second part. Either remove this redundant check or use a meaningful modulo value like chunk_count % 10 == 0.
| if chunk_count % 1 == 0 or bytes_uploaded % (16 * 1024) == 0: | |
| if chunk_count % 10 == 0 or bytes_uploaded % (16 * 1024) == 0: |
| def generate_test_file(size_in_kb: int = 10) -> tuple[bytes, FileMetadata]: | ||
| """Generates a test file with specified size in KB.""" | ||
| chunk_generator = ((letter * 1024).encode() for letter in chain.from_iterable(repeat(ascii_letters))) | ||
| content = b''.join(next(chunk_generator) for _ in range(size_in_kb)) | ||
| chunk_generator = ((str(letter) * 32).encode() for letter in chain.from_iterable(repeat(ascii_letters))) | ||
| content = b''.join(next(chunk_generator) for _ in range(size_in_kb * 1024 // 32)) |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number 32 appears twice without explanation. Consider defining it as a constant with a descriptive name like CHUNK_PATTERN_SIZE = 32 to make the relationship between the two uses clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
static/js/file-transfer.js:1
- The condition
chunk_count % 1 == 0will always be true for any integer, making this check redundant. Consider removing it or using a different threshold likechunk_count % 10 == 0to save progress periodically.
const TRANSFER_ID_MAX_NUMBER = 1000;
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| const BUFFER_THRESHOLD_MOBILE = CHUNK_SIZE_MOBILE * 16; | ||
| const BUFFER_THRESHOLD_DESKTOP = CHUNK_SIZE_DESKTOP * 16; | ||
| const MAX_HASH_SAMPLING = 2 * 1024**2; | ||
| const DEBUG_LOGS = false; |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DEBUG_LOGS is hardcoded to false. Consider making this configurable based on environment or build configuration to avoid accidentally disabling debugging in development.
| const DEBUG_LOGS = false; | |
| const DEBUG_LOGS = ( | |
| (typeof window !== 'undefined' && window.DEBUG_LOGS === true) || | |
| (typeof window !== 'undefined' && window.location && window.location.search.includes('debug=true')) | |
| ); |
| """Set an event flag for this transfer.""" | ||
| event_key = self.key(event_name) | ||
| if last_id is not None: | ||
| min_id = f'({last_id.decode() if isinstance(last_id, bytes) else last_id}' |
Copilot
AI
Sep 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The f-string is missing a closing parenthesis. It should be f'({last_id.decode() if isinstance(last_id, bytes) else last_id})' to properly format the Redis XRANGE minimum ID.
| min_id = f'({last_id.decode() if isinstance(last_id, bytes) else last_id}' | |
| min_id = f'({last_id.decode() if isinstance(last_id, bytes) else last_id})' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ( character means that the ID range is exclusive (doesn't include start and end IDs). There shouldn't be an end parenthesis since it's not text but a specific Redis syntax.
File Transfer Backend Improvements
Storeclass, enabling more robust and resumable file transfers. Methods for chunk handling (put_chunk,get_next_chunk,get_chunk_by_range) now use streams instead of lists.lib/models.py(UploadProgress,DownloadProgress,ResumeInfo, andClientState) to support progress tracking and client state management for resumable transfers.Storeto save and retrieve upload/download progress and sender/receiver state, supporting transfer resumption and better error handling.Storeto reliably remove all transfer-related keys and prevent race conditions. [1] [2]Testing and Dependency Updates
HTTPTestClientclass with helper methods, improving test readability and maintainability. [1] [2] [3]Codebase Cleanup
lib/callbacks.pyto streamline the codebase.