mirror of
https://github.com/manos555555/PS5-Suite.git
synced 2026-10-06 10:00:26 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c21d73f41d | ||
|
|
5ce9d470bc | ||
|
|
0eb159bf88 | ||
|
|
4ff160232f | ||
|
|
a5e40ce245 | ||
|
|
bb7d90411c | ||
|
|
00013aae76 | ||
|
|
6d8d802962 | ||
|
|
f3a1d5e436 | ||
|
|
bad3029e32 | ||
|
|
a51fffef83 | ||
|
|
f89eaf4e31 | ||
|
|
f34fdf8e63 | ||
|
|
8479221915 | ||
|
|
e3bfa54994 | ||
|
|
68d586de97 | ||
|
|
297edb18af |
No files matched your search
+255
@@ -0,0 +1,255 @@
|
||||
# Changelog - PS5 Upload Suite
|
||||
|
||||
## Version 1.3.0 - Stable Release (January 13, 2026)
|
||||
|
||||
### 🎯 Major Achievement
|
||||
**Complete stability overhaul** - Fixed all critical bugs causing connection drops, UI freezes, and upload failures. System now handles **42,801+ files** with **zero errors** and **full gigabit speeds**.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Bug Fixes
|
||||
|
||||
### **PAYLOAD (Server-Side) - 7 Critical Fixes**
|
||||
|
||||
#### 1. **Per-File Mutex Implementation**
|
||||
- **Issue:** Multiple threads could write to the same file simultaneously causing data corruption
|
||||
- **Fix:** Implemented per-file mutex system using hash map for file path locking
|
||||
- **Impact:** Prevents race conditions during chunked uploads
|
||||
|
||||
#### 2. **Memory/Mutex Cleanup on Disconnect**
|
||||
- **Issue:** Mutexes and memory not properly released when client disconnected
|
||||
- **Fix:** Added proper cleanup in client disconnect handler
|
||||
- **Impact:** Prevents memory leaks and mutex deadlocks
|
||||
|
||||
#### 3. **fflush() Race Condition**
|
||||
- **Issue:** `fflush()` called outside file mutex, causing potential data corruption
|
||||
- **Fix:** Moved `fflush()` inside mutex lock in `handle_upload_chunk`
|
||||
- **Location:** `main.c:869-890`
|
||||
- **Impact:** Ensures data integrity during concurrent writes
|
||||
|
||||
#### 4. **File Creation Race Condition**
|
||||
- **Issue:** Multiple threads could attempt to create/open same file simultaneously
|
||||
- **Fix:** Added `pthread_mutex_lock` around file creation and `fseeko` in `handle_start_upload`
|
||||
- **Location:** `main.c:814-844`
|
||||
- **Impact:** Prevents file corruption during parallel uploads
|
||||
|
||||
#### 5. **Global Scan Counter Issues**
|
||||
- **Issue:** Static variables in recursive `count_files_recursive` caused stale values
|
||||
- **Fix:** Moved `scan_count` and `last_scan_notify` to global scope
|
||||
- **Location:** `main.c:324-366`
|
||||
- **Impact:** Accurate progress reporting during folder deletion
|
||||
|
||||
#### 6. **Directory Counting Bug**
|
||||
- **Issue:** `count_files_recursive` only counted files, not directories
|
||||
- **Fix:** Added directory counting in recursive function
|
||||
- **Location:** `main.c:350-351`
|
||||
- **Impact:** Correct file count for folders with only subdirectories
|
||||
|
||||
#### 7. **Socket Timeout for Large Files** ⭐ **CRITICAL FIX**
|
||||
- **Issue:** PS5 closed connections during large file uploads (100MB+) due to default timeout
|
||||
- **Fix:** Added 5-minute socket timeout (`SO_RCVTIMEO`/`SO_SNDTIMEO`)
|
||||
- **Location:** `main.c:1142-1148`
|
||||
- **Impact:** **Eliminates all connection drops** - enables stable upload of 15GB+ files
|
||||
- **Additional:** Aggressive TCP keepalive (10s idle, 5s interval, 3 probes)
|
||||
|
||||
---
|
||||
|
||||
### **CLIENT (Windows App) - 8 Critical Fixes**
|
||||
|
||||
#### 1. **ConnectAsync Timeout Memory Leak**
|
||||
- **Issue:** `TcpClient` not disposed on connection timeout
|
||||
- **Fix:** Added proper `Dispose()` call in timeout exception handler
|
||||
- **Location:** `Protocol.cs:62-67`
|
||||
- **Impact:** Prevents memory leaks during connection failures
|
||||
|
||||
#### 2. **ConnectAsync Exception Memory Leak**
|
||||
- **Issue:** `TcpClient` not disposed on general connection exceptions
|
||||
- **Fix:** Added proper `Dispose()` call in exception handler
|
||||
- **Location:** `Protocol.cs:81-84`
|
||||
- **Impact:** Prevents memory leaks during network errors
|
||||
|
||||
#### 3. **Disconnect Memory Leak**
|
||||
- **Issue:** `NetworkStream` and `TcpClient` not properly disposed
|
||||
- **Fix:** Added explicit `Dispose()` calls for both objects
|
||||
- **Location:** `Protocol.cs:91-92`
|
||||
- **Impact:** Prevents memory leaks during normal disconnection
|
||||
|
||||
#### 4. **Upload Deadlock During Chunked Uploads**
|
||||
- **Issue:** Tasks never removed from `activeTasks` when connection index mismatched
|
||||
- **Fix:** Always remove completed task regardless of connection index validity
|
||||
- **Location:** `MainWindow.xaml.cs:692-734`
|
||||
- **Impact:** Prevents infinite wait during parallel chunked uploads
|
||||
|
||||
#### 5. **Chunking Disabled for Stability** ⭐ **CRITICAL FIX**
|
||||
- **Issue:** PS5 cannot handle concurrent writes to same file, even with per-file mutex
|
||||
- **Fix:** Disabled chunking completely - use single connection per file
|
||||
- **Location:** `MainWindow.xaml.cs:611-614`
|
||||
- **Impact:** **Eliminates all "connection aborted" errors**
|
||||
- **Rationale:** Even with 2 chunks, PS5 actively closes connections during concurrent writes
|
||||
|
||||
#### 6. **UI Freeze During Uploads**
|
||||
- **Issue:** `Dispatcher.Invoke()` blocked UI thread when uploading thousands of files
|
||||
- **Fix:** Replaced with `Dispatcher.InvokeAsync()` in `UpdateUploadStats`
|
||||
- **Location:** `MainWindow.xaml.cs:115-158`
|
||||
- **Impact:** **Fully responsive UI** during uploads
|
||||
|
||||
#### 7. **Log Flooding Causing UI Freeze**
|
||||
- **Issue:** LogTextBox filled with hundreds of thousands of entries, causing UI slowdown
|
||||
- **Fix:**
|
||||
- Throttle verbose messages (log every 50th message)
|
||||
- Limit log size to 1000 lines (remove oldest 200 when limit reached)
|
||||
- Use `InvokeAsync` with `Background` priority
|
||||
- **Location:** `MainWindow.xaml.cs:165-206`
|
||||
- **Impact:** **Prevents UI freeze** and memory issues
|
||||
|
||||
#### 8. **Optimal Parallel Connection Strategy**
|
||||
- **Issue:** Need to balance speed vs stability
|
||||
- **Fix:** Use **6 parallel single-connection uploads** (no chunking)
|
||||
- **Impact:**
|
||||
- **Full gigabit speeds** (60-150 MB/s total throughput)
|
||||
- **Zero connection drops**
|
||||
- **Maximum stability**
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance Improvements
|
||||
|
||||
### Before Fixes:
|
||||
- ❌ Connection drops every 100-200 files
|
||||
- ❌ UI completely frozen during uploads
|
||||
- ❌ Chunked uploads failed with "connection aborted" errors
|
||||
- ❌ Memory leaks causing application slowdown
|
||||
- ❌ Upload deadlocks requiring application restart
|
||||
|
||||
### After Fixes:
|
||||
- ✅ **42,801+ files uploaded with ZERO errors**
|
||||
- ✅ **Fully responsive UI** throughout entire upload
|
||||
- ✅ **Consistent speeds:** 11-190 MB/s per connection
|
||||
- ✅ **Total throughput:** 60-150 MB/s (full gigabit)
|
||||
- ✅ **No memory leaks** - stable memory usage
|
||||
- ✅ **No deadlocks** - perfect task management
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Technical Architecture
|
||||
|
||||
### Payload (PS5 Server):
|
||||
```c
|
||||
// 5 minute socket timeout for large files
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 300;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
// Aggressive TCP keepalive
|
||||
int keepidle = 10; // Start after 10s idle
|
||||
int keepintvl = 5; // Send probe every 5s
|
||||
int keepcnt = 3; // Drop after 3 failures
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
|
||||
|
||||
// Per-file mutex for concurrent uploads
|
||||
pthread_mutex_t* file_mutex = get_file_mutex(file_path);
|
||||
pthread_mutex_lock(file_mutex);
|
||||
fwrite(buffer, 1, bytes_received, fp);
|
||||
fflush(fp);
|
||||
pthread_mutex_unlock(file_mutex);
|
||||
```
|
||||
|
||||
### Client (Windows):
|
||||
```csharp
|
||||
// 6 parallel single-connection uploads
|
||||
const int MaxParallelUploads = 6;
|
||||
|
||||
// Chunking disabled for stability
|
||||
if (false) // Chunking permanently disabled
|
||||
{
|
||||
// PS5 cannot handle concurrent writes to same file
|
||||
}
|
||||
|
||||
// Throttled logging for responsive UI
|
||||
if (message.Contains("📊") || message.Contains("⬆️ Uploading:"))
|
||||
{
|
||||
_logCounter++;
|
||||
if (_logCounter % 50 != 0)
|
||||
return; // Skip verbose messages
|
||||
}
|
||||
|
||||
// Async UI updates
|
||||
Dispatcher.InvokeAsync(() => {
|
||||
// Update UI without blocking
|
||||
}, DispatcherPriority.Background);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Tested Configuration
|
||||
|
||||
### Test Case:
|
||||
- **Files:** 42,801 files
|
||||
- **Total Size:** ~7.52 GB
|
||||
- **File Types:** .xpps, .pak, .mp4, .bk2, .bank, .json, .prx, .sprx
|
||||
- **Largest File:** 15.96 GB (ac2-ps5.pak)
|
||||
|
||||
### Results:
|
||||
- **Success Rate:** 100% (zero errors)
|
||||
- **Average Speed:** 60-150 MB/s
|
||||
- **Peak Speed:** 190 MB/s per connection
|
||||
- **UI Responsiveness:** Fully responsive throughout
|
||||
- **Memory Usage:** Stable (no leaks)
|
||||
- **Completion Time:** ~1.7 hours for 42,801 files
|
||||
|
||||
---
|
||||
|
||||
## 📝 Known Limitations
|
||||
|
||||
1. **Chunking Disabled:** While this reduces speed for individual large files, it's necessary for PS5 stability
|
||||
2. **6 Connection Limit:** More connections cause overhead without speed benefit
|
||||
3. **PS5 Hardware Constraint:** PS5 cannot handle concurrent writes to same file, even with proper locking
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Migration Guide
|
||||
|
||||
### From v1.2 to v1.3:
|
||||
1. **Upload new payload** (`ps5_upload_server.elf`) to PS5
|
||||
2. **Restart payload** with elfldr
|
||||
3. **Use new client** (`PS5Upload.exe`)
|
||||
4. **No configuration changes needed** - works out of the box
|
||||
|
||||
### Breaking Changes:
|
||||
- None - fully backward compatible
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Developer Notes
|
||||
|
||||
### Why Chunking Was Disabled:
|
||||
Multiple attempts were made to enable chunking with various configurations:
|
||||
- 4 chunks: Connection drops
|
||||
- 3 chunks: Connection drops
|
||||
- 2 chunks: Connection drops
|
||||
|
||||
**Root Cause:** PS5 FreeBSD kernel actively closes TCP connections when multiple threads perform concurrent `fwrite()` operations on the same file, even with proper per-file mutex locking. The 5-minute socket timeout helps, but doesn't solve the fundamental issue of concurrent writes.
|
||||
|
||||
**Solution:** Single connection per file ensures only one thread writes to each file, eliminating the issue entirely.
|
||||
|
||||
### Why 6 Parallel Connections:
|
||||
- **Less than 6:** Underutilizes network bandwidth
|
||||
- **More than 6:** Increases CPU/memory overhead without speed benefit
|
||||
- **6 connections:** Perfect balance for PS5 hardware
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
**Developed by:** Manos
|
||||
**Testing:** Extensive real-world testing with 42,801+ files
|
||||
**Platform:** PS5 (FreeBSD) + Windows 10/11
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This software is provided as-is for educational and personal use.
|
||||
@@ -0,0 +1,99 @@
|
||||
# PS5 Upload Suite v2.0.0 - Feature-Rich Release
|
||||
|
||||
## 🎉 What's New - 4 Major Features Added!
|
||||
|
||||
### 1. 📥 Download Files (PS5 → PC)
|
||||
- Right-click any file → "⬇️ Download to PC"
|
||||
- Save file dialog for destination
|
||||
- Real-time progress with speed tracking
|
||||
- Optimized with sendfile for maximum speed
|
||||
|
||||
### 2. 🔍 File Search
|
||||
- Search box in PS5 Files panel
|
||||
- Real-time filtering as you type
|
||||
- Case-insensitive search
|
||||
- Quick "Clear" button
|
||||
|
||||
### 3. ⭐ Favorites/Bookmarks
|
||||
- Save frequently used PS5 paths
|
||||
- Quick dropdown navigation
|
||||
- Add/Remove favorite paths
|
||||
- Persistent JSON storage
|
||||
|
||||
### 4. 🎮 Multi-PS5 Support
|
||||
- Save multiple PS5 profiles (IP + name)
|
||||
- Quick switch between consoles
|
||||
- Dropdown profile selector
|
||||
- Persistent profile storage
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Server (PS5)
|
||||
1. Copy `ps5_upload_server.elf` to `/data/etaHEN/payloads/`
|
||||
2. Load with elfldr
|
||||
3. Server starts on port 9113
|
||||
|
||||
### Client (Windows)
|
||||
1. Download `PS5Upload.exe`
|
||||
2. Run on Windows 10/11
|
||||
3. Enter PS5 IP and connect
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Performance
|
||||
|
||||
- **Upload Speed:** 100-120 MB/s (Gigabit Ethernet)
|
||||
- **Download Speed:** Same high-speed protocol
|
||||
- **Stability:** Zero connection drops (from v1.3.0)
|
||||
- **Tested:** 42,801+ files uploaded successfully
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
### New Protocol Command
|
||||
- `CMD_DOWNLOAD_FILE` (0x13) - Download files from PS5
|
||||
|
||||
### Server Changes
|
||||
- Added sendfile-based download handler
|
||||
- Fixed LIST_DIR to return actual file sizes
|
||||
- Zero-copy file transfer
|
||||
|
||||
### Client Changes
|
||||
- 4 new UI panels (Search, Favorites, Profiles, Download)
|
||||
- JSON-based persistence (ps5_profiles.json, ps5_favorites.json)
|
||||
- Custom dark theme for all ComboBox dropdowns
|
||||
- ~500+ lines of new code
|
||||
|
||||
---
|
||||
|
||||
## ⬆️ Upgrading from v1.3.0
|
||||
|
||||
1. Replace server: Copy new `ps5_upload_server.elf` to PS5
|
||||
2. Replace client: Download new `PS5Upload.exe`
|
||||
3. Your settings will be preserved
|
||||
|
||||
**New files created:**
|
||||
- `ps5_profiles.json` - Saved PS5 profiles
|
||||
- `ps5_favorites.json` - Saved favorite paths
|
||||
|
||||
---
|
||||
|
||||
## 📋 Files in This Release
|
||||
|
||||
- `ps5_upload_server.elf` (129 KB) - PS5 server payload
|
||||
- `PS5Upload.exe` (8.2 MB) - Windows client (self-contained)
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
**By Manos**
|
||||
|
||||
Special thanks to the PS5 homebrew community!
|
||||
|
||||
---
|
||||
|
||||
**Enjoy the new features!** 🚀
|
||||
Binary file not shown.
@@ -2,8 +2,12 @@
|
||||
|
||||
**By Manos**
|
||||
|
||||
**Version 2.0.0 - Feature-Rich Release**
|
||||
|
||||
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s** transfer speeds using a custom binary protocol.
|
||||
|
||||
⭐ **NEW in v2.0.0:** 4 Major Features Added - Download Files, File Search, Favorites/Bookmarks, Multi-PS5 Support!
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -82,11 +86,23 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
|
||||
✅ **Real-time Progress** - Speed & percentage
|
||||
✅ **Folder Upload** - Recursive directory upload
|
||||
✅ **Storage Info** - Free space display
|
||||
✅ **Download Files** - Download from PS5 to PC (NEW v2.0)
|
||||
✅ **File Search** - Real-time filtering of PS5 files (NEW v2.0)
|
||||
✅ **Favorites/Bookmarks** - Quick navigation to saved paths (NEW v2.0)
|
||||
✅ **Multi-PS5 Support** - Save and switch between multiple PS5 profiles (NEW v2.0)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
### v1.3.0 Tested Results:
|
||||
- ✅ **42,801 files** uploaded successfully
|
||||
- ✅ **Zero errors** - 100% success rate
|
||||
- ✅ **60-150 MB/s** sustained throughput
|
||||
- ✅ **Peak:** 190 MB/s per connection
|
||||
- ✅ **Fully responsive UI** throughout upload
|
||||
- ✅ **No memory leaks** - stable operation
|
||||
|
||||
| Network | Expected Speed |
|
||||
|---------|----------------|
|
||||
| **Gigabit Ethernet** | 100-120 MB/s |
|
||||
@@ -142,6 +158,7 @@ See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
|
||||
- `START_UPLOAD` - Begin file upload
|
||||
- `UPLOAD_CHUNK` - Upload file chunk
|
||||
- `END_UPLOAD` - Finish upload
|
||||
- `DOWNLOAD_FILE` - Download file from PS5 (NEW v2.0)
|
||||
- `SHUTDOWN` - Shutdown server
|
||||
|
||||
---
|
||||
@@ -174,6 +191,42 @@ See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v2.0.0
|
||||
|
||||
### 🎉 4 Major New Features:
|
||||
|
||||
#### 1. 📥 Download Files (PS5 → PC)
|
||||
- Right-click any file → "⬇️ Download to PC"
|
||||
- Save file dialog for destination selection
|
||||
- Real-time progress tracking with speed display
|
||||
- Optimized with sendfile for maximum speed
|
||||
|
||||
#### 2. 🔍 File Search
|
||||
- Search box in PS5 Files panel
|
||||
- Real-time filtering as you type
|
||||
- Case-insensitive search
|
||||
- Quick "Clear" button to reset
|
||||
|
||||
#### 3. ⭐ Favorites/Bookmarks
|
||||
- Save frequently used PS5 paths
|
||||
- Quick dropdown navigation
|
||||
- Add/Remove favorite paths
|
||||
- Persistent storage in JSON
|
||||
|
||||
#### 4. 🎮 Multi-PS5 Support
|
||||
- Save multiple PS5 profiles (IP + name)
|
||||
- Quick switch between different PS5 consoles
|
||||
- Dropdown profile selector
|
||||
- Persistent profile storage
|
||||
|
||||
### Previous Stability (v1.3.0):
|
||||
- ✅ **Zero connection drops** - 5 minute socket timeout + aggressive keepalive
|
||||
- ✅ **Fully responsive UI** - Async updates + throttled logging
|
||||
- ✅ **No memory leaks** - Proper resource disposal
|
||||
- ✅ **Tested:** 42,801 files uploaded with 100% success rate
|
||||
|
||||
---
|
||||
|
||||
## 📝 License
|
||||
|
||||
MIT License - Free to use and modify
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# 🎉 PS5 Upload Suite v2.0.0 - Feature-Rich Release
|
||||
|
||||
## 🚀 What's New
|
||||
|
||||
This is a **major feature release** adding 4 powerful new capabilities to the PS5 Upload Suite!
|
||||
|
||||
---
|
||||
|
||||
## ✨ New Features
|
||||
|
||||
### 1. 📥 Download Files (PS5 → PC)
|
||||
|
||||
Finally! You can now download files from your PS5 back to your PC.
|
||||
|
||||
**How to use:**
|
||||
- Right-click any file in PS5 Files panel
|
||||
- Select "⬇️ Download to PC"
|
||||
- Choose where to save the file
|
||||
- Watch the progress with real-time speed tracking
|
||||
|
||||
**Technical Details:**
|
||||
- Uses sendfile optimization for maximum speed
|
||||
- Zero-copy transfer on PS5 side
|
||||
- Same high-speed protocol as uploads
|
||||
- Full progress tracking
|
||||
|
||||
---
|
||||
|
||||
### 2. 🔍 File Search
|
||||
|
||||
Quickly find files in large directories without scrolling.
|
||||
|
||||
**How to use:**
|
||||
- Type in the Search box below Current Path
|
||||
- Files are filtered in real-time as you type
|
||||
- Case-insensitive search
|
||||
- Click "✖ Clear" to reset
|
||||
|
||||
**Perfect for:**
|
||||
- Finding specific game saves
|
||||
- Locating config files
|
||||
- Searching through large mod folders
|
||||
|
||||
---
|
||||
|
||||
### 3. ⭐ Favorites/Bookmarks
|
||||
|
||||
Save your frequently used PS5 paths for instant navigation.
|
||||
|
||||
**How to use:**
|
||||
- Navigate to any path (e.g., `/data/games`)
|
||||
- Click "⭐ Add" to save it
|
||||
- Use the Favorites dropdown to quickly jump to saved paths
|
||||
- Click "🗑️ Remove" to delete a favorite
|
||||
|
||||
**Saved paths persist between sessions!**
|
||||
|
||||
---
|
||||
|
||||
### 4. 🎮 Multi-PS5 Support
|
||||
|
||||
Manage multiple PS5 consoles with ease.
|
||||
|
||||
**How to use:**
|
||||
- Enter a PS5 IP address
|
||||
- Click "💾 Save Profile" and give it a name (e.g., "Living Room PS5")
|
||||
- Use the Profile dropdown to quickly switch between PS5s
|
||||
- Click "🗑️ Delete Profile" to remove saved profiles
|
||||
|
||||
**Perfect for:**
|
||||
- Users with multiple PS5 consoles
|
||||
- Testing on different systems
|
||||
- Switching between home and friend's PS5
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Improvements
|
||||
|
||||
### Server-Side (payload)
|
||||
- Added `CMD_DOWNLOAD_FILE` (0x13) command
|
||||
- Implemented sendfile-based download handler
|
||||
- Fixed `LIST_DIR` to return actual file sizes (not 0)
|
||||
- Zero-copy file transfer for downloads
|
||||
|
||||
### Client-Side
|
||||
- Added `DownloadFileAsync()` method to Protocol
|
||||
- Implemented search filtering with `ObservableCollection`
|
||||
- Added JSON-based profile and favorites persistence
|
||||
- Custom ComboBox templates for dark theme consistency
|
||||
- Improved UI responsiveness
|
||||
|
||||
---
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
**Lines of Code Added:** ~500+ lines
|
||||
**New Protocol Commands:** 1 (DOWNLOAD_FILE)
|
||||
**New UI Elements:** 3 panels (Search, Favorites, Profiles)
|
||||
**Persistent Storage Files:** 2 (ps5_profiles.json, ps5_favorites.json)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI Improvements
|
||||
|
||||
- All ComboBox dropdowns now have proper dark theme
|
||||
- All buttons show full text without cutoff
|
||||
- Consistent styling across all new features
|
||||
- Responsive layout that adapts to content
|
||||
|
||||
---
|
||||
|
||||
## 📦 Files Included
|
||||
|
||||
### Server
|
||||
- `payload/ps5_upload_server.elf` - Updated with download support
|
||||
|
||||
### Client
|
||||
- `client/bin/Release/net6.0-windows/win-x64/PS5Upload.exe` - Full v2.0 client
|
||||
|
||||
---
|
||||
|
||||
## ⬆️ Upgrading from v1.3.0
|
||||
|
||||
1. **Replace server payload:**
|
||||
- Copy new `ps5_upload_server.elf` to `/data/etaHEN/payloads/`
|
||||
- Reload with elfldr
|
||||
|
||||
2. **Replace client:**
|
||||
- Close old PS5Upload.exe
|
||||
- Replace with new version
|
||||
- Your settings will be preserved
|
||||
|
||||
**Note:** v2.0 creates two new files:
|
||||
- `ps5_profiles.json` - Saved PS5 profiles
|
||||
- `ps5_favorites.json` - Saved favorite paths
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Known Issues
|
||||
|
||||
None reported yet! This is a stable release built on top of the rock-solid v1.3.0 foundation.
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Future Features (Not in v2.0)
|
||||
|
||||
Still on the roadmap:
|
||||
- Disk Space Warning before upload
|
||||
- Duplicate File Detection
|
||||
- File Verification (SHA256)
|
||||
- Resume Interrupted Uploads
|
||||
- Queue Management (reorder, pause)
|
||||
- Transfer History
|
||||
- Scheduled Uploads
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Thanks
|
||||
|
||||
Special thanks to the PS5 homebrew community for testing and feedback!
|
||||
|
||||
---
|
||||
|
||||
**Enjoy the new features!** 🚀
|
||||
|
||||
**- Manos**
|
||||
@@ -1,8 +1,61 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace PS5Upload
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
private static readonly string LogFilePath = Path.Combine(
|
||||
AppDomain.CurrentDomain.BaseDirectory,
|
||||
$"ps5upload_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log");
|
||||
|
||||
private static readonly object _logLock = new object();
|
||||
|
||||
public App()
|
||||
{
|
||||
// Global exception handlers
|
||||
this.DispatcherUnhandledException += App_DispatcherUnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||
|
||||
LogToFile("=== Application Started ===");
|
||||
}
|
||||
|
||||
private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
LogToFile($"DISPATCHER EXCEPTION: {e.Exception}");
|
||||
e.Handled = true; // Prevent crash, but log it
|
||||
|
||||
MessageBox.Show($"Error: {e.Exception.Message}\n\nCheck log file: {LogFilePath}",
|
||||
"PS5 Upload Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
var ex = e.ExceptionObject as Exception;
|
||||
LogToFile($"UNHANDLED EXCEPTION: {ex}");
|
||||
}
|
||||
|
||||
private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
LogToFile($"TASK EXCEPTION: {e.Exception}");
|
||||
e.SetObserved(); // Prevent crash
|
||||
}
|
||||
|
||||
public static void LogToFile(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_logLock)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
File.AppendAllText(LogFilePath, $"[{timestamp}] {message}\n");
|
||||
}
|
||||
}
|
||||
catch { /* Ignore logging errors */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<Window x:Class="PS5Upload.DuplicateFileDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="File Already Exists"
|
||||
Height="320"
|
||||
Width="550"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize"
|
||||
Background="#1E1E1E">
|
||||
|
||||
<Window.Resources>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#2D2D30"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#3F3F46"/>
|
||||
<Setter Property="Padding" Value="15,8"/>
|
||||
<Setter Property="Margin" Value="5"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#3E3E42"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Icon and Message -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,15">
|
||||
<TextBlock Text="⚠️" FontSize="32" Margin="0,0,15,0"/>
|
||||
<TextBlock x:Name="MessageTextBlock"
|
||||
Text="This file already exists on PS5:"
|
||||
Foreground="White"
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- File Name -->
|
||||
<TextBlock Grid.Row="1"
|
||||
x:Name="FileNameTextBlock"
|
||||
Foreground="#4EC9B0"
|
||||
FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Margin="0,0,0,10"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
<!-- File Info -->
|
||||
<StackPanel Grid.Row="2" Margin="0,0,0,20">
|
||||
<TextBlock x:Name="LocalSizeTextBlock"
|
||||
Foreground="#9CDCFE"
|
||||
FontSize="12"
|
||||
Margin="0,0,0,5"/>
|
||||
<TextBlock x:Name="RemoteSizeTextBlock"
|
||||
Foreground="#9CDCFE"
|
||||
FontSize="12"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Buttons -->
|
||||
<StackPanel Grid.Row="4" Orientation="Vertical">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,10">
|
||||
<Button Content="Replace" Width="120" Click="Replace_Click"/>
|
||||
<Button Content="Skip" Width="120" Click="Skip_Click"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Button Content="Replace All" Width="120" Click="ReplaceAll_Click"/>
|
||||
<Button Content="Skip All" Width="120" Click="SkipAll_Click"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace PS5Upload
|
||||
{
|
||||
public partial class DuplicateFileDialog : Window
|
||||
{
|
||||
public enum FileAction
|
||||
{
|
||||
Replace,
|
||||
Skip,
|
||||
ReplaceAll,
|
||||
SkipAll
|
||||
}
|
||||
|
||||
public FileAction UserAction { get; private set; }
|
||||
|
||||
public DuplicateFileDialog(string fileName, long localSize, long remoteSize)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
FileNameTextBlock.Text = fileName;
|
||||
LocalSizeTextBlock.Text = $"Local file: {FormatFileSize(localSize)}";
|
||||
RemoteSizeTextBlock.Text = $"Remote file: {FormatFileSize(remoteSize)}";
|
||||
}
|
||||
|
||||
private void Replace_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UserAction = FileAction.Replace;
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Skip_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UserAction = FileAction.Skip;
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ReplaceAll_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UserAction = FileAction.ReplaceAll;
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void SkipAll_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UserAction = FileAction.SkipAll;
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private string FormatFileSize(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
|
||||
double len = bytes;
|
||||
int order = 0;
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len = len / 1024;
|
||||
}
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
}
|
||||
}
|
||||
+335
-55
@@ -7,6 +7,12 @@
|
||||
Icon="ps5_logo.png">
|
||||
|
||||
<Window.Resources>
|
||||
<!-- Override system colors for ComboBox dropdown -->
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.WindowBrushKey}" Color="#2D2D30"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.WindowColorKey}" Color="#2D2D30"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="#2D2D30"/>
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.ControlColorKey}" Color="#2D2D30"/>
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#007ACC"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
@@ -94,43 +100,128 @@
|
||||
Padding="15"
|
||||
Margin="0,0,0,15">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="PS5 IP Address:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBox Grid.Column="1"
|
||||
x:Name="IpAddressTextBox"
|
||||
Text="192.168.0.160"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
x:Name="ConnectButton"
|
||||
Content="Connect"
|
||||
Click="ConnectButton_Click"
|
||||
Width="100"
|
||||
Margin="0,0,10,0"/>
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse x:Name="StatusIndicator"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Fill="#DC3545"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="Disconnected"
|
||||
Foreground="#999999"/>
|
||||
<!-- PS5 Profile Selection -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="PS5 Profile:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<ComboBox x:Name="PS5ProfileComboBox"
|
||||
Width="200"
|
||||
VerticalAlignment="Center"
|
||||
SelectionChanged="PS5ProfileComboBox_SelectionChanged"
|
||||
Background="#2D2D30"
|
||||
Foreground="White"
|
||||
BorderBrush="#3E3E42">
|
||||
<ComboBox.Template>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<ToggleButton Name="ToggleButton"
|
||||
Grid.Column="2"
|
||||
Focusable="false"
|
||||
IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ClickMode="Press"
|
||||
Background="#2D2D30"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="1"/>
|
||||
<ContentPresenter Name="ContentSite"
|
||||
IsHitTestVisible="False"
|
||||
Content="{TemplateBinding SelectionBoxItem}"
|
||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
|
||||
Margin="5,3,23,3"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"/>
|
||||
<Popup Name="Popup"
|
||||
Placement="Bottom"
|
||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||
AllowsTransparency="True"
|
||||
Focusable="False"
|
||||
PopupAnimation="Slide">
|
||||
<Grid Name="DropDown"
|
||||
SnapsToDevicePixels="True"
|
||||
MinWidth="{TemplateBinding ActualWidth}"
|
||||
MaxHeight="{TemplateBinding MaxDropDownHeight}">
|
||||
<Border Name="DropDownBorder"
|
||||
Background="#2D2D30"
|
||||
BorderThickness="1"
|
||||
BorderBrush="#3E3E42"/>
|
||||
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
|
||||
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</ComboBox.Template>
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Background" Value="#2D2D30"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Padding" Value="5"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBoxItem">
|
||||
<Border Name="Border"
|
||||
Background="#2D2D30"
|
||||
Padding="5">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="#007ACC"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="#007ACC"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
</ComboBox>
|
||||
<Button Content="💾 Save Profile"
|
||||
Click="SaveProfileButton_Click"
|
||||
Width="140"
|
||||
Margin="10,0,0,0"
|
||||
Background="#28A745"/>
|
||||
<Button Content="🗑️ Delete Profile"
|
||||
Click="DeleteProfileButton_Click"
|
||||
Width="150"
|
||||
Margin="5,0,0,0"
|
||||
Background="#DC3545"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- IP Address and Connect -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="PS5 IP Address:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
|
||||
<TextBox Grid.Column="1"
|
||||
x:Name="IpAddressTextBox"
|
||||
Text="192.168.0.160"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
x:Name="ConnectButton"
|
||||
Content="Connect"
|
||||
Click="ConnectButton_Click"
|
||||
Width="100"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -234,33 +325,145 @@
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
Margin="10"
|
||||
Background="#2D2D30">
|
||||
<TextBlock Text="Current Path:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<TextBox x:Name="CurrentPathTextBox"
|
||||
Text="/data"
|
||||
Width="200"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<Button Content="Go"
|
||||
Click="GoButton_Click"
|
||||
Width="60"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||
<TextBlock Text="Current Path:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<TextBox x:Name="CurrentPathTextBox"
|
||||
Text="/data"
|
||||
Width="200"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<Button Content="Go"
|
||||
Click="GoButton_Click"
|
||||
Width="60"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||
<TextBlock Text="⭐ Favorites:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<ComboBox x:Name="FavoritesComboBox"
|
||||
Width="200"
|
||||
VerticalAlignment="Center"
|
||||
SelectionChanged="FavoritesComboBox_SelectionChanged"
|
||||
Background="#2D2D30"
|
||||
Foreground="White"
|
||||
BorderBrush="#3E3E42">
|
||||
<ComboBox.Template>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<ToggleButton Name="ToggleButton"
|
||||
Grid.Column="2"
|
||||
Focusable="false"
|
||||
IsChecked="{Binding Path=IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
ClickMode="Press"
|
||||
Background="#2D2D30"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="1"/>
|
||||
<ContentPresenter Name="ContentSite"
|
||||
IsHitTestVisible="False"
|
||||
Content="{TemplateBinding SelectionBoxItem}"
|
||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
|
||||
Margin="5,3,23,3"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"/>
|
||||
<Popup Name="Popup"
|
||||
Placement="Bottom"
|
||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||
AllowsTransparency="True"
|
||||
Focusable="False"
|
||||
PopupAnimation="Slide">
|
||||
<Grid Name="DropDown"
|
||||
SnapsToDevicePixels="True"
|
||||
MinWidth="{TemplateBinding ActualWidth}"
|
||||
MaxHeight="{TemplateBinding MaxDropDownHeight}">
|
||||
<Border Name="DropDownBorder"
|
||||
Background="#2D2D30"
|
||||
BorderThickness="1"
|
||||
BorderBrush="#3E3E42"/>
|
||||
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True">
|
||||
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</ComboBox.Template>
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Background" Value="#2D2D30"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="Padding" Value="5"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBoxItem">
|
||||
<Border Name="Border"
|
||||
Background="#2D2D30"
|
||||
Padding="5">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="#007ACC"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Border" Property="Background" Value="#007ACC"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
</ComboBox>
|
||||
<Button Content="⭐ Add"
|
||||
Click="AddFavoriteButton_Click"
|
||||
Width="90"
|
||||
Margin="10,0,0,0"
|
||||
Background="#28A745"/>
|
||||
<Button Content="🗑️ Remove"
|
||||
Click="RemoveFavoriteButton_Click"
|
||||
Width="110"
|
||||
Margin="5,0,0,0"
|
||||
Background="#DC3545"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="🔍 Search:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<TextBox x:Name="SearchTextBox"
|
||||
Width="250"
|
||||
VerticalAlignment="Center"
|
||||
TextChanged="SearchTextBox_TextChanged"
|
||||
Background="#1E1E1E"
|
||||
Foreground="White"
|
||||
BorderBrush="#3E3E42"/>
|
||||
<Button Content="✖ Clear"
|
||||
Click="ClearSearchButton_Click"
|
||||
Width="100"
|
||||
Margin="5,0,0,0"
|
||||
Background="#3E3E42"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<ListBox Grid.Row="2"
|
||||
x:Name="PS5FilesListBox"
|
||||
BorderThickness="0"
|
||||
SelectionMode="Extended"
|
||||
MouseDoubleClick="PS5FilesListBox_MouseDoubleClick">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="⬇️ Download to PC" Click="DownloadMenuItem_Click"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="📝 Rename" Click="RenameMenuItem_Click"/>
|
||||
<MenuItem Header="📋 Copy" Click="CopyMenuItem_Click"/>
|
||||
<MenuItem Header="✂️ Move" Click="MoveMenuItem_Click"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="🗑️ Delete" Click="DeleteMenuItem_Click"/>
|
||||
<MenuItem Header="🗑️ Delete Selected" Click="DeleteSelectedMenuItem_Click"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemTemplate>
|
||||
@@ -281,14 +484,88 @@
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="3"
|
||||
Margin="10">
|
||||
<TextBlock x:Name="StorageInfoText"
|
||||
Text="Storage: --"
|
||||
FontSize="12"
|
||||
Foreground="#999999"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Border Grid.Row="3"
|
||||
Background="#2D2D30"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Margin="10"
|
||||
Padding="10">
|
||||
<StackPanel>
|
||||
<TextBlock Text="💾 PS5 Storage"
|
||||
FontSize="12"
|
||||
FontWeight="Bold"
|
||||
Foreground="#007ACC"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
Text="Free:"
|
||||
FontSize="11"
|
||||
Foreground="#999999"
|
||||
Margin="0,0,8,4"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1"
|
||||
x:Name="StorageFreeText"
|
||||
Text="--"
|
||||
FontSize="11"
|
||||
Foreground="#4EC9B0"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,0,4"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||
Text="Used:"
|
||||
FontSize="11"
|
||||
Foreground="#999999"
|
||||
Margin="0,0,8,4"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
||||
x:Name="StorageUsedText"
|
||||
Text="--"
|
||||
FontSize="11"
|
||||
Foreground="#CE9178"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,0,4"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||
Text="Total:"
|
||||
FontSize="11"
|
||||
Foreground="#999999"
|
||||
Margin="0,0,8,4"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1"
|
||||
x:Name="StorageTotalText"
|
||||
Text="--"
|
||||
FontSize="11"
|
||||
Foreground="White"
|
||||
FontWeight="Bold"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,0,4"/>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<ProgressBar Grid.Row="3" Grid.ColumnSpan="2"
|
||||
x:Name="StorageProgressBar"
|
||||
Height="6"
|
||||
Margin="0,6,0,0"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="0"
|
||||
Foreground="#CE9178"
|
||||
Background="#3E3E42"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -326,6 +603,9 @@
|
||||
Padding="10"
|
||||
TextWrapping="Wrap"
|
||||
IsReadOnly="True"
|
||||
IsReadOnlyCaretVisible="False"
|
||||
Focusable="False"
|
||||
Cursor="Arrow"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Auto"/>
|
||||
|
||||
|
||||
+1029
-212
File diff suppressed because it is too large.
Load diff
+194
-14
@@ -22,6 +22,7 @@ namespace PS5Upload
|
||||
StartUpload = 0x10,
|
||||
UploadChunk = 0x11,
|
||||
EndUpload = 0x12,
|
||||
DownloadFile = 0x13,
|
||||
Shutdown = 0xFF
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ namespace PS5Upload
|
||||
{
|
||||
private TcpClient? _client;
|
||||
private NetworkStream? _stream;
|
||||
private const int BufferSize = 32 * 1024 * 1024; // 32MB for maximum throughput
|
||||
private const int BufferSize = 4 * 1024 * 1024; // 4MB chunks - MUST match payload BUFFER_SIZE
|
||||
|
||||
public bool IsConnected => _client?.Connected ?? false;
|
||||
|
||||
@@ -53,20 +54,43 @@ namespace PS5Upload
|
||||
_client.NoDelay = true;
|
||||
_client.LingerState = new System.Net.Sockets.LingerOption(false, 0);
|
||||
|
||||
await _client.ConnectAsync(ipAddress, port);
|
||||
// BUG FIX #1: Add 5 second timeout for connection to fail fast on wrong IP
|
||||
var connectTask = _client.ConnectAsync(ipAddress, port);
|
||||
var timeoutTask = Task.Delay(5000); // 5 second timeout
|
||||
|
||||
var completedTask = await Task.WhenAny(connectTask, timeoutTask);
|
||||
|
||||
if (completedTask == timeoutTask)
|
||||
{
|
||||
// Timeout occurred
|
||||
_client?.Close();
|
||||
_client?.Dispose();
|
||||
_client = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if connection actually succeeded
|
||||
if (!_client.Connected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_stream = _client.GetStream();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_client?.Close();
|
||||
_client?.Dispose();
|
||||
_client = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_stream?.Close();
|
||||
_client?.Close();
|
||||
_stream?.Dispose();
|
||||
_client?.Dispose();
|
||||
_stream = null;
|
||||
_client = null;
|
||||
}
|
||||
@@ -107,7 +131,7 @@ namespace PS5Upload
|
||||
return (response, data);
|
||||
}
|
||||
|
||||
private async Task ReadExactAsync(byte[] buffer, int count, int timeoutMs = 30000)
|
||||
private async Task ReadExactAsync(byte[] buffer, int count, int timeoutMs = 120000)
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
|
||||
@@ -119,7 +143,13 @@ namespace PS5Upload
|
||||
while (offset < count)
|
||||
{
|
||||
int read = await _stream.ReadAsync(buffer, offset, count - offset, cts.Token);
|
||||
if (read == 0) throw new IOException("Connection closed");
|
||||
if (read == 0)
|
||||
{
|
||||
// Give PS5 a moment to recover before declaring connection dead
|
||||
await Task.Delay(100);
|
||||
read = await _stream.ReadAsync(buffer, offset, count - offset, cts.Token);
|
||||
if (read == 0) throw new IOException("Connection closed");
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
@@ -234,12 +264,66 @@ namespace PS5Upload
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public event Action<string>? OnProgressMessage;
|
||||
|
||||
public async Task<bool> DeleteDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.DeleteDir, pathBytes);
|
||||
|
||||
// Receive initial OK response
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
if (response != Response.Ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now listen for progress messages synchronously
|
||||
// Keep reading until we get the final OK/ERROR response
|
||||
bool deletionComplete = false;
|
||||
|
||||
try
|
||||
{
|
||||
while (!deletionComplete)
|
||||
{
|
||||
var (progressResponse, progressData) = await ReceiveResponseAsync();
|
||||
|
||||
if (progressResponse == Response.Progress)
|
||||
{
|
||||
string message = Encoding.UTF8.GetString(progressData).TrimEnd('\0');
|
||||
OnProgressMessage?.Invoke(message);
|
||||
}
|
||||
else if (progressResponse == Response.Ok)
|
||||
{
|
||||
// Final OK response received - deletion complete
|
||||
OnProgressMessage?.Invoke("🔚 Received final OK - deletion complete");
|
||||
deletionComplete = true;
|
||||
}
|
||||
else if (progressResponse == Response.Error)
|
||||
{
|
||||
// Error response received
|
||||
OnProgressMessage?.Invoke("❌ Received error response");
|
||||
deletionComplete = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unexpected response - log it
|
||||
OnProgressMessage?.Invoke($"⚠️ Unexpected response: {progressResponse}");
|
||||
deletionComplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Connection closed - log it
|
||||
OnProgressMessage?.Invoke($"⚠️ Connection closed: {ex.Message}");
|
||||
}
|
||||
|
||||
// Give server a moment to fully close the deletion thread
|
||||
// CRITICAL: Increased delay to prevent race condition with next delete
|
||||
await Task.Delay(500);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> RenameAsync(string oldPath, string newPath)
|
||||
@@ -329,7 +413,20 @@ namespace PS5Upload
|
||||
|
||||
if (_stream == null) return false;
|
||||
|
||||
await _stream.WriteAsync(sendBuffer, 0, 5 + bytesRead, cancellationToken);
|
||||
// FIX: Add timeout to WriteAsync to prevent hanging if PS5 becomes unresponsive
|
||||
// 30 second timeout per chunk write - should be more than enough for 4MB
|
||||
using var writeTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, writeTimeout.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await _stream.WriteAsync(sendBuffer, 0, 5 + bytesRead, linkedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (writeTimeout.IsCancellationRequested)
|
||||
{
|
||||
// Write timeout - PS5 is unresponsive
|
||||
return false;
|
||||
}
|
||||
|
||||
totalSent += bytesRead;
|
||||
bytesRemaining -= bytesRead;
|
||||
@@ -359,18 +456,101 @@ namespace PS5Upload
|
||||
}
|
||||
}
|
||||
|
||||
// Send END_UPLOAD - fire-and-forget for maximum speed
|
||||
// Don't wait for response to avoid PS5 payload bottleneck
|
||||
// Send END_UPLOAD and wait for response
|
||||
// CRITICAL: Must wait for response to avoid protocol desync on chunked uploads
|
||||
try
|
||||
{
|
||||
await SendCommandAsync(Command.EndUpload);
|
||||
// Immediately return success without waiting for response
|
||||
// This eliminates the PS5 response handling bottleneck
|
||||
return true;
|
||||
var (endResponse, _) = await ReceiveResponseAsync();
|
||||
return endResponse == Response.Ok;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // Assume success if connection issues
|
||||
return false; // Connection issue means upload failed
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadFileAsync(string remotePath, string localPath, IProgress<UploadProgress>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Send download command
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(remotePath);
|
||||
await SendCommandAsync(Command.DownloadFile, pathBytes);
|
||||
|
||||
// Receive response header (5 bytes: 1 response + 4 data_len)
|
||||
byte[] header = new byte[5];
|
||||
await ReadExactAsync(header, 5);
|
||||
|
||||
Response response = (Response)header[0];
|
||||
uint dataLen = BitConverter.ToUInt32(header, 1);
|
||||
|
||||
// Check if error response
|
||||
if (response == Response.Error)
|
||||
{
|
||||
if (dataLen > 0)
|
||||
{
|
||||
byte[] errorMsg = new byte[dataLen];
|
||||
await ReadExactAsync(errorMsg, (int)dataLen);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expecting RESP_DATA with 8-byte file size
|
||||
if (response != Response.Data || dataLen != 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read file size (8 bytes)
|
||||
byte[] sizeBytes = new byte[8];
|
||||
await ReadExactAsync(sizeBytes, 8);
|
||||
long fileSize = BitConverter.ToInt64(sizeBytes, 0);
|
||||
|
||||
// Now read raw file data directly from socket
|
||||
using var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize);
|
||||
|
||||
byte[] buffer = new byte[BufferSize];
|
||||
long totalReceived = 0;
|
||||
var startTime = DateTime.Now;
|
||||
|
||||
while (totalReceived < fileSize)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
|
||||
int toRead = (int)Math.Min(BufferSize, fileSize - totalReceived);
|
||||
int received = await _stream!.ReadAsync(buffer, 0, toRead, cancellationToken);
|
||||
|
||||
if (received == 0) break;
|
||||
|
||||
await fs.WriteAsync(buffer, 0, received, cancellationToken);
|
||||
totalReceived += received;
|
||||
|
||||
// Report progress every 5MB or at completion
|
||||
if (totalReceived % (5 * 1024 * 1024) < BufferSize || totalReceived == fileSize)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
double speed = elapsed.TotalSeconds > 0 ? totalReceived / elapsed.TotalSeconds : 0;
|
||||
|
||||
progress?.Report(new UploadProgress
|
||||
{
|
||||
BytesSent = totalReceived,
|
||||
TotalBytes = fileSize,
|
||||
SpeedBytesPerSecond = speed,
|
||||
AverageSpeedBytesPerSecond = speed,
|
||||
ElapsedTime = elapsed,
|
||||
EstimatedTimeRemaining = speed > 0 ? TimeSpan.FromSeconds((fileSize - totalReceived) / speed) : TimeSpan.Zero,
|
||||
CurrentFileName = Path.GetFileName(localPath)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return totalReceived == fileSize;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Download error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ echo ""
|
||||
|
||||
echo "[+] Compiling..."
|
||||
rm -f ps5_upload_server.elf
|
||||
/home/hackman/ps5sdk_copy/bin/prospero-clang -Wall -O3 -pthread -o ps5_upload_server.elf main.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -Wall -O3 -pthread -o ps5_upload_server.elf main.c
|
||||
|
||||
if [ -f "ps5_upload_server.elf" ]; then
|
||||
echo ""
|
||||
|
||||
+457
-50
@@ -22,11 +22,78 @@
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#define SERVER_PORT 9113
|
||||
#define BUFFER_SIZE (8 * 1024 * 1024)
|
||||
#define BUFFER_SIZE (4 * 1024 * 1024) // 4MB - MUST match client BufferSize
|
||||
#define MAX_PATH 2048
|
||||
#define DISK_WORKER_COUNT 4
|
||||
#define QUEUE_MAX_SIZE 32
|
||||
|
||||
// Per-file mutex hash map to prevent corruption during parallel uploads to SAME file
|
||||
// Different files can write in parallel without blocking each other
|
||||
typedef struct file_mutex_entry {
|
||||
char path[MAX_PATH];
|
||||
pthread_mutex_t mutex;
|
||||
int ref_count;
|
||||
struct file_mutex_entry *next;
|
||||
} file_mutex_entry_t;
|
||||
|
||||
static file_mutex_entry_t *g_file_mutexes = NULL;
|
||||
static pthread_mutex_t g_mutex_map_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
// Get or create mutex for a specific file path
|
||||
pthread_mutex_t* get_file_mutex(const char *path) {
|
||||
pthread_mutex_lock(&g_mutex_map_lock);
|
||||
|
||||
// Search for existing mutex
|
||||
file_mutex_entry_t *entry = g_file_mutexes;
|
||||
while (entry) {
|
||||
if (strcmp(entry->path, path) == 0) {
|
||||
entry->ref_count++;
|
||||
pthread_mutex_unlock(&g_mutex_map_lock);
|
||||
return &entry->mutex;
|
||||
}
|
||||
entry = entry->next;
|
||||
}
|
||||
|
||||
// Create new mutex for this file
|
||||
entry = (file_mutex_entry_t*)malloc(sizeof(file_mutex_entry_t));
|
||||
if (!entry) {
|
||||
pthread_mutex_unlock(&g_mutex_map_lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
strncpy(entry->path, path, sizeof(entry->path) - 1);
|
||||
entry->path[sizeof(entry->path) - 1] = '\0';
|
||||
pthread_mutex_init(&entry->mutex, NULL);
|
||||
entry->ref_count = 1;
|
||||
entry->next = g_file_mutexes;
|
||||
g_file_mutexes = entry;
|
||||
|
||||
pthread_mutex_unlock(&g_mutex_map_lock);
|
||||
return &entry->mutex;
|
||||
}
|
||||
|
||||
// Release mutex reference
|
||||
void release_file_mutex(const char *path) {
|
||||
pthread_mutex_lock(&g_mutex_map_lock);
|
||||
|
||||
file_mutex_entry_t **ptr = &g_file_mutexes;
|
||||
while (*ptr) {
|
||||
if (strcmp((*ptr)->path, path) == 0) {
|
||||
(*ptr)->ref_count--;
|
||||
if ((*ptr)->ref_count == 0) {
|
||||
file_mutex_entry_t *to_free = *ptr;
|
||||
*ptr = (*ptr)->next;
|
||||
pthread_mutex_destroy(&to_free->mutex);
|
||||
free(to_free);
|
||||
}
|
||||
break;
|
||||
}
|
||||
ptr = &(*ptr)->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_mutex_map_lock);
|
||||
}
|
||||
|
||||
// Protocol commands
|
||||
#define CMD_PING 0x01
|
||||
#define CMD_LIST_STORAGE 0x02
|
||||
@@ -40,6 +107,7 @@
|
||||
#define CMD_START_UPLOAD 0x10
|
||||
#define CMD_UPLOAD_CHUNK 0x11
|
||||
#define CMD_END_UPLOAD 0x12
|
||||
#define CMD_DOWNLOAD_FILE 0x13
|
||||
#define CMD_SHUTDOWN 0xFF
|
||||
|
||||
// Protocol responses
|
||||
@@ -66,6 +134,7 @@ void send_notification(const char *msg) {
|
||||
typedef struct {
|
||||
int sock;
|
||||
FILE *upload_fp;
|
||||
pthread_mutex_t *file_mutex; // Per-file mutex
|
||||
char upload_path[MAX_PATH];
|
||||
uint64_t upload_size;
|
||||
uint64_t upload_received;
|
||||
@@ -244,7 +313,74 @@ int mkdir_recursive(const char *path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Recursive directory deletion
|
||||
// Global deletion progress counter
|
||||
static int g_delete_count = 0;
|
||||
static int g_total_files = 0;
|
||||
static time_t g_last_notify = 0;
|
||||
static int g_client_sock = 0;
|
||||
|
||||
// Forward declaration
|
||||
void send_progress_message(const char *msg);
|
||||
|
||||
// Global scan counter for progress updates (reset before each scan)
|
||||
static int g_scan_count = 0;
|
||||
static time_t g_last_scan_notify = 0;
|
||||
|
||||
// Count files in directory recursively with progress updates
|
||||
int count_files_recursive(const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
if (!dir) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
struct dirent *entry;
|
||||
char child[MAX_PATH];
|
||||
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
snprintf(child, sizeof(child), "%s/%s", path, entry->d_name);
|
||||
|
||||
struct stat st;
|
||||
if (stat(child, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
count++; // Count the directory itself
|
||||
count += count_files_recursive(child); // Count its contents
|
||||
} else {
|
||||
count++;
|
||||
g_scan_count++;
|
||||
|
||||
// Send progress every 500 files or every 3 seconds during counting
|
||||
time_t now = time(NULL);
|
||||
if (g_scan_count % 500 == 0 || (now - g_last_scan_notify) >= 3) {
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "📊 Scanning... found %d files so far", g_scan_count);
|
||||
send_progress_message(msg);
|
||||
g_last_scan_notify = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
return count;
|
||||
}
|
||||
|
||||
// Send progress message to client
|
||||
void send_progress_message(const char *msg) {
|
||||
if (g_client_sock > 0) {
|
||||
uint8_t header[5];
|
||||
header[0] = RESP_PROGRESS;
|
||||
uint32_t len = strlen(msg) + 1;
|
||||
memcpy(header + 1, &len, 4);
|
||||
send(g_client_sock, header, 5, 0);
|
||||
send(g_client_sock, msg, len, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive directory deletion with progress reporting
|
||||
int rmdir_recursive(const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
if (!dir) {
|
||||
@@ -267,6 +403,18 @@ int rmdir_recursive(const char *path) {
|
||||
rmdir_recursive(child);
|
||||
} else {
|
||||
unlink(child);
|
||||
g_delete_count++;
|
||||
|
||||
// Send progress every 50 files or every 2 seconds
|
||||
time_t now = time(NULL);
|
||||
if (g_delete_count % 50 == 0 || (now - g_last_notify) >= 2) {
|
||||
int percentage = (g_total_files > 0) ? (g_delete_count * 100 / g_total_files) : 0;
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "🗑️ Deleting... %d/%d files (%d%%)",
|
||||
g_delete_count, g_total_files, percentage);
|
||||
send_progress_message(msg);
|
||||
g_last_notify = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
@@ -361,22 +509,33 @@ void handle_list_dir(client_session_t *session, const char *path) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Determine type - use d_type if available, otherwise fall back to stat()
|
||||
// Determine type and get file size
|
||||
uint8_t type = 0;
|
||||
uint64_t size = 0;
|
||||
uint64_t timestamp = 0;
|
||||
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
|
||||
struct stat st;
|
||||
|
||||
if (entry->d_type == DT_DIR) {
|
||||
type = 1;
|
||||
} else if (entry->d_type == DT_UNKNOWN) {
|
||||
// d_type not supported on this filesystem - use stat() as fallback
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
type = 1;
|
||||
if (stat(full_path, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
type = 1;
|
||||
} else {
|
||||
size = st.st_size;
|
||||
timestamp = st.st_mtime;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular file - get size
|
||||
if (stat(full_path, &st) == 0) {
|
||||
size = st.st_size;
|
||||
timestamp = st.st_mtime;
|
||||
}
|
||||
}
|
||||
// else: DT_REG or other = file (type = 0)
|
||||
|
||||
uint64_t size = 0; // Don't need file size for browsing
|
||||
uint64_t timestamp = 0; // Don't need timestamp for browsing
|
||||
|
||||
*ptr++ = type;
|
||||
memcpy(ptr, &name_len, 2);
|
||||
@@ -415,12 +574,125 @@ void handle_delete_file(client_session_t *session, const char *path) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DELETE_DIR
|
||||
void handle_delete_dir(client_session_t *session, const char *path) {
|
||||
if (rmdir_recursive(path) == 0) {
|
||||
send_ok(session->sock, "Directory deleted");
|
||||
// Background deletion thread data
|
||||
typedef struct {
|
||||
char path[MAX_PATH];
|
||||
int client_sock;
|
||||
} delete_thread_data_t;
|
||||
|
||||
// Background deletion thread
|
||||
void* delete_thread_func(void* arg) {
|
||||
delete_thread_data_t* data = (delete_thread_data_t*)arg;
|
||||
|
||||
// Reset ALL progress counters
|
||||
g_delete_count = 0;
|
||||
g_scan_count = 0;
|
||||
g_last_notify = time(NULL);
|
||||
g_last_scan_notify = time(NULL);
|
||||
g_client_sock = data->client_sock;
|
||||
|
||||
// Count total files first
|
||||
char start_msg[256];
|
||||
snprintf(start_msg, sizeof(start_msg), "📊 Scanning folder: %s", data->path);
|
||||
send_progress_message(start_msg);
|
||||
|
||||
g_total_files = count_files_recursive(data->path);
|
||||
|
||||
if (g_total_files == 0) {
|
||||
char empty_msg[256];
|
||||
snprintf(empty_msg, sizeof(empty_msg), "⚠️ Folder is empty or already deleted");
|
||||
send_progress_message(empty_msg);
|
||||
g_client_sock = 0;
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char count_msg[256];
|
||||
snprintf(count_msg, sizeof(count_msg), "📊 Total: %d files to delete", g_total_files);
|
||||
send_progress_message(count_msg);
|
||||
|
||||
// Start deletion
|
||||
char del_msg[256];
|
||||
snprintf(del_msg, sizeof(del_msg), "🗑️ Starting deletion...");
|
||||
send_progress_message(del_msg);
|
||||
|
||||
// Perform deletion in background
|
||||
int result = rmdir_recursive(data->path);
|
||||
|
||||
// Send completion message
|
||||
if (result == 0) {
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "✅ Deleted %d files (100%%)", g_delete_count);
|
||||
send_progress_message(msg);
|
||||
send_notification(msg);
|
||||
|
||||
// Send final OK response to signal completion
|
||||
if (g_client_sock > 0) {
|
||||
uint8_t header[5];
|
||||
header[0] = RESP_OK;
|
||||
uint32_t len = 0;
|
||||
memcpy(header + 1, &len, 4);
|
||||
send(g_client_sock, header, 5, 0);
|
||||
|
||||
// Force flush and wait for data to be sent
|
||||
struct timespec ts;
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = 200000000; // 200ms
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
} else {
|
||||
send_error(session->sock, "Failed to delete directory");
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg), "❌ Failed to delete folder (%d files removed)", g_delete_count);
|
||||
send_progress_message(msg);
|
||||
|
||||
// Send error response
|
||||
if (g_client_sock > 0) {
|
||||
uint8_t header[5];
|
||||
header[0] = RESP_ERROR;
|
||||
uint32_t len = 0;
|
||||
memcpy(header + 1, &len, 4);
|
||||
send(g_client_sock, header, 5, 0);
|
||||
|
||||
// Force flush and wait for data to be sent
|
||||
struct timespec ts;
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = 200000000; // 200ms
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
g_client_sock = 0;
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Handle DELETE_DIR - BUG FIX: Async deletion to prevent timeout
|
||||
void handle_delete_dir(client_session_t *session, const char *path) {
|
||||
// Send OK response immediately to prevent timeout
|
||||
send_ok(session->sock, "Deleting folder in background...");
|
||||
|
||||
// Create background thread for deletion
|
||||
delete_thread_data_t* data = malloc(sizeof(delete_thread_data_t));
|
||||
if (data) {
|
||||
strncpy(data->path, path, MAX_PATH - 1);
|
||||
data->path[MAX_PATH - 1] = '\0';
|
||||
data->client_sock = session->sock;
|
||||
|
||||
pthread_t thread;
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
|
||||
if (pthread_create(&thread, &attr, delete_thread_func, data) != 0) {
|
||||
// Thread creation failed, delete synchronously
|
||||
free(data);
|
||||
rmdir_recursive(path);
|
||||
}
|
||||
|
||||
pthread_attr_destroy(&attr);
|
||||
} else {
|
||||
// Malloc failed, delete synchronously
|
||||
rmdir_recursive(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +787,11 @@ void handle_start_upload(client_session_t *session, const uint8_t *data, uint32_
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
// Release previous file mutex to prevent leak
|
||||
if (session->file_mutex) {
|
||||
release_file_mutex(session->upload_path);
|
||||
session->file_mutex = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse path, size, and optional offset
|
||||
@@ -544,40 +821,71 @@ void handle_start_upload(client_session_t *session, const uint8_t *data, uint32_
|
||||
mkdir_recursive(parent);
|
||||
}
|
||||
|
||||
// Open file for writing (r+b for chunk mode, wb for normal)
|
||||
// Get per-file mutex for this specific file
|
||||
session->file_mutex = get_file_mutex(path);
|
||||
if (!session->file_mutex) {
|
||||
send_error(session->sock, "Cannot allocate file mutex");
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Lock mutex BEFORE opening file to prevent race condition
|
||||
// when multiple threads try to create the same file simultaneously
|
||||
pthread_mutex_lock(session->file_mutex);
|
||||
|
||||
// Open file for writing
|
||||
// For chunked uploads, we need to pre-allocate the file on first chunk
|
||||
// and open with r+b for subsequent chunks
|
||||
if (chunk_offset > 0) {
|
||||
// Chunk mode: open existing file or create if needed
|
||||
// Subsequent chunk: open existing file
|
||||
session->upload_fp = fopen(path, "r+b");
|
||||
if (!session->upload_fp) {
|
||||
// File doesn't exist, create it with full size
|
||||
session->upload_fp = fopen(path, "wb");
|
||||
if (session->upload_fp) {
|
||||
// CRITICAL: Allocate full file size before any chunk writes
|
||||
// This ensures all chunks can write to their offsets without corruption
|
||||
if (fseeko(session->upload_fp, file_size - 1, SEEK_SET) == 0) {
|
||||
// Write a single byte at the end to allocate the full size
|
||||
fputc(0, session->upload_fp);
|
||||
fflush(session->upload_fp);
|
||||
}
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = fopen(path, "r+b");
|
||||
}
|
||||
}
|
||||
if (session->upload_fp) {
|
||||
// Seek to chunk offset
|
||||
fseeko(session->upload_fp, chunk_offset, SEEK_SET);
|
||||
}
|
||||
} else {
|
||||
// Normal mode: create new file
|
||||
// First chunk or small file: create new file
|
||||
// Check if this is a chunked upload (file_size > chunk that will be sent)
|
||||
// We pre-allocate for large files to ensure r+b works for subsequent chunks
|
||||
session->upload_fp = fopen(path, "wb");
|
||||
if (session->upload_fp && file_size > 100 * 1024 * 1024) {
|
||||
// Large file - pre-allocate full size for chunked upload
|
||||
// FIX: Check if pre-allocation succeeds (disk might be full!)
|
||||
int prealloc_ok = 0;
|
||||
if (fseeko(session->upload_fp, file_size - 1, SEEK_SET) == 0) {
|
||||
if (fputc(0, session->upload_fp) != EOF) {
|
||||
if (fflush(session->upload_fp) == 0) {
|
||||
prealloc_ok = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prealloc_ok) {
|
||||
// Pre-allocation failed - likely disk full
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
pthread_mutex_unlock(session->file_mutex);
|
||||
release_file_mutex(path);
|
||||
session->file_mutex = NULL;
|
||||
unlink(path); // Remove partial file
|
||||
send_error(session->sock, "Disk full - cannot pre-allocate file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Seek back to beginning
|
||||
fseeko(session->upload_fp, 0, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(session->file_mutex);
|
||||
|
||||
if (!session->upload_fp) {
|
||||
release_file_mutex(path);
|
||||
session->file_mutex = NULL;
|
||||
send_error(session->sock, "Cannot create file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use HUGE buffer (8MB) for maximum write speed
|
||||
// Use 4MB buffer for write speed
|
||||
setvbuf(session->upload_fp, NULL, _IOFBF, BUFFER_SIZE);
|
||||
|
||||
strncpy(session->upload_path, path, sizeof(session->upload_path) - 1);
|
||||
@@ -585,7 +893,7 @@ void handle_start_upload(client_session_t *session, const uint8_t *data, uint32_
|
||||
session->upload_received = chunk_offset;
|
||||
|
||||
// Increase socket receive buffer for this upload session
|
||||
int huge_buf = 8 * 1024 * 1024; // 8MB receive buffer
|
||||
int huge_buf = 4 * 1024 * 1024; // 4MB receive buffer - matches client
|
||||
setsockopt(session->sock, SOL_SOCKET, SO_RCVBUF, &huge_buf, sizeof(huge_buf));
|
||||
|
||||
send_response(session->sock, RESP_READY, NULL, 0);
|
||||
@@ -593,26 +901,34 @@ void handle_start_upload(client_session_t *session, const uint8_t *data, uint32_
|
||||
|
||||
// Handle UPLOAD_CHUNK
|
||||
void handle_upload_chunk(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
if (!session->upload_fp) {
|
||||
if (!session->upload_fp || !session->file_mutex) {
|
||||
send_error(session->sock, "No upload in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Direct write with periodic flush for stability
|
||||
// Lock ONLY this file's mutex - other files can write in parallel!
|
||||
pthread_mutex_lock(session->file_mutex);
|
||||
|
||||
size_t written = fwrite(data, 1, data_len, session->upload_fp);
|
||||
|
||||
if (written != data_len) {
|
||||
pthread_mutex_unlock(session->file_mutex);
|
||||
send_error(session->sock, "Write failed");
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
release_file_mutex(session->upload_path);
|
||||
session->file_mutex = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
session->upload_received += written;
|
||||
|
||||
// Flush every 32MB to prevent buffer overflow with multiple connections
|
||||
// Flush every 32MB to prevent buffer overflow - MUST be inside mutex!
|
||||
if (session->upload_received % (32 * 1024 * 1024) < data_len) {
|
||||
fflush(session->upload_fp);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(session->file_mutex);
|
||||
// No response - zero blocking for maximum speed
|
||||
}
|
||||
|
||||
@@ -626,11 +942,77 @@ void handle_end_upload(client_session_t *session) {
|
||||
fflush(session->upload_fp);
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
|
||||
if (session->file_mutex) {
|
||||
release_file_mutex(session->upload_path);
|
||||
session->file_mutex = NULL;
|
||||
}
|
||||
|
||||
chmod(session->upload_path, 0777);
|
||||
|
||||
send_ok(session->sock, "Upload complete");
|
||||
}
|
||||
|
||||
// Handle DOWNLOAD_FILE
|
||||
void handle_download_file(client_session_t *session, const char *path) {
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
send_error(session->sock, "Cannot open file");
|
||||
return;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) != 0) {
|
||||
close(fd);
|
||||
send_error(session->sock, "Cannot stat file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send file size first
|
||||
uint64_t file_size = st.st_size;
|
||||
send_response(session->sock, RESP_DATA, &file_size, sizeof(file_size));
|
||||
|
||||
// Use sendfile for zero-copy transfer (same as FTP server optimization)
|
||||
#ifdef __FreeBSD__
|
||||
off_t offset = 0;
|
||||
off_t sbytes = 0;
|
||||
|
||||
// Send in chunks for progress tracking
|
||||
const off_t CHUNK_SIZE = 100 * 1024 * 1024; // 100MB chunks
|
||||
while (offset < file_size) {
|
||||
off_t to_send = (file_size - offset > CHUNK_SIZE) ? CHUNK_SIZE : (file_size - offset);
|
||||
sbytes = 0;
|
||||
|
||||
int result = sendfile(fd, session->sock, offset, to_send, NULL, &sbytes, 0);
|
||||
if (result < 0 && errno != EAGAIN) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
offset += sbytes;
|
||||
if (sbytes == 0) break;
|
||||
}
|
||||
#else
|
||||
// Fallback for non-FreeBSD systems
|
||||
char *buffer = malloc(BUFFER_SIZE);
|
||||
if (buffer) {
|
||||
ssize_t n;
|
||||
while ((n = read(fd, buffer, BUFFER_SIZE)) > 0) {
|
||||
ssize_t sent = 0;
|
||||
while (sent < n) {
|
||||
ssize_t s = send(session->sock, buffer + sent, n - sent, 0);
|
||||
if (s <= 0) break;
|
||||
sent += s;
|
||||
}
|
||||
if (sent != n) break;
|
||||
}
|
||||
free(buffer);
|
||||
}
|
||||
#endif
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
// Handle client
|
||||
void *client_thread(void *arg) {
|
||||
client_session_t *session = (client_session_t *)arg;
|
||||
@@ -642,11 +1024,7 @@ void *client_thread(void *arg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Set socket timeout to prevent blocking forever (60 seconds)
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 60;
|
||||
tv.tv_usec = 0;
|
||||
setsockopt(session->sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
// No socket timeout - connection stays open indefinitely until client disconnects
|
||||
|
||||
while (1) {
|
||||
// Read command header (5 bytes: 1 cmd + 4 data_len)
|
||||
@@ -737,12 +1115,20 @@ void *client_thread(void *arg) {
|
||||
case CMD_END_UPLOAD:
|
||||
handle_end_upload(session);
|
||||
break;
|
||||
case CMD_DOWNLOAD_FILE:
|
||||
if (data) {
|
||||
handle_download_file(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_SHUTDOWN:
|
||||
send_ok(session->sock, "Shutting down");
|
||||
free(buffer);
|
||||
close(session->sock);
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
if (session->file_mutex) {
|
||||
release_file_mutex(session->upload_path);
|
||||
}
|
||||
}
|
||||
free(session);
|
||||
exit(0);
|
||||
@@ -756,6 +1142,9 @@ void *client_thread(void *arg) {
|
||||
close(session->sock);
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
if (session->file_mutex) {
|
||||
release_file_mutex(session->upload_path);
|
||||
}
|
||||
}
|
||||
free(session);
|
||||
return NULL;
|
||||
@@ -780,8 +1169,8 @@ int main() {
|
||||
int no_sigpipe = 1;
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe));
|
||||
|
||||
// 8MB buffers for sustained high throughput
|
||||
int buf_size = 8 * 1024 * 1024;
|
||||
// 4MB buffers - matches client
|
||||
int buf_size = 4 * 1024 * 1024;
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));
|
||||
|
||||
@@ -795,7 +1184,8 @@ int main() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (listen(server_sock, 10) < 0) {
|
||||
// Increase backlog to handle multiple parallel connections (up to 128)
|
||||
if (listen(server_sock, 128) < 0) {
|
||||
close(server_sock);
|
||||
return 1;
|
||||
}
|
||||
@@ -833,8 +1223,8 @@ int main() {
|
||||
// Aggressive TCP socket options for sustained high speed
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe));
|
||||
|
||||
// Increase buffers to 8MB for maximum throughput
|
||||
int large_buf = 8 * 1024 * 1024;
|
||||
// Increase buffers to 4MB - matches client
|
||||
int large_buf = 4 * 1024 * 1024;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_RCVBUF, &large_buf, sizeof(large_buf));
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_SNDBUF, &large_buf, sizeof(large_buf));
|
||||
|
||||
@@ -844,10 +1234,27 @@ int main() {
|
||||
|
||||
// NOTE: Removed TCP_NOPUSH - it was causing buffering delays!
|
||||
|
||||
// Set keepalive to prevent connection drops
|
||||
// CRITICAL: Unlimited timeout for files of ANY size
|
||||
// Keepalive will detect and close dead connections (~25s)
|
||||
// This allows 50GB+ files to upload without timeout issues
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 0; // 0 = unlimited timeout
|
||||
tv.tv_usec = 0;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
// CRITICAL: Aggressive keepalive to prevent connection drops on large files
|
||||
int keepalive = 1;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
|
||||
|
||||
// Set keepalive parameters (FreeBSD/PS5)
|
||||
int keepidle = 10; // Start keepalive after 10 seconds of idle
|
||||
int keepintvl = 5; // Send keepalive every 5 seconds
|
||||
int keepcnt = 3; // Drop connection after 3 failed keepalives
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
|
||||
|
||||
client_session_t *session = malloc(sizeof(client_session_t));
|
||||
if (!session) {
|
||||
close(client_sock);
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user