mirror of
https://github.com/manos555555/PS5-Suite.git
synced 2026-10-06 10:00:26 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a837ea9fe1 | ||
|
|
ac7cea2e4b | ||
|
|
abaa028d28 | ||
|
|
45afca892c | ||
|
|
f065b79b33 | ||
|
|
4403d86be5 | ||
|
|
af252efc9a | ||
|
|
2da27aed13 | ||
|
|
264ad6210d | ||
|
|
db48bc6fb2 | ||
|
|
4ac43ebb8b | ||
|
|
20caced5f4 | ||
|
|
285279788f | ||
|
|
70d174cecc | ||
|
|
f128c6a6b4 | ||
|
|
c211c086ac | ||
|
|
4ca30876c7 | ||
|
|
5ae3c1709e | ||
|
|
3374e87bfe | ||
|
|
c21d73f41d | ||
|
|
5ce9d470bc | ||
|
|
0eb159bf88 | ||
|
|
4ff160232f | ||
|
|
a5e40ce245 | ||
|
|
bb7d90411c | ||
|
|
00013aae76 | ||
|
|
6d8d802962 | ||
|
|
f3a1d5e436 | ||
|
|
bad3029e32 | ||
|
|
a51fffef83 | ||
|
|
f89eaf4e31 | ||
|
|
f34fdf8e63 | ||
|
|
8479221915 | ||
|
|
e3bfa54994 | ||
|
|
68d586de97 | ||
|
|
297edb18af |
No files matched your search
-60
@@ -1,60 +0,0 @@
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio cache/options
|
||||
.vs/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
*.cache
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Files built by Visual Studio
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Caches
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.old
|
||||
|
||||
# PowerShell script
|
||||
convert_to_ico.ps1
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
# Changelog - PS5 Upload Suite
|
||||
|
||||
## Version 4.1.0 - Memory Optimization & Large File Handling (February 5, 2026)
|
||||
|
||||
### 🧠 Memory & Performance Improvements
|
||||
|
||||
#### 1. Large File Count Support (156K+ Files)
|
||||
- **Fixed crash** with games containing 156,253+ files (e.g., Astrobot)
|
||||
- **Removed massive dictionary** - No longer creates 156K FileInfo objects at once
|
||||
- **Incremental calculation** - Total bytes calculated progressively during scan
|
||||
- **Memory efficient** - Handles unlimited file counts without OutOfMemoryException
|
||||
|
||||
#### 2. Lock-Free Progress Tracking
|
||||
- **ConcurrentDictionary** - Replaced Dictionary with thread-safe collections
|
||||
- **Zero lock contention** - Eliminated `lock (_progressLock)` blocks
|
||||
- **Atomic operations** - Uses `Interlocked.Add()` for progress updates
|
||||
- **Smooth UI** - No stuttering from lock contention
|
||||
|
||||
#### 3. Auto Memory Cleanup
|
||||
- **Completed files removed** - Dictionaries cleaned after file completion
|
||||
- **Reduced memory footprint** - Only active uploads tracked
|
||||
- **Prevents memory overflow** - Long-running uploads stay stable
|
||||
|
||||
#### 4. Reduced UI Stuttering
|
||||
- **Batch logging** - Small files logged every 50 completions (up from 25)
|
||||
- **Less UI pressure** - Fewer `Dispatcher.Invoke()` calls
|
||||
- **Smoother experience** - Especially noticeable with 100K+ files
|
||||
|
||||
### 🔧 Technical Changes
|
||||
- Changed `_fileProgressBytes` from Dictionary to ConcurrentDictionary
|
||||
- Changed `_fileChunkProgressBytes` from Dictionary to ConcurrentDictionary
|
||||
- Changed `_chunkLogLastBytes` from Dictionary to ConcurrentDictionary
|
||||
- Removed `fileSizeLookup` dictionary creation (caused OutOfMemoryException)
|
||||
- Increased `SmallFileLogBatchSize` from 25 to 50
|
||||
- Added dictionary cleanup in file completion handlers
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- Fixed application crash when uploading games with 156K+ files
|
||||
- Fixed UI stuttering during small file uploads
|
||||
- Fixed memory overflow from massive dictionary allocations
|
||||
- Fixed lock contention causing UI freezes
|
||||
|
||||
### 📊 Tested With
|
||||
- **Astrobot**: 156,253 files, 1,724 directories
|
||||
- **Result**: Zero crashes, smooth upload, stable memory usage
|
||||
|
||||
---
|
||||
|
||||
## Version 4.0.0 - Retry Feature & Stability Improvements (January 27, 2026)
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
#### 🔄 Retry Failed Transfers
|
||||
- **Right-click context menu** on failed transfers
|
||||
- **Retry Upload** - Re-queue failed files for upload
|
||||
- **Remove from List** - Clear individual failed items
|
||||
- **Persistent paths** - Failed transfers store local and remote paths
|
||||
- **Smart retry** - Validates file existence before retry
|
||||
|
||||
#### 🗑️ Clear All Button Fix
|
||||
- **Transfer History Clear All** now works correctly
|
||||
- **Confirmation dialog** before clearing history
|
||||
- **Clears both** completed and failed transfers
|
||||
|
||||
### 🔧 Performance & Stability
|
||||
|
||||
#### Connection Stability
|
||||
- **Reverted 16MB buffer** - Caused connection drops with 16 parallel connections
|
||||
- **Stable 8MB buffer** - Proven reliable for high-speed transfers
|
||||
- **TCP NoDelay** - Already enabled for optimal performance
|
||||
- **16MB socket buffers** - Maintained for maximum throughput
|
||||
|
||||
#### Upload Performance
|
||||
- **Large files (>100MB)**: 30-36 MB/s per file
|
||||
- **Small files (<100MB)**: 8-12 MB/s per file
|
||||
- **4 parallel large files** - Prevents PS5 memory exhaustion
|
||||
- **16 total connections** - Optimal for mixed workloads
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- Fixed Clear All button not clearing transfer history
|
||||
- Fixed connection drops when using oversized buffers
|
||||
- Added proper event handlers for failed transfer context menu
|
||||
- Improved error handling for retry operations
|
||||
|
||||
### 📝 Code Quality
|
||||
- Added comprehensive inline documentation
|
||||
- Improved code organization and readability
|
||||
- Better error messages for failed operations
|
||||
- Enhanced UI feedback for user actions
|
||||
|
||||
---
|
||||
|
||||
## Version 3.3.0 - Smart Search & Enhanced Mobile (January 26, 2026)
|
||||
|
||||
### 🔍 Smart Search Feature
|
||||
- **Full Filesystem Indexing** - Index entire PS5 filesystem (200,000+ files)
|
||||
- **Instant Search** - Lightning-fast search with wildcards (`*.pkg`, `*loader*`)
|
||||
- **Size Filters** - Search by file size (`size:>1GB`, `size:<100MB`)
|
||||
- **Case-Insensitive** - Matches both filename and full path
|
||||
- **Navigate from Search** - Double-click results to jump to folder (Desktop) / Tap to navigate (Mobile)
|
||||
- **Index Status** - Real-time indexing progress with file/directory counts
|
||||
- **Smart Indexing** - Skips problematic directories (`/dev`, `/proc`, `/sys`)
|
||||
|
||||
### 📱 Mobile App v1.2
|
||||
- **🔍 Search Tab** - Full Smart Search functionality on Android
|
||||
- **Navigate to Location** - Tap search results to navigate to folder
|
||||
- **Copy Path** - Copy file paths to clipboard from search results
|
||||
- **Connection Manager** - Singleton pattern for reliable IP/port management
|
||||
- **Improved Navigation** - Fixed MainPage reference for seamless navigation
|
||||
|
||||
### 🖥️ Desktop Client Updates
|
||||
- **Search Tab** - New dedicated tab for Smart Search
|
||||
- **💻 Shell Terminal** - Execute commands directly on PS5
|
||||
- Run system commands remotely
|
||||
- Real-time output display
|
||||
- Command history
|
||||
- Working directory support
|
||||
- **Equal Panel Layout** - Local Files, PS5 Files, and Right panel now equal width
|
||||
- **Improved UI** - Index Status and buttons positioned above search box
|
||||
- **Debounced Search** - Smooth typing experience with 300ms debounce
|
||||
|
||||
### 🔧 Payload Updates
|
||||
- **Indexing Commands** - `CMD_INDEX_START` (0x40), `CMD_INDEX_STATUS` (0x41), `CMD_SEARCH_INDEX` (0x42)
|
||||
- **In-Memory Index** - Linked list structure for fast search
|
||||
- **Wildcard Matching** - Support for `*` and `?` wildcards
|
||||
- **Thread-Safe** - Mutex-protected index operations
|
||||
- **Root Path Support** - Can index from `/` (entire filesystem)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- Fixed indexing hangs when scanning root directory
|
||||
- Fixed search protocol compatibility between mobile and desktop
|
||||
- Fixed navigation stack issues in mobile app
|
||||
- Fixed nullable reference warnings in mobile code
|
||||
- Added proper error handling for index operations
|
||||
|
||||
---
|
||||
|
||||
## Version 3.2.1 - Privacy & Bug Fixes (January 24, 2026)
|
||||
|
||||
### 🔒 Privacy Improvements
|
||||
- **Removed disk space display** - No longer shows PS5 storage capacity (Free/Used/Total)
|
||||
- **Fixed storage query bug** - Resolved syntax errors in statvfs code that could cause crashes
|
||||
- **Privacy-focused** - Application no longer queries or displays sensitive storage information
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- Fixed potential crash from malformed storage query code
|
||||
- Removed unused protocol busy flag that caused compiler warnings
|
||||
- Cleaned up all storage-related UI elements and backend code
|
||||
|
||||
---
|
||||
|
||||
## Version 3.2.0 - Mobile Improvements & Bug Fixes (January 24, 2026)
|
||||
|
||||
### 🎯 Improvements
|
||||
|
||||
#### 💾 Accurate Storage Display
|
||||
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
|
||||
- **Accounts for reserved space** - Same calculation as PS5 system
|
||||
- Uses proper calculation: `(total_blocks - reserved) * block_size`
|
||||
|
||||
#### 📱 Mobile App Improvements
|
||||
- **Multi-select mode** - Toggle button (☑️/✅) to select multiple files/folders
|
||||
- **Folder browser** - Visual folder picker for Copy/Move destinations with recursive navigation
|
||||
- **Sorted file list** - Folders always displayed at top, then files (alphabetically)
|
||||
- **Exit confirmation** - Prompt dialog when pressing back button
|
||||
- **Delete folders** - Now supports folder deletion (not just files)
|
||||
- **Batch operations** - Delete/download multiple items at once
|
||||
|
||||
#### 3. 🐛 Bug Fixes
|
||||
- **Fixed empty folder deletion** - No more "Unexpected response" error when deleting empty directories
|
||||
- **Fixed Copy/Move crash** - No longer crashes when selecting destination folder (saved file info before browsing)
|
||||
- **Fixed multi-select count** - Correct item count in delete confirmation dialog
|
||||
- **Fixed folder navigation** - Can enter folders after actions complete (auto-exit multi-select mode)
|
||||
|
||||
---
|
||||
|
||||
## Version 3.1.0 - Parallel Chunked Uploads (January 2026)
|
||||
|
||||
### 🚀 Performance Improvements
|
||||
|
||||
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
|
||||
- **Large files (>100MB)** are now split into 500MB chunks
|
||||
- **4 parallel connections** upload chunks simultaneously
|
||||
- **Result:** ~104 MB/s upload speed (2x faster than before!)
|
||||
- **Fixed race condition:** First chunk creates file before others start
|
||||
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
|
||||
|
||||
#### 2. 📊 Real-time Progress Display
|
||||
- **Speed display** now updates in real-time during chunked uploads
|
||||
- **ETA display** shows accurate time remaining
|
||||
- **Progress bar** correctly shows 0-100% for chunked files
|
||||
- No more speed dropping to 0 during large file uploads
|
||||
|
||||
#### 3. 💾 Storage Display Improvements
|
||||
- **Changed label** to "Data Storage (/data)" for clarity
|
||||
- **Uses statfs with f_bavail** for more accurate available space
|
||||
- **Real-time updates** every 5 seconds when connected
|
||||
|
||||
---
|
||||
|
||||
## Version 3.0.0 - Mobile Client & Path Fixes (January 2026)
|
||||
|
||||
### 🐛 Critical Bug Fixes
|
||||
|
||||
#### 1. 🔧 Path Normalization Fix
|
||||
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
|
||||
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
|
||||
- **Solution:** Added `NormalizePath()` function in both client and server
|
||||
- **Result:** 100% reliable directory creation and file uploads
|
||||
|
||||
#### 2. 📱 Android Mobile Client
|
||||
- **NEW:** Full-featured Android app for PS5 file management
|
||||
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
|
||||
- Upload files from phone to PS5
|
||||
- Download files from PS5 with Share option
|
||||
- Browse PS5 filesystem
|
||||
- Create new folders
|
||||
- Rename files/folders
|
||||
- Copy files/folders
|
||||
- Move files/folders
|
||||
- Delete files
|
||||
- Favorites for quick navigation
|
||||
- Debug Log with Copy to clipboard
|
||||
- Transfer History tracking
|
||||
|
||||
#### 3. 🛠️ Server Improvements
|
||||
- Path normalization in all handler functions
|
||||
- More robust error handling
|
||||
- Improved stability for parallel uploads
|
||||
|
||||
---
|
||||
|
||||
## Version 2.1.0 - Performance & History (January 2026)
|
||||
|
||||
### 🚀 Performance Optimizations
|
||||
|
||||
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
|
||||
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
|
||||
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
|
||||
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
|
||||
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
|
||||
- **Per-file:** 11-14 MB/s sustained per connection
|
||||
|
||||
#### 2. 📊 Transfer History
|
||||
- Complete history of all uploads and downloads
|
||||
- Success/Failed status tracking with error messages
|
||||
- Speed statistics (average, min, max)
|
||||
- Export to CSV/JSON for analysis
|
||||
- Persistent storage across sessions
|
||||
|
||||
#### 3. 🔄 Auto-Clear History on Startup
|
||||
- Optional checkbox to clear history automatically
|
||||
- Useful for keeping UI clean between sessions
|
||||
- Setting saved in `ps5_upload_settings.json`
|
||||
|
||||
#### 4. 🖥️ Maximized Window UI
|
||||
- Application opens in full-screen mode by default
|
||||
- Better visibility for large file transfers
|
||||
- Can be resized/restored as needed
|
||||
|
||||
### Technical Improvements
|
||||
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
|
||||
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
|
||||
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
|
||||
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
|
||||
|
||||
---
|
||||
|
||||
## Version 2.0.0 - Download & Multi-PS5 (January 2026)
|
||||
|
||||
### 🎉 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
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
# PS5 Upload Protocol Specification
|
||||
|
||||
## Overview
|
||||
Custom binary protocol for high-speed file transfers between PC and PS5.
|
||||
Port: 9113
|
||||
|
||||
## Message Format
|
||||
|
||||
All messages follow this structure:
|
||||
```
|
||||
[Command (1 byte)][Data Length (4 bytes)][Data (variable)]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Client → Server
|
||||
|
||||
| Command | Value | Description | Data Format |
|
||||
|---------|-------|-------------|-------------|
|
||||
| PING | 0x01 | Test connection | None |
|
||||
| LIST_STORAGE | 0x02 | Get storage info | None |
|
||||
| LIST_DIR | 0x03 | List directory | Path (null-terminated string) |
|
||||
| CREATE_DIR | 0x04 | Create directory | Path (null-terminated string) |
|
||||
| DELETE_FILE | 0x05 | Delete file | Path (null-terminated string) |
|
||||
| DELETE_DIR | 0x06 | Delete directory | Path (null-terminated string) |
|
||||
| START_UPLOAD | 0x10 | Start file upload | Path (null-terminated) + File size (8 bytes) |
|
||||
| UPLOAD_CHUNK | 0x11 | Upload file chunk | Chunk data (raw bytes) |
|
||||
| END_UPLOAD | 0x12 | Finish upload | None |
|
||||
| SHUTDOWN | 0xFF | Shutdown server | None |
|
||||
|
||||
### Server → Client
|
||||
|
||||
| Response | Value | Description | Data Format |
|
||||
|----------|-------|-------------|-------------|
|
||||
| OK | 0x01 | Success | Optional message |
|
||||
| ERROR | 0x02 | Error occurred | Error message (null-terminated) |
|
||||
| DATA | 0x03 | Response data | Variable format |
|
||||
| READY | 0x04 | Ready for next chunk | None |
|
||||
| PROGRESS | 0x05 | Upload progress | Bytes written (8 bytes) |
|
||||
|
||||
## Data Formats
|
||||
|
||||
### LIST_STORAGE Response
|
||||
```
|
||||
[Drive count (4 bytes)]
|
||||
For each drive:
|
||||
[Path length (2 bytes)][Path][Total space (8 bytes)][Free space (8 bytes)]
|
||||
```
|
||||
|
||||
### LIST_DIR Response
|
||||
```
|
||||
[Entry count (4 bytes)]
|
||||
For each entry:
|
||||
[Type (1 byte: 0=file, 1=dir)][Name length (2 bytes)][Name][Size (8 bytes)][Timestamp (8 bytes)]
|
||||
```
|
||||
|
||||
## Flow Examples
|
||||
|
||||
### Upload File
|
||||
```
|
||||
Client: START_UPLOAD "/data/test.bin" size=1048576
|
||||
Server: READY
|
||||
Client: UPLOAD_CHUNK [4MB data]
|
||||
Server: PROGRESS bytes=4194304
|
||||
Client: UPLOAD_CHUNK [remaining data]
|
||||
Server: PROGRESS bytes=1048576
|
||||
Client: END_UPLOAD
|
||||
Server: OK
|
||||
```
|
||||
|
||||
### List Directory
|
||||
```
|
||||
Client: LIST_DIR "/data"
|
||||
Server: DATA [directory listing]
|
||||
```
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
- 4MB socket buffers
|
||||
- SO_NOSIGPIPE enabled
|
||||
- TCP_NODELAY for low latency
|
||||
- Direct disk writes (no temp files)
|
||||
- Streaming chunks (no full file buffering)
|
||||
@@ -2,37 +2,50 @@
|
||||
|
||||
**By Manos**
|
||||
|
||||
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s** transfer speeds using a custom binary protocol.
|
||||
**Version 4.1.0 - Memory Optimization & Large File Handling**
|
||||
|
||||

|
||||

|
||||
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **104+ MB/s** upload speeds for large files using parallel chunked uploads!
|
||||
|
||||
⭐ **NEW in v4.1.0:**
|
||||
- 🧠 **Memory Optimization** - Handles 156K+ files without crashes
|
||||
- ⚡ **Reduced UI Stuttering** - Batch logging for smoother experience
|
||||
- 🔧 **Lock-Free Operations** - ConcurrentDictionary for thread-safety
|
||||
- 🧹 **Auto Memory Cleanup** - Completed files removed from tracking
|
||||
- � **Incremental Processing** - No massive dictionary allocations
|
||||
|
||||
📱 **Android Mobile Client available!**
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📦 What's Included
|
||||
## 📦 Downloads
|
||||
|
||||
Download the latest release from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page:
|
||||
|
||||
### 1. PS5 Server Payload
|
||||
- **File:** `payload/ps5_upload_server.elf`
|
||||
- **File:** `ps5_upload_server.elf`
|
||||
- **Port:** 9113
|
||||
- **Protocol:** Custom binary (optimized for speed)
|
||||
- **Features:**
|
||||
- 4MB socket buffers
|
||||
- SO_NOSIGPIPE enabled
|
||||
- TCP_NODELAY for low latency
|
||||
- Direct disk writes (no temp files)
|
||||
- Multi-threaded client handling
|
||||
- High-speed custom binary protocol
|
||||
- 16MB socket buffers for maximum throughput
|
||||
- Multi-threaded client handling
|
||||
|
||||
### 2. Windows GUI Client
|
||||
- **File:** `client/bin/Release/net6.0-windows/win-x64/publish/PS5Upload.exe`
|
||||
- **Framework:** .NET 6.0 WPF
|
||||
- **Features:**
|
||||
- Modern dark theme UI
|
||||
- Drag & drop file/folder upload
|
||||
- Browse PS5 filesystem
|
||||
- Real-time upload progress
|
||||
- Speed indicator (MB/s)
|
||||
- Storage space display
|
||||
- Recursive folder upload
|
||||
- **File:** `PS5Upload.exe`
|
||||
- Self-contained executable (no .NET installation required)
|
||||
- Modern dark theme UI
|
||||
- Drag & drop file/folder upload
|
||||
- Real-time upload progress with speed tracking
|
||||
- Transfer History with success/failed tracking
|
||||
- Privacy-focused (no storage monitoring)
|
||||
|
||||
### 3. 📱 Android Mobile Client
|
||||
- **File:** `PS5UploadMobile.apk`
|
||||
- Upload/download files from your phone
|
||||
- Multi-PS5 Profiles support
|
||||
- Browse PS5 filesystem
|
||||
- Favorites for quick navigation
|
||||
|
||||
---
|
||||
|
||||
@@ -40,17 +53,13 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
|
||||
|
||||
### Step 1: Load PS5 Payload
|
||||
|
||||
1. Copy `payload/ps5_upload_server.elf` to your PS5:
|
||||
```
|
||||
/data/etaHEN/payloads/ps5_upload_server.elf
|
||||
```
|
||||
1. Download `ps5_upload_server.elf` from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page
|
||||
|
||||
2. Load the payload with elfldr
|
||||
2. Copy it to your PS5: `/data/etaHEN/payloads/ps5_upload_server.elf`
|
||||
|
||||
3. You should see notification:
|
||||
```
|
||||
PS5 Upload Server: 192.168.0.XXX:9113 - By Manos
|
||||
```
|
||||
3. Load the payload with elfldr
|
||||
|
||||
4. You should see notification: `PS5 Upload Server: 192.168.0.XXX:9113 - By Manos`
|
||||
|
||||
### Step 2: Run Windows Client
|
||||
|
||||
@@ -76,73 +85,54 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
|
||||
✅ **Robust Error Handling** - Graceful failures
|
||||
|
||||
### Windows Client
|
||||
✅ **Modern UI** - Dark theme, clean design
|
||||
✅ **Modern UI** - Dark theme, maximized window for better visibility
|
||||
✅ **Drag & Drop** - Files and folders
|
||||
✅ **Browse PS5** - Navigate filesystem
|
||||
✅ **Real-time Progress** - Speed & percentage
|
||||
✅ **Real-time Progress** - Speed & percentage tracking
|
||||
✅ **Optimized Upload** - 8 parallel connections for 88-110 MB/s aggregate speed
|
||||
✅ **Transfer History** - Track all uploads/downloads with success/failed status
|
||||
✅ **Auto-Clear History** - Optional auto-clear on startup
|
||||
✅ **Folder Upload** - Recursive directory upload
|
||||
✅ **Storage Info** - Free space display
|
||||
✅ **Download Files** - Download from PS5 to PC
|
||||
✅ **🔍 Smart Search** - Full filesystem indexing with instant search (NEW v3.3)
|
||||
- Wildcard support: `*.pkg`, `*loader*`, `game*.bin`
|
||||
- Size filters: `size:>1GB`, `size:<100MB`
|
||||
- Case-insensitive matching
|
||||
- Search both filename and full path
|
||||
- Double-click to navigate to folder
|
||||
✅ **💻 Shell Terminal** - Execute commands directly on PS5 (NEW v3.3)
|
||||
- Run system commands remotely
|
||||
- Real-time output display
|
||||
- Command history
|
||||
- Working directory support
|
||||
✅ **Favorites/Bookmarks** - Quick navigation to saved paths
|
||||
✅ **Multi-PS5 Support** - Save and switch between multiple PS5 profiles
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
| Network | Expected Speed |
|
||||
|---------|----------------|
|
||||
| **Gigabit Ethernet** | 100-120 MB/s |
|
||||
| **WiFi 6 (5GHz)** | 60-80 MB/s |
|
||||
| **WiFi 5 (5GHz)** | 40-60 MB/s |
|
||||
### v3.1.0 Optimized Results:
|
||||
- ✅ **104+ MB/s** upload speed for large files (parallel chunked uploads)
|
||||
- ✅ **4 parallel chunk connections** for maximum throughput
|
||||
- ✅ **500MB chunks** - optimal size for PS5 disk performance
|
||||
- ✅ **Real-time progress** - Speed/ETA updates during upload
|
||||
- ✅ **Zero chunk failures** - Fixed race conditions
|
||||
- ✅ **Fully responsive UI** throughout upload
|
||||
- ✅ **No memory leaks** - stable operation
|
||||
|
||||
**Note:** Speeds depend on network quality and PS5 disk performance.
|
||||
### Previous Results (v1.3.0):
|
||||
- ✅ **42,801 files** uploaded successfully
|
||||
- ✅ **60-150 MB/s** sustained throughput
|
||||
|
||||
---
|
||||
| Network | Expected Upload Speed | Expected Download Speed |
|
||||
|---------|----------------------|------------------------|
|
||||
| **Gigabit Ethernet** | 88-110 MB/s (aggregate) | 100-120 MB/s |
|
||||
| **WiFi 6 (5GHz)** | 32-70 MB/s (aggregate) | 60-80 MB/s |
|
||||
| **WiFi 5 (5GHz)** | 20-50 MB/s (aggregate) | 40-60 MB/s |
|
||||
|
||||
## 🔧 Building from Source
|
||||
**Note:** Upload speeds depend on network quality and PS5 disk write performance. WiFi has ~40-60% overhead compared to Ethernet.
|
||||
|
||||
### PS5 Payload
|
||||
|
||||
Requirements:
|
||||
- PS5 SDK (prospero-clang)
|
||||
- WSL or Linux
|
||||
|
||||
```bash
|
||||
cd payload
|
||||
bash compile.sh
|
||||
```
|
||||
|
||||
### Windows Client
|
||||
|
||||
Requirements:
|
||||
- .NET 6.0 SDK or later
|
||||
- Windows 10/11
|
||||
|
||||
```cmd
|
||||
cd client
|
||||
build.bat
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```cmd
|
||||
dotnet publish -c Release -r win-x64 --self-contained true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Protocol Specification
|
||||
|
||||
See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
|
||||
|
||||
### Commands
|
||||
- `PING` - Test connection
|
||||
- `LIST_STORAGE` - Get storage info
|
||||
- `LIST_DIR` - List directory contents
|
||||
- `CREATE_DIR` - Create directory
|
||||
- `DELETE_FILE` - Delete file
|
||||
- `DELETE_DIR` - Delete directory
|
||||
- `START_UPLOAD` - Begin file upload
|
||||
- `UPLOAD_CHUNK` - Upload file chunk
|
||||
- `END_UPLOAD` - Finish upload
|
||||
- `SHUTDOWN` - Shutdown server
|
||||
|
||||
---
|
||||
|
||||
@@ -174,9 +164,156 @@ See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.2.0
|
||||
|
||||
### 🎯 Improvements:
|
||||
|
||||
#### 1. 💾 Accurate Storage Display
|
||||
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
|
||||
- **Accounts for reserved space** - Same calculation as PS5 system
|
||||
|
||||
#### 2. 📱 Mobile App Improvements
|
||||
- **Multi-select mode** - Toggle button to select multiple files/folders
|
||||
- **Folder browser** - Visual folder picker for Copy/Move destinations
|
||||
- **Sorted file list** - Folders always displayed at top
|
||||
- **Exit confirmation** - Prompt when pressing back button
|
||||
- **Delete folders** - Now supports folder deletion (not just files)
|
||||
- **Batch operations** - Delete/download multiple items at once
|
||||
|
||||
#### 3. 🐛 Bug Fixes
|
||||
- **Fixed empty folder deletion** - No more "Unexpected response" error
|
||||
- **Fixed Copy/Move crash** - No longer crashes when selecting destination
|
||||
- **Fixed multi-select count** - Correct item count in delete confirmation
|
||||
- **Fixed folder navigation** - Can enter folders after actions complete
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.1.0
|
||||
|
||||
### 🚀 Performance Improvements:
|
||||
|
||||
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
|
||||
- **Large files (>100MB)** are now split into 500MB chunks
|
||||
- **4 parallel connections** upload chunks simultaneously
|
||||
- **Result:** ~104 MB/s upload speed (2x faster than before!)
|
||||
- **Fixed race condition:** First chunk creates file before others start
|
||||
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
|
||||
|
||||
#### 2. 📊 Real-time Progress Display
|
||||
- **Speed display** now updates in real-time during chunked uploads
|
||||
- **ETA display** shows accurate time remaining
|
||||
- **Progress bar** correctly shows 0-100% for chunked files
|
||||
- No more speed dropping to 0 during large file uploads
|
||||
|
||||
#### 3. 💾 Storage Display Improvements
|
||||
- **Changed label** to "Data Storage (/data)" for clarity
|
||||
- **Uses statfs with f_bavail** for more accurate available space
|
||||
- **Real-time updates** every 5 seconds when connected
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.0.0
|
||||
|
||||
### 🐛 Critical Bug Fixes:
|
||||
|
||||
#### 1. 🔧 Path Normalization Fix
|
||||
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
|
||||
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
|
||||
- **Solution:** Added `NormalizePath()` function in both client and server
|
||||
- **Result:** 100% reliable directory creation and file uploads
|
||||
|
||||
#### 2. 📱 Android Mobile Client
|
||||
- **NEW:** Full-featured Android app for PS5 file management
|
||||
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
|
||||
- Upload files from phone to PS5
|
||||
- Download files from PS5 with Share option
|
||||
- Browse PS5 filesystem
|
||||
- Favorites for quick navigation
|
||||
- Debug Log with Copy to clipboard
|
||||
- Transfer History tracking
|
||||
|
||||
#### 3. 🛠️ Server Improvements
|
||||
- Path normalization in all handler functions
|
||||
- More robust error handling
|
||||
- Improved stability for parallel uploads
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v2.1.0
|
||||
|
||||
### 🚀 Performance Optimizations:
|
||||
|
||||
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
|
||||
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
|
||||
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
|
||||
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
|
||||
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
|
||||
- **Per-file:** 11-14 MB/s sustained per connection
|
||||
|
||||
#### 2. 📊 Transfer History
|
||||
- Complete history of all uploads and downloads
|
||||
- Success/Failed status tracking with error messages
|
||||
- Speed statistics (average, min, max)
|
||||
- Export to CSV/JSON for analysis
|
||||
- Persistent storage across sessions
|
||||
|
||||
#### 3. 🔄 Auto-Clear History on Startup
|
||||
- Optional checkbox to clear history automatically
|
||||
- Useful for keeping UI clean between sessions
|
||||
- Setting saved in `ps5_upload_settings.json`
|
||||
|
||||
#### 4. 🖥️ Maximized Window UI
|
||||
- Application opens in full-screen mode by default
|
||||
- Better visibility for large file transfers
|
||||
- Can be resized/restored as needed
|
||||
|
||||
### Technical Improvements:
|
||||
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
|
||||
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
|
||||
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
|
||||
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
|
||||
|
||||
---
|
||||
|
||||
## 📝 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
|
||||
This software is provided as-is for personal use.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<Application x:Class="PS5Upload.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -1,8 +0,0 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace PS5Upload
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
}
|
||||
+814
-124
File diff suppressed because it is too large.
Load diff
+2168
-266
File diff suppressed because it is too large.
Load diff
@@ -1,29 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AssemblyName>PS5Upload</AssemblyName>
|
||||
<RootNamespace>PS5Upload</RootNamespace>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Manos</Authors>
|
||||
<Description>High-Speed File Upload Tool for PS5</Description>
|
||||
<Copyright>Copyright © 2026 Manos</Copyright>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<ApplicationIcon>ps5_logo_fixed.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="ps5_logo.png" />
|
||||
<None Include="ps5_logo.ico">
|
||||
<Link>ps5_logo.ico</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,453 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PS5Upload
|
||||
{
|
||||
// Protocol commands
|
||||
public enum Command : byte
|
||||
{
|
||||
Ping = 0x01,
|
||||
ListStorage = 0x02,
|
||||
ListDir = 0x03,
|
||||
CreateDir = 0x04,
|
||||
DeleteFile = 0x05,
|
||||
DeleteDir = 0x06,
|
||||
Rename = 0x07,
|
||||
CopyFile = 0x08,
|
||||
MoveFile = 0x09,
|
||||
StartUpload = 0x10,
|
||||
UploadChunk = 0x11,
|
||||
EndUpload = 0x12,
|
||||
Shutdown = 0xFF
|
||||
}
|
||||
|
||||
// Protocol responses
|
||||
public enum Response : byte
|
||||
{
|
||||
Ok = 0x01,
|
||||
Error = 0x02,
|
||||
Data = 0x03,
|
||||
Ready = 0x04,
|
||||
Progress = 0x05
|
||||
}
|
||||
|
||||
public class PS5Protocol : IDisposable
|
||||
{
|
||||
private TcpClient? _client;
|
||||
private NetworkStream? _stream;
|
||||
private const int BufferSize = 32 * 1024 * 1024; // 32MB for maximum throughput
|
||||
|
||||
public bool IsConnected => _client?.Connected ?? false;
|
||||
|
||||
public async Task<bool> ConnectAsync(string ipAddress, int port = 9113)
|
||||
{
|
||||
try
|
||||
{
|
||||
_client = new TcpClient();
|
||||
_client.ReceiveBufferSize = 64 * 1024 * 1024; // 64MB for maximum TCP throughput
|
||||
_client.SendBufferSize = 64 * 1024 * 1024; // 64MB for maximum TCP throughput
|
||||
_client.NoDelay = true;
|
||||
_client.LingerState = new System.Net.Sockets.LingerOption(false, 0);
|
||||
|
||||
await _client.ConnectAsync(ipAddress, port);
|
||||
_stream = _client.GetStream();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_stream?.Close();
|
||||
_client?.Close();
|
||||
_stream = null;
|
||||
_client = null;
|
||||
}
|
||||
|
||||
private async Task SendCommandAsync(Command cmd, byte[]? data = null)
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
|
||||
byte[] header = new byte[5];
|
||||
header[0] = (byte)cmd;
|
||||
|
||||
uint dataLen = data != null ? (uint)data.Length : 0;
|
||||
BitConverter.GetBytes(dataLen).CopyTo(header, 1);
|
||||
|
||||
await _stream.WriteAsync(header, 0, 5);
|
||||
if (data != null && data.Length > 0)
|
||||
{
|
||||
await _stream.WriteAsync(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(Response response, byte[] data)> ReceiveResponseAsync()
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
|
||||
byte[] header = new byte[5];
|
||||
await ReadExactAsync(header, 5);
|
||||
|
||||
Response response = (Response)header[0];
|
||||
uint dataLen = BitConverter.ToUInt32(header, 1);
|
||||
|
||||
byte[] data = new byte[dataLen];
|
||||
if (dataLen > 0)
|
||||
{
|
||||
await ReadExactAsync(data, (int)dataLen);
|
||||
}
|
||||
|
||||
return (response, data);
|
||||
}
|
||||
|
||||
private async Task ReadExactAsync(byte[] buffer, int count, int timeoutMs = 30000)
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
|
||||
int offset = 0;
|
||||
using var cts = new CancellationTokenSource(timeoutMs);
|
||||
|
||||
try
|
||||
{
|
||||
while (offset < count)
|
||||
{
|
||||
int read = await _stream.ReadAsync(buffer, offset, count - offset, cts.Token);
|
||||
if (read == 0) throw new IOException("Connection closed");
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw new TimeoutException("Read timeout");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PingAsync()
|
||||
{
|
||||
await SendCommandAsync(Command.Ping);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<StorageInfo[]> ListStorageAsync()
|
||||
{
|
||||
await SendCommandAsync(Command.ListStorage);
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
|
||||
if (response != Response.Data) return Array.Empty<StorageInfo>();
|
||||
|
||||
using var ms = new MemoryStream(data);
|
||||
using var br = new BinaryReader(ms);
|
||||
|
||||
int count = br.ReadInt32();
|
||||
var result = new StorageInfo[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ushort pathLen = br.ReadUInt16();
|
||||
string path = Encoding.UTF8.GetString(br.ReadBytes(pathLen));
|
||||
long total = br.ReadInt64();
|
||||
long free = br.ReadInt64();
|
||||
|
||||
result[i] = new StorageInfo { Path = path, TotalBytes = total, FreeBytes = free };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<FileEntry[]> ListDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.ListDir, pathBytes);
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
|
||||
if (response != Response.Data) return Array.Empty<FileEntry>();
|
||||
|
||||
using var ms = new MemoryStream(data);
|
||||
using var br = new BinaryReader(ms);
|
||||
|
||||
int count = br.ReadInt32();
|
||||
|
||||
// Handle empty directory
|
||||
if (count <= 0) return Array.Empty<FileEntry>();
|
||||
|
||||
var result = new FileEntry[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
byte type = br.ReadByte();
|
||||
ushort nameLen = br.ReadUInt16();
|
||||
string name = Encoding.UTF8.GetString(br.ReadBytes(nameLen));
|
||||
long size = br.ReadInt64();
|
||||
long timestamp = br.ReadInt64();
|
||||
|
||||
// Clamp timestamp to valid DateTimeOffset range
|
||||
DateTime dt;
|
||||
try
|
||||
{
|
||||
if (timestamp < -62135596800 || timestamp > 253402300799)
|
||||
{
|
||||
dt = DateTime.Now; // Use current time for invalid timestamps
|
||||
}
|
||||
else
|
||||
{
|
||||
dt = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
dt = DateTime.Now;
|
||||
}
|
||||
|
||||
result[i] = new FileEntry
|
||||
{
|
||||
Name = name,
|
||||
IsDirectory = type == 1,
|
||||
Size = size,
|
||||
Timestamp = dt
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> CreateDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.CreateDir, pathBytes);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFileAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.DeleteFile, pathBytes);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.DeleteDir, pathBytes);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> RenameAsync(string oldPath, string newPath)
|
||||
{
|
||||
byte[] oldBytes = Encoding.UTF8.GetBytes(oldPath + "\0");
|
||||
byte[] newBytes = Encoding.UTF8.GetBytes(newPath + "\0");
|
||||
byte[] data = new byte[oldBytes.Length + newBytes.Length];
|
||||
Array.Copy(oldBytes, 0, data, 0, oldBytes.Length);
|
||||
Array.Copy(newBytes, 0, data, oldBytes.Length, newBytes.Length);
|
||||
|
||||
await SendCommandAsync(Command.Rename, data);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> CopyFileAsync(string srcPath, string dstPath)
|
||||
{
|
||||
byte[] srcBytes = Encoding.UTF8.GetBytes(srcPath + "\0");
|
||||
byte[] dstBytes = Encoding.UTF8.GetBytes(dstPath + "\0");
|
||||
byte[] data = new byte[srcBytes.Length + dstBytes.Length];
|
||||
Array.Copy(srcBytes, 0, data, 0, srcBytes.Length);
|
||||
Array.Copy(dstBytes, 0, data, srcBytes.Length, dstBytes.Length);
|
||||
|
||||
await SendCommandAsync(Command.CopyFile, data);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> MoveFileAsync(string srcPath, string dstPath)
|
||||
{
|
||||
byte[] srcBytes = Encoding.UTF8.GetBytes(srcPath + "\0");
|
||||
byte[] dstBytes = Encoding.UTF8.GetBytes(dstPath + "\0");
|
||||
byte[] data = new byte[srcBytes.Length + dstBytes.Length];
|
||||
Array.Copy(srcBytes, 0, data, 0, srcBytes.Length);
|
||||
Array.Copy(dstBytes, 0, data, srcBytes.Length, dstBytes.Length);
|
||||
|
||||
await SendCommandAsync(Command.MoveFile, data);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> UploadFileAsync(string localPath, string remotePath, IProgress<UploadProgress>? progress = null, CancellationToken cancellationToken = default, long chunkOffset = 0, long chunkSize = 0)
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(localPath);
|
||||
if (!fileInfo.Exists) return false;
|
||||
|
||||
// Determine actual upload size
|
||||
long uploadSize = chunkSize > 0 ? chunkSize : fileInfo.Length;
|
||||
|
||||
// Send START_UPLOAD with optional chunk offset
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(remotePath);
|
||||
byte[] startData = new byte[pathBytes.Length + 1 + 8 + 8]; // path + null + size + offset
|
||||
Array.Copy(pathBytes, 0, startData, 0, pathBytes.Length);
|
||||
BitConverter.GetBytes(fileInfo.Length).CopyTo(startData, pathBytes.Length + 1);
|
||||
BitConverter.GetBytes(chunkOffset).CopyTo(startData, pathBytes.Length + 9);
|
||||
|
||||
await SendCommandAsync(Command.StartUpload, startData);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
|
||||
if (response != Response.Ready) return false;
|
||||
|
||||
// Upload chunks - simple async approach
|
||||
long totalSent = 0;
|
||||
var startTime = DateTime.Now;
|
||||
double avgSpeed = 0;
|
||||
|
||||
byte[] sendBuffer = new byte[5 + BufferSize];
|
||||
sendBuffer[0] = (byte)Command.UploadChunk;
|
||||
|
||||
using (FileStream fs = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.SequentialScan))
|
||||
{
|
||||
// Seek to chunk offset if specified
|
||||
if (chunkOffset > 0)
|
||||
{
|
||||
fs.Seek(chunkOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
int chunksSent = 0;
|
||||
int bytesRead;
|
||||
long bytesRemaining = uploadSize;
|
||||
|
||||
while (bytesRemaining > 0 && (bytesRead = await fs.ReadAsync(sendBuffer, 5, (int)Math.Min(BufferSize, bytesRemaining), cancellationToken)) > 0)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
|
||||
BitConverter.GetBytes((uint)bytesRead).CopyTo(sendBuffer, 1);
|
||||
|
||||
if (_stream == null) return false;
|
||||
|
||||
await _stream.WriteAsync(sendBuffer, 0, 5 + bytesRead, cancellationToken);
|
||||
|
||||
totalSent += bytesRead;
|
||||
bytesRemaining -= bytesRead;
|
||||
chunksSent++;
|
||||
|
||||
if (chunksSent % 5 == 0 || bytesRemaining == 0)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
double currentSpeed = elapsed.TotalSeconds > 0 ? totalSent / elapsed.TotalSeconds : 0;
|
||||
avgSpeed = currentSpeed;
|
||||
|
||||
TimeSpan eta = currentSpeed > 0
|
||||
? TimeSpan.FromSeconds(bytesRemaining / currentSpeed)
|
||||
: TimeSpan.Zero;
|
||||
|
||||
progress?.Report(new UploadProgress
|
||||
{
|
||||
BytesSent = chunkOffset + totalSent,
|
||||
TotalBytes = fileInfo.Length,
|
||||
SpeedBytesPerSecond = currentSpeed,
|
||||
AverageSpeedBytesPerSecond = avgSpeed,
|
||||
ElapsedTime = elapsed,
|
||||
EstimatedTimeRemaining = eta,
|
||||
CurrentFileName = fileInfo.Name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send END_UPLOAD - fire-and-forget for maximum speed
|
||||
// Don't wait for response to avoid PS5 payload bottleneck
|
||||
try
|
||||
{
|
||||
await SendCommandAsync(Command.EndUpload);
|
||||
// Immediately return success without waiting for response
|
||||
// This eliminates the PS5 response handling bottleneck
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // Assume success if connection issues
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public class StorageInfo
|
||||
{
|
||||
public string Path { get; set; } = "";
|
||||
public long TotalBytes { get; set; }
|
||||
public long FreeBytes { get; set; }
|
||||
}
|
||||
|
||||
public class FileEntry
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public bool IsDirectory { get; set; }
|
||||
public long Size { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
|
||||
public class UploadProgress
|
||||
{
|
||||
public long BytesSent { get; set; }
|
||||
public long TotalBytes { get; set; }
|
||||
public double SpeedBytesPerSecond { get; set; }
|
||||
public double AverageSpeedBytesPerSecond { get; set; }
|
||||
public TimeSpan ElapsedTime { get; set; }
|
||||
public TimeSpan EstimatedTimeRemaining { get; set; }
|
||||
public string CurrentFileName { get; set; } = "";
|
||||
|
||||
// For folder progress
|
||||
public int CurrentFileIndex { get; set; }
|
||||
public int TotalFiles { get; set; }
|
||||
public long TotalFolderBytes { get; set; }
|
||||
public long TotalFolderBytesSent { get; set; }
|
||||
}
|
||||
|
||||
// High-speed parallel uploader using multiple connections
|
||||
public class ParallelUploader : IDisposable
|
||||
{
|
||||
private readonly string _ipAddress;
|
||||
private readonly int _port;
|
||||
private readonly int _connectionCount;
|
||||
private const int ChunkSize = 4 * 1024 * 1024; // 4MB chunks per connection
|
||||
|
||||
public ParallelUploader(string ipAddress, int port = 9113, int connectionCount = 4)
|
||||
{
|
||||
_ipAddress = ipAddress;
|
||||
_port = port;
|
||||
_connectionCount = connectionCount;
|
||||
}
|
||||
|
||||
public async Task<bool> UploadFileAsync(string localPath, string remotePath, IProgress<UploadProgress>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(localPath);
|
||||
if (!fileInfo.Exists) return false;
|
||||
|
||||
// For small files, use single connection
|
||||
if (fileInfo.Length < ChunkSize * 2)
|
||||
{
|
||||
using var protocol = new PS5Protocol();
|
||||
if (!await protocol.ConnectAsync(_ipAddress, _port)) return false;
|
||||
return await protocol.UploadFileAsync(localPath, remotePath, progress, cancellationToken);
|
||||
}
|
||||
|
||||
// For large files, use parallel connections
|
||||
// This requires server support for chunked/parallel uploads
|
||||
// For now, fall back to single connection
|
||||
using var proto = new PS5Protocol();
|
||||
if (!await proto.ConnectAsync(_ipAddress, _port)) return false;
|
||||
return await proto.UploadFileAsync(localPath, remotePath, progress, cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
@echo off
|
||||
echo ================================================
|
||||
echo PS5 Upload Client - Building
|
||||
echo By Manos
|
||||
echo ================================================
|
||||
echo.
|
||||
|
||||
echo [+] Building Windows GUI Client...
|
||||
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true
|
||||
|
||||
if exist "bin\Release\net6.0-windows\win-x64\publish\PS5Upload.exe" (
|
||||
echo.
|
||||
echo ================================================
|
||||
echo [+] SUCCESS! Built PS5Upload.exe
|
||||
echo ================================================
|
||||
echo.
|
||||
echo Location: bin\Release\net6.0-windows\win-x64\publish\PS5Upload.exe
|
||||
echo.
|
||||
echo Copy PS5Upload.exe to any location and run it!
|
||||
) else (
|
||||
echo.
|
||||
echo ================================================
|
||||
echo [-] BUILD FAILED
|
||||
echo ================================================
|
||||
echo.
|
||||
echo Make sure .NET 6.0 SDK is installed:
|
||||
echo https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
exit /b 1
|
||||
)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,14 @@
|
||||
<?xml version = "1.0" encoding = "UTF-8" ?>
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:PS5UploadMobile"
|
||||
x:Class="PS5UploadMobile.App">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace PS5UploadMobile;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
MainPage = new AppShell();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Shell
|
||||
x:Class="PS5UploadMobile.AppShell"
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:PS5UploadMobile"
|
||||
Shell.FlyoutBehavior="Disabled">
|
||||
|
||||
<ShellContent
|
||||
Title="Home"
|
||||
ContentTemplate="{DataTemplate local:MainPage}"
|
||||
Route="MainPage" />
|
||||
|
||||
</Shell>
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace PS5UploadMobile;
|
||||
|
||||
public partial class AppShell : Shell
|
||||
{
|
||||
public AppShell()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="PS5UploadMobile.MainPage"
|
||||
Title="PS5 Upload Suite">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto,Auto">
|
||||
|
||||
<!-- Top Bar with Menu -->
|
||||
<Grid Grid.Row="0" Padding="10" BackgroundColor="#1E1E1E" ColumnDefinitions="Auto,*,Auto">
|
||||
<Button Grid.Column="0" Text="☰" FontSize="24" BackgroundColor="Transparent"
|
||||
Clicked="OnMenuClicked" WidthRequest="50"/>
|
||||
<Label Grid.Column="1" Text="PS5 Upload Suite" FontSize="20" FontAttributes="Bold"
|
||||
TextColor="White" VerticalOptions="Center" HorizontalOptions="Center"/>
|
||||
<Button Grid.Column="2" Text="⭐" FontSize="20" BackgroundColor="Transparent"
|
||||
Clicked="OnAddFavoriteClicked" WidthRequest="50"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Connection Panel -->
|
||||
<Frame Grid.Row="1" Padding="10" Margin="10,5,10,5" BackgroundColor="#1E1E1E" CornerRadius="8">
|
||||
<VerticalStackLayout Spacing="10">
|
||||
<Label Text="PS5 Connection" FontSize="18" FontAttributes="Bold" TextColor="White"/>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="10">
|
||||
<Entry x:Name="IpEntry" Grid.Column="0" Placeholder="PS5 IP (192.168.0.160)"
|
||||
Text="192.168.0.160" TextColor="White" PlaceholderColor="Gray"/>
|
||||
<Entry x:Name="PortEntry" Grid.Column="1" Placeholder="Port (9113)"
|
||||
Text="9113" Keyboard="Numeric" TextColor="White" PlaceholderColor="Gray"/>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="10">
|
||||
<Button x:Name="ConnectBtn" Grid.Column="0" Text="Connect"
|
||||
Clicked="OnConnectClicked" BackgroundColor="#0078D4"/>
|
||||
<Button x:Name="DisconnectBtn" Grid.Column="1" Text="Disconnect"
|
||||
Clicked="OnDisconnectClicked" BackgroundColor="#D13438" IsEnabled="False"/>
|
||||
</Grid>
|
||||
|
||||
<Label x:Name="StatusLabel" Text="Not connected" FontSize="12"
|
||||
TextColor="Orange" HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Path Navigation -->
|
||||
<Frame Grid.Row="2" Padding="10" Margin="10,0,10,5" BackgroundColor="#2D2D2D" CornerRadius="8">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Button Grid.Column="0" Text="⬆️" Clicked="OnUpDirectoryClicked"
|
||||
WidthRequest="50" BackgroundColor="#3E3E3E"/>
|
||||
<Entry x:Name="PathEntry" Grid.Column="1" Text="/data/"
|
||||
TextColor="White" Margin="10,0"/>
|
||||
<Button Grid.Column="2" Text="🔄" Clicked="OnRefreshClicked"
|
||||
WidthRequest="50" BackgroundColor="#3E3E3E"/>
|
||||
</Grid>
|
||||
</Frame>
|
||||
|
||||
<!-- File List -->
|
||||
<Frame Grid.Row="3" Padding="0" Margin="10,0,10,5" BackgroundColor="#2D2D2D" CornerRadius="8">
|
||||
<CollectionView x:Name="FileListView" SelectionMode="Single"
|
||||
SelectionChanged="OnFileSelected">
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Padding="15,10" ColumnDefinitions="Auto,*,Auto">
|
||||
<Label Grid.Column="0" Text="{Binding Icon}" FontSize="20"
|
||||
VerticalOptions="Center" Margin="0,0,10,0"/>
|
||||
<VerticalStackLayout Grid.Column="1" VerticalOptions="Center">
|
||||
<Label Text="{Binding Name}" TextColor="White" FontSize="16"/>
|
||||
<Label Text="{Binding Size}" TextColor="Gray" FontSize="12"/>
|
||||
</VerticalStackLayout>
|
||||
<Label Grid.Column="2" Text="{Binding Type}" TextColor="Gray"
|
||||
FontSize="12" VerticalOptions="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
</Frame>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<Frame Grid.Row="4" Padding="10" Margin="10,0,10,5" BackgroundColor="#2D2D2D"
|
||||
CornerRadius="8" IsVisible="{Binding IsTransferring}">
|
||||
<VerticalStackLayout Spacing="5">
|
||||
<Label x:Name="ProgressLabel" Text="Uploading..." TextColor="White" FontSize="14"/>
|
||||
<ProgressBar x:Name="ProgressBar" Progress="0" ProgressColor="#0078D4"/>
|
||||
<Label x:Name="ProgressPercentLabel" Text="0%" TextColor="Gray"
|
||||
FontSize="12" HorizontalOptions="Center"/>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<Grid Grid.Row="5" Padding="10" ColumnDefinitions="*,*,*" ColumnSpacing="10">
|
||||
<Button Grid.Column="0" Text="📤 Upload" Clicked="OnUploadClicked"
|
||||
BackgroundColor="#107C10"/>
|
||||
<Button Grid.Column="1" Text="📥 Download" Clicked="OnDownloadClicked"
|
||||
BackgroundColor="#0078D4"/>
|
||||
<Button Grid.Column="2" Text="🗑️ Delete" Clicked="OnDeleteClicked"
|
||||
BackgroundColor="#D13438"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
@@ -0,0 +1,571 @@
|
||||
#nullable enable
|
||||
using PS5UploadMobile.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace PS5UploadMobile;
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
private PS5UploadService _uploadService;
|
||||
private ObservableCollection<FileItem> _files;
|
||||
private string _currentPath = "/data/";
|
||||
private FileItem? _selectedFile;
|
||||
|
||||
// New features
|
||||
private List<string> _favorites = new List<string>();
|
||||
private List<string> _debugLog = new List<string>();
|
||||
private List<string> _transferHistory = new List<string>();
|
||||
private const string FavoritesKey = "ps5_favorites";
|
||||
|
||||
// PS5 Profiles
|
||||
private Dictionary<string, string> _ps5Profiles = new Dictionary<string, string>(); // name -> ip:port
|
||||
private const string ProfilesKey = "ps5_profiles";
|
||||
private string _currentProfileName = "";
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_uploadService = new PS5UploadService();
|
||||
_files = new ObservableCollection<FileItem>();
|
||||
FileListView.ItemsSource = _files;
|
||||
|
||||
_uploadService.LogMessage += OnLogMessage;
|
||||
_uploadService.ProgressChanged += OnProgressChanged;
|
||||
|
||||
LoadFavorites();
|
||||
LoadProfiles();
|
||||
}
|
||||
|
||||
// Menu button handler
|
||||
private async void OnMenuClicked(object sender, EventArgs e)
|
||||
{
|
||||
string action = await DisplayActionSheet("Menu", "Cancel", null,
|
||||
"🎮 PS5 Profiles",
|
||||
"📋 Debug Log",
|
||||
"📊 Transfer History",
|
||||
"⭐ Favorites",
|
||||
"🗑️ Clear Logs");
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "🎮 PS5 Profiles":
|
||||
await ShowProfiles();
|
||||
break;
|
||||
case "📋 Debug Log":
|
||||
await ShowDebugLog();
|
||||
break;
|
||||
case "📊 Transfer History":
|
||||
await ShowTransferHistory();
|
||||
break;
|
||||
case "⭐ Favorites":
|
||||
await ShowFavorites();
|
||||
break;
|
||||
case "🗑️ Clear Logs":
|
||||
_debugLog.Clear();
|
||||
_transferHistory.Clear();
|
||||
await DisplayAlert("Cleared", "Logs cleared", "OK");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowDebugLog()
|
||||
{
|
||||
string log = _debugLog.Count > 0
|
||||
? string.Join("\n", _debugLog.TakeLast(50))
|
||||
: "No log entries yet";
|
||||
|
||||
string action = await DisplayActionSheet("Debug Log", "Close", null, "📋 Copy Log", "👁️ View Log");
|
||||
|
||||
if (action == "📋 Copy Log")
|
||||
{
|
||||
await Clipboard.SetTextAsync(log);
|
||||
await DisplayAlert("Copied", "Log copied to clipboard", "OK");
|
||||
}
|
||||
else if (action == "👁️ View Log")
|
||||
{
|
||||
await DisplayAlert("Debug Log", log, "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowTransferHistory()
|
||||
{
|
||||
string history = _transferHistory.Count > 0
|
||||
? string.Join("\n", _transferHistory.TakeLast(20))
|
||||
: "No transfers yet";
|
||||
await DisplayAlert("Transfer History", history, "OK");
|
||||
}
|
||||
|
||||
private async Task ShowFavorites()
|
||||
{
|
||||
if (_favorites.Count == 0)
|
||||
{
|
||||
await DisplayAlert("Favorites", "No favorites saved.\nTap ⭐ to add current path.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add options for each favorite plus a delete option
|
||||
var options = new List<string>();
|
||||
foreach (var fav in _favorites)
|
||||
{
|
||||
options.Add($"📁 {fav}");
|
||||
}
|
||||
options.Add("🗑️ Delete a favorite");
|
||||
|
||||
string? selected = await DisplayActionSheet("Favorites", "Cancel", null, options.ToArray());
|
||||
|
||||
if (selected == null || selected == "Cancel")
|
||||
return;
|
||||
|
||||
if (selected == "🗑️ Delete a favorite")
|
||||
{
|
||||
await DeleteFavorite();
|
||||
}
|
||||
else if (selected.StartsWith("📁 "))
|
||||
{
|
||||
string path = selected.Substring(3);
|
||||
if (_favorites.Contains(path))
|
||||
{
|
||||
_currentPath = path;
|
||||
PathEntry.Text = _currentPath;
|
||||
await RefreshFileList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteFavorite()
|
||||
{
|
||||
if (_favorites.Count == 0) return;
|
||||
|
||||
string? toDelete = await DisplayActionSheet("Delete which favorite?", "Cancel", null, _favorites.ToArray());
|
||||
if (toDelete != null && toDelete != "Cancel" && _favorites.Contains(toDelete))
|
||||
{
|
||||
_favorites.Remove(toDelete);
|
||||
SaveFavorites();
|
||||
await DisplayAlert("Deleted", $"'{toDelete}' removed from favorites", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnAddFavoriteClicked(object sender, EventArgs e)
|
||||
{
|
||||
if (_favorites.Contains(_currentPath))
|
||||
{
|
||||
bool remove = await DisplayAlert("Favorite Exists",
|
||||
$"Remove '{_currentPath}' from favorites?", "Remove", "Cancel");
|
||||
if (remove)
|
||||
{
|
||||
_favorites.Remove(_currentPath);
|
||||
SaveFavorites();
|
||||
await DisplayAlert("Removed", "Favorite removed", "OK");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_favorites.Add(_currentPath);
|
||||
SaveFavorites();
|
||||
await DisplayAlert("Added", $"'{_currentPath}' added to favorites", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadFavorites()
|
||||
{
|
||||
string? saved = Preferences.Get(FavoritesKey, "");
|
||||
if (!string.IsNullOrEmpty(saved))
|
||||
{
|
||||
_favorites = saved.Split('|').Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveFavorites()
|
||||
{
|
||||
Preferences.Set(FavoritesKey, string.Join("|", _favorites));
|
||||
}
|
||||
|
||||
// PS5 Profile Management
|
||||
private void LoadProfiles()
|
||||
{
|
||||
string? saved = Preferences.Get(ProfilesKey, "");
|
||||
if (!string.IsNullOrEmpty(saved))
|
||||
{
|
||||
_ps5Profiles.Clear();
|
||||
var entries = saved.Split('|').Where(s => !string.IsNullOrEmpty(s));
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var parts = entry.Split('=');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
_ps5Profiles[parts[0]] = parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveProfiles()
|
||||
{
|
||||
var entries = _ps5Profiles.Select(p => $"{p.Key}={p.Value}");
|
||||
Preferences.Set(ProfilesKey, string.Join("|", entries));
|
||||
}
|
||||
|
||||
private async Task ShowProfiles()
|
||||
{
|
||||
var options = new List<string>();
|
||||
|
||||
foreach (var profile in _ps5Profiles)
|
||||
{
|
||||
string marker = profile.Key == _currentProfileName ? "✓ " : "";
|
||||
options.Add($"{marker}🎮 {profile.Key} ({profile.Value})");
|
||||
}
|
||||
options.Add("➕ Add new profile");
|
||||
if (_ps5Profiles.Count > 0)
|
||||
{
|
||||
options.Add("🗑️ Delete a profile");
|
||||
}
|
||||
|
||||
string? selected = await DisplayActionSheet("PS5 Profiles", "Cancel", null, options.ToArray());
|
||||
|
||||
if (selected == null || selected == "Cancel")
|
||||
return;
|
||||
|
||||
if (selected == "➕ Add new profile")
|
||||
{
|
||||
await AddNewProfile();
|
||||
}
|
||||
else if (selected == "🗑️ Delete a profile")
|
||||
{
|
||||
await DeleteProfile();
|
||||
}
|
||||
else if (selected.Contains("🎮"))
|
||||
{
|
||||
// Extract profile name
|
||||
int start = selected.IndexOf("🎮 ") + 3;
|
||||
int end = selected.IndexOf(" (");
|
||||
if (start > 2 && end > start)
|
||||
{
|
||||
string profileName = selected.Substring(start, end - start);
|
||||
await SelectProfile(profileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddNewProfile()
|
||||
{
|
||||
string? name = await DisplayPromptAsync("New Profile", "Enter profile name:", "Save", "Cancel", "My PS5");
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
|
||||
string? ip = await DisplayPromptAsync("PS5 IP", "Enter PS5 IP address:", "Save", "Cancel", "192.168.0.160");
|
||||
if (string.IsNullOrEmpty(ip)) return;
|
||||
|
||||
string? port = await DisplayPromptAsync("Port", "Enter port (default 9113):", "Save", "Cancel", "9113", keyboard: Keyboard.Numeric);
|
||||
if (string.IsNullOrEmpty(port)) port = "9113";
|
||||
|
||||
_ps5Profiles[name] = $"{ip}:{port}";
|
||||
SaveProfiles();
|
||||
|
||||
await DisplayAlert("Saved", $"Profile '{name}' saved", "OK");
|
||||
await SelectProfile(name);
|
||||
}
|
||||
|
||||
private async Task DeleteProfile()
|
||||
{
|
||||
if (_ps5Profiles.Count == 0) return;
|
||||
|
||||
string? toDelete = await DisplayActionSheet("Delete which profile?", "Cancel", null, _ps5Profiles.Keys.ToArray());
|
||||
if (toDelete != null && toDelete != "Cancel" && _ps5Profiles.ContainsKey(toDelete))
|
||||
{
|
||||
_ps5Profiles.Remove(toDelete);
|
||||
SaveProfiles();
|
||||
if (_currentProfileName == toDelete)
|
||||
{
|
||||
_currentProfileName = "";
|
||||
}
|
||||
await DisplayAlert("Deleted", $"Profile '{toDelete}' deleted", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectProfile(string profileName)
|
||||
{
|
||||
if (!_ps5Profiles.ContainsKey(profileName)) return;
|
||||
|
||||
string ipPort = _ps5Profiles[profileName];
|
||||
var parts = ipPort.Split(':');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
IpEntry.Text = parts[0];
|
||||
PortEntry.Text = parts[1];
|
||||
_currentProfileName = profileName;
|
||||
AddLog($"Selected profile: {profileName} ({ipPort})");
|
||||
await DisplayAlert("Profile Selected", $"Using '{profileName}'\nIP: {parts[0]}\nPort: {parts[1]}", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnConnectClicked(object sender, EventArgs e)
|
||||
{
|
||||
ConnectBtn.IsEnabled = false;
|
||||
StatusLabel.Text = "Connecting...";
|
||||
StatusLabel.TextColor = Colors.Yellow;
|
||||
|
||||
_uploadService.PS5Ip = IpEntry.Text;
|
||||
_uploadService.Port = int.Parse(PortEntry.Text);
|
||||
|
||||
AddLog($"Connecting to {IpEntry.Text}:{PortEntry.Text}...");
|
||||
bool connected = await _uploadService.ConnectAsync();
|
||||
|
||||
if (connected)
|
||||
{
|
||||
StatusLabel.Text = "Connected ✓";
|
||||
StatusLabel.TextColor = Colors.Green;
|
||||
ConnectBtn.IsEnabled = false;
|
||||
DisconnectBtn.IsEnabled = true;
|
||||
AddLog("Connected successfully");
|
||||
|
||||
await RefreshFileList();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusLabel.Text = "Connection failed";
|
||||
StatusLabel.TextColor = Colors.Red;
|
||||
ConnectBtn.IsEnabled = true;
|
||||
AddLog("Connection failed");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDisconnectClicked(object sender, EventArgs e)
|
||||
{
|
||||
await _uploadService.DisconnectAsync();
|
||||
|
||||
StatusLabel.Text = "Disconnected";
|
||||
StatusLabel.TextColor = Colors.Orange;
|
||||
ConnectBtn.IsEnabled = true;
|
||||
DisconnectBtn.IsEnabled = false;
|
||||
|
||||
_files.Clear();
|
||||
AddLog("Disconnected");
|
||||
}
|
||||
|
||||
private async void OnRefreshClicked(object sender, EventArgs e)
|
||||
{
|
||||
await RefreshFileList();
|
||||
}
|
||||
|
||||
private async void OnUpDirectoryClicked(object sender, EventArgs e)
|
||||
{
|
||||
if (_currentPath != "/")
|
||||
{
|
||||
int lastSlash = _currentPath.TrimEnd('/').LastIndexOf('/');
|
||||
_currentPath = _currentPath.Substring(0, lastSlash + 1);
|
||||
PathEntry.Text = _currentPath;
|
||||
await RefreshFileList();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnFileSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.CurrentSelection.Count > 0)
|
||||
{
|
||||
_selectedFile = e.CurrentSelection[0] as FileItem;
|
||||
|
||||
if (_selectedFile?.IsDirectory == true)
|
||||
{
|
||||
_currentPath = _selectedFile.FullPath;
|
||||
PathEntry.Text = _currentPath;
|
||||
await RefreshFileList();
|
||||
FileListView.SelectedItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnUploadClicked(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await FilePicker.PickAsync();
|
||||
if (result != null)
|
||||
{
|
||||
ProgressLabel.Text = $"Uploading {result.FileName}...";
|
||||
ProgressBar.Progress = 0;
|
||||
ProgressPercentLabel.Text = "0%";
|
||||
|
||||
// Copy file to temp location for reliable access on Android
|
||||
string tempPath = Path.Combine(FileSystem.CacheDirectory, result.FileName);
|
||||
using (var sourceStream = await result.OpenReadAsync())
|
||||
using (var destStream = File.Create(tempPath))
|
||||
{
|
||||
await sourceStream.CopyToAsync(destStream);
|
||||
}
|
||||
|
||||
string remotePath = _currentPath.TrimEnd('/') + "/" + result.FileName;
|
||||
AddLog($"Uploading: {result.FileName} -> {remotePath}");
|
||||
|
||||
bool success = await _uploadService.UploadFileAsync(tempPath, remotePath);
|
||||
|
||||
// Clean up temp file
|
||||
try { File.Delete(tempPath); } catch { }
|
||||
|
||||
if (success)
|
||||
{
|
||||
AddTransfer($"⬆️ {result.FileName}");
|
||||
await DisplayAlert("Success", "File uploaded successfully!", "OK");
|
||||
await RefreshFileList();
|
||||
}
|
||||
else
|
||||
{
|
||||
await DisplayAlert("Error", "Upload failed", "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"Upload error: {ex.Message}");
|
||||
await DisplayAlert("Error", $"Upload failed: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDownloadClicked(object sender, EventArgs e)
|
||||
{
|
||||
if (_selectedFile == null || _selectedFile.IsDirectory)
|
||||
{
|
||||
await DisplayAlert("Error", "Please select a file to download", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// First download to cache, then let user save via Share
|
||||
string cachePath = Path.Combine(FileSystem.CacheDirectory, _selectedFile.Name);
|
||||
|
||||
ProgressLabel.Text = $"Downloading {_selectedFile.Name}...";
|
||||
ProgressBar.Progress = 0;
|
||||
ProgressPercentLabel.Text = "0%";
|
||||
|
||||
AddLog($"Downloading: {_selectedFile.FullPath} -> {cachePath}");
|
||||
bool success = await _uploadService.DownloadFileAsync(_selectedFile.FullPath, cachePath);
|
||||
|
||||
if (success)
|
||||
{
|
||||
AddTransfer($"⬇️ {_selectedFile.Name}");
|
||||
|
||||
// Use Share to let user choose where to save
|
||||
// This opens the Android share sheet where user can:
|
||||
// - Save to Files app (choose location)
|
||||
// - Save to Downloads
|
||||
// - Send via other apps
|
||||
string? action = await DisplayActionSheet(
|
||||
$"'{_selectedFile.Name}' downloaded!",
|
||||
"Close", null,
|
||||
"💾 Save to location...",
|
||||
"📂 Open file");
|
||||
|
||||
if (action == "💾 Save to location...")
|
||||
{
|
||||
// Share opens Android's save/share dialog where user can choose location
|
||||
await Share.RequestAsync(new ShareFileRequest
|
||||
{
|
||||
Title = $"Save {_selectedFile.Name}",
|
||||
File = new ShareFile(cachePath)
|
||||
});
|
||||
}
|
||||
else if (action == "📂 Open file")
|
||||
{
|
||||
try
|
||||
{
|
||||
await Launcher.OpenAsync(new OpenFileRequest
|
||||
{
|
||||
File = new ReadOnlyFile(cachePath)
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
await DisplayAlert("Error", "No app found to open this file type", "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await DisplayAlert("Error", "Download failed", "OK");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"Download error: {ex.Message}");
|
||||
await DisplayAlert("Error", $"Download failed: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDeleteClicked(object sender, EventArgs e)
|
||||
{
|
||||
if (_selectedFile == null)
|
||||
{
|
||||
await DisplayAlert("Error", "Please select a file to delete", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
bool confirm = await DisplayAlert("Confirm",
|
||||
$"Delete {_selectedFile.Name}?", "Yes", "No");
|
||||
|
||||
if (confirm)
|
||||
{
|
||||
AddLog($"Deleting: {_selectedFile.FullPath}");
|
||||
bool success = await _uploadService.DeleteFileAsync(_selectedFile.FullPath);
|
||||
if (success)
|
||||
{
|
||||
AddTransfer($"🗑️ {_selectedFile.Name}");
|
||||
await RefreshFileList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshFileList()
|
||||
{
|
||||
try
|
||||
{
|
||||
_files.Clear();
|
||||
AddLog($"Listing: {_currentPath}");
|
||||
|
||||
var fileItems = await _uploadService.ListDirectoryAsync(_currentPath);
|
||||
|
||||
foreach (var fileItem in fileItems)
|
||||
{
|
||||
_files.Add(fileItem);
|
||||
}
|
||||
|
||||
AddLog($"Found {fileItems.Count} items");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"List error: {ex.Message}");
|
||||
await DisplayAlert("Error", $"Failed to list directory: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddLog(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
_debugLog.Add($"[{timestamp}] {message}");
|
||||
if (_debugLog.Count > 100) _debugLog.RemoveAt(0);
|
||||
}
|
||||
|
||||
private void AddTransfer(string message)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
_transferHistory.Add($"[{timestamp}] {message}");
|
||||
if (_transferHistory.Count > 50) _transferHistory.RemoveAt(0);
|
||||
}
|
||||
|
||||
private void OnLogMessage(object? sender, string message)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
AddLog(message);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProgressChanged(object? sender, int progress)
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
ProgressBar.Progress = progress / 100.0;
|
||||
ProgressPercentLabel.Text = $"{progress}%";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace PS5UploadMobile;
|
||||
|
||||
public static class MauiProgram
|
||||
{
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>()
|
||||
.ConfigureFonts(fonts =>
|
||||
{
|
||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
|
||||
});
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net6.0-android</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>PS5UploadMobile</RootNamespace>
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!-- Display name -->
|
||||
<ApplicationTitle>PS5 Upload Suite</ApplicationTitle>
|
||||
|
||||
<!-- App Identifier -->
|
||||
<ApplicationId>com.ps5tools.uploadsuite</ApplicationId>
|
||||
<ApplicationIdGuid>0cb2ca56-8fa9-400b-8669-c219279a7aad</ApplicationIdGuid>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
|
||||
|
||||
<!-- Android specific -->
|
||||
<AndroidManifest>Platforms\Android\AndroidManifest.xml</AndroidManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="$(TargetFramework.StartsWith('net6.0-android'))">
|
||||
<PackageReference Include="System.Net.Sockets" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- App Icon - PS5 Logo from Windows client -->
|
||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" />
|
||||
|
||||
<!-- Images -->
|
||||
<MauiImage Include="Resources\Images\*" />
|
||||
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<MauiFont Include="Resources\Fonts\*" />
|
||||
|
||||
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
|
||||
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,270 @@
|
||||
#nullable enable
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace PS5UploadMobile.Services
|
||||
{
|
||||
public class FtpService
|
||||
{
|
||||
private string _ps5Ip = "192.168.0.160";
|
||||
private int _ftpPort = 2121;
|
||||
private TcpClient? _controlClient;
|
||||
private NetworkStream? _controlStream;
|
||||
private StreamReader? _controlReader;
|
||||
|
||||
public event EventHandler<string>? LogMessage;
|
||||
public event EventHandler<int>? ProgressChanged;
|
||||
|
||||
public string PS5Ip
|
||||
{
|
||||
get => _ps5Ip;
|
||||
set => _ps5Ip = value;
|
||||
}
|
||||
|
||||
public int FtpPort
|
||||
{
|
||||
get => _ftpPort;
|
||||
set => _ftpPort = value;
|
||||
}
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_controlClient = new TcpClient();
|
||||
await _controlClient.ConnectAsync(_ps5Ip, _ftpPort);
|
||||
_controlStream = _controlClient.GetStream();
|
||||
_controlReader = new StreamReader(_controlStream, Encoding.ASCII);
|
||||
|
||||
// Read welcome message
|
||||
string? response = await _controlReader.ReadLineAsync();
|
||||
LogMessage?.Invoke(this, $"Connected: {response}");
|
||||
|
||||
// Login as anonymous
|
||||
await SendCommandAsync("USER anonymous");
|
||||
await SendCommandAsync("PASS anonymous");
|
||||
await SendCommandAsync("TYPE I"); // Binary mode
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Connection failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_controlClient?.Connected == true)
|
||||
{
|
||||
await SendCommandAsync("QUIT");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
_controlReader?.Dispose();
|
||||
_controlStream?.Dispose();
|
||||
_controlClient?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> ListDirectoryAsync(string path)
|
||||
{
|
||||
var files = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
// Change to directory
|
||||
await SendCommandAsync($"CWD {path}");
|
||||
|
||||
// Enter passive mode
|
||||
string pasvResponse = await SendCommandAsync("PASV");
|
||||
var dataEndpoint = ParsePasvResponse(pasvResponse);
|
||||
|
||||
// Connect to data channel
|
||||
using var dataClient = new TcpClient();
|
||||
await dataClient.ConnectAsync(dataEndpoint.Address, dataEndpoint.Port);
|
||||
using var dataStream = dataClient.GetStream();
|
||||
using var dataReader = new StreamReader(dataStream, Encoding.ASCII);
|
||||
|
||||
// Send LIST command
|
||||
await SendCommandAsync("LIST");
|
||||
|
||||
// Read directory listing
|
||||
string? line;
|
||||
while ((line = await dataReader.ReadLineAsync()) != null)
|
||||
{
|
||||
files.Add(line);
|
||||
}
|
||||
|
||||
dataClient.Close();
|
||||
await ReadResponseAsync(); // Read final response
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"List failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public async Task<bool> UploadFileAsync(string localPath, string remotePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(localPath);
|
||||
long totalBytes = fileInfo.Length;
|
||||
long uploadedBytes = 0;
|
||||
|
||||
// Enter passive mode
|
||||
string pasvResponse = await SendCommandAsync("PASV");
|
||||
var dataEndpoint = ParsePasvResponse(pasvResponse);
|
||||
|
||||
// Connect to data channel
|
||||
using var dataClient = new TcpClient();
|
||||
await dataClient.ConnectAsync(dataEndpoint.Address, dataEndpoint.Port);
|
||||
using var dataStream = dataClient.GetStream();
|
||||
|
||||
// Send STOR command
|
||||
await SendCommandAsync($"STOR {remotePath}");
|
||||
|
||||
// Upload file
|
||||
using var fileStream = File.OpenRead(localPath);
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
await dataStream.WriteAsync(buffer, 0, bytesRead);
|
||||
uploadedBytes += bytesRead;
|
||||
|
||||
int progress = (int)((uploadedBytes * 100) / totalBytes);
|
||||
ProgressChanged?.Invoke(this, progress);
|
||||
}
|
||||
|
||||
dataClient.Close();
|
||||
await ReadResponseAsync(); // Read final response
|
||||
|
||||
LogMessage?.Invoke(this, $"Upload complete: {Path.GetFileName(localPath)}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Upload failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadFileAsync(string remotePath, string localPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get file size
|
||||
string sizeResponse = await SendCommandAsync($"SIZE {remotePath}");
|
||||
long totalBytes = ParseSizeResponse(sizeResponse);
|
||||
long downloadedBytes = 0;
|
||||
|
||||
// Enter passive mode
|
||||
string pasvResponse = await SendCommandAsync("PASV");
|
||||
var dataEndpoint = ParsePasvResponse(pasvResponse);
|
||||
|
||||
// Connect to data channel
|
||||
using var dataClient = new TcpClient();
|
||||
await dataClient.ConnectAsync(dataEndpoint.Address, dataEndpoint.Port);
|
||||
using var dataStream = dataClient.GetStream();
|
||||
|
||||
// Send RETR command
|
||||
await SendCommandAsync($"RETR {remotePath}");
|
||||
|
||||
// Download file
|
||||
using var fileStream = File.Create(localPath);
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await dataStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer, 0, bytesRead);
|
||||
downloadedBytes += bytesRead;
|
||||
|
||||
if (totalBytes > 0)
|
||||
{
|
||||
int progress = (int)((downloadedBytes * 100) / totalBytes);
|
||||
ProgressChanged?.Invoke(this, progress);
|
||||
}
|
||||
}
|
||||
|
||||
dataClient.Close();
|
||||
await ReadResponseAsync(); // Read final response
|
||||
|
||||
LogMessage?.Invoke(this, $"Download complete: {Path.GetFileName(remotePath)}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Download failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFileAsync(string remotePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync($"DELE {remotePath}");
|
||||
LogMessage?.Invoke(this, $"Deleted: {remotePath}");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Delete failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> SendCommandAsync(string command)
|
||||
{
|
||||
if (_controlStream == null || _controlReader == null)
|
||||
throw new InvalidOperationException("Not connected");
|
||||
|
||||
byte[] commandBytes = Encoding.ASCII.GetBytes(command + "\r\n");
|
||||
await _controlStream.WriteAsync(commandBytes, 0, commandBytes.Length);
|
||||
|
||||
return await ReadResponseAsync();
|
||||
}
|
||||
|
||||
private async Task<string> ReadResponseAsync()
|
||||
{
|
||||
if (_controlReader == null)
|
||||
throw new InvalidOperationException("Not connected");
|
||||
|
||||
string? response = await _controlReader.ReadLineAsync();
|
||||
LogMessage?.Invoke(this, $"FTP: {response}");
|
||||
return response ?? "";
|
||||
}
|
||||
|
||||
private IPEndPoint ParsePasvResponse(string response)
|
||||
{
|
||||
// Parse PASV response: 227 Entering Passive Mode (192,168,0,160,8,19)
|
||||
int start = response.IndexOf('(');
|
||||
int end = response.IndexOf(')');
|
||||
string[] parts = response.Substring(start + 1, end - start - 1).Split(',');
|
||||
|
||||
string ip = $"{parts[0]}.{parts[1]}.{parts[2]}.{parts[3]}";
|
||||
int port = int.Parse(parts[4]) * 256 + int.Parse(parts[5]);
|
||||
|
||||
return new IPEndPoint(IPAddress.Parse(ip), port);
|
||||
}
|
||||
|
||||
private long ParseSizeResponse(string response)
|
||||
{
|
||||
// Parse SIZE response: 213 12345
|
||||
string[] parts = response.Split(' ');
|
||||
return parts.Length > 1 ? long.Parse(parts[1]) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#nullable enable
|
||||
using PS5Upload;
|
||||
|
||||
namespace PS5UploadMobile.Services
|
||||
{
|
||||
public class PS5UploadService
|
||||
{
|
||||
private string _ps5Ip = "192.168.0.160";
|
||||
private int _port = 9113;
|
||||
private PS5Protocol? _protocol;
|
||||
|
||||
public event EventHandler<string>? LogMessage;
|
||||
public event EventHandler<int>? ProgressChanged;
|
||||
|
||||
public PS5UploadService()
|
||||
{
|
||||
_protocol = new PS5Protocol();
|
||||
}
|
||||
|
||||
public string PS5Ip
|
||||
{
|
||||
get => _ps5Ip;
|
||||
set => _ps5Ip = value;
|
||||
}
|
||||
|
||||
public int Port
|
||||
{
|
||||
get => _port;
|
||||
set => _port = value;
|
||||
}
|
||||
|
||||
public bool IsConnected => _protocol?.IsConnected ?? false;
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_protocol = new PS5Protocol();
|
||||
bool connected = await _protocol.ConnectAsync(_ps5Ip, _port);
|
||||
|
||||
if (connected)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Connected to PS5 at {_ps5Ip}:{_port}");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage?.Invoke(this, "Failed to connect to PS5");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Connection error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task DisconnectAsync()
|
||||
{
|
||||
if (_protocol != null)
|
||||
{
|
||||
_protocol.Disconnect();
|
||||
LogMessage?.Invoke(this, "Disconnected from PS5");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<List<FileItem>> ListDirectoryAsync(string path)
|
||||
{
|
||||
var files = new List<FileItem>();
|
||||
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
LogMessage?.Invoke(this, "Not connected to PS5");
|
||||
return files;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var entries = await _protocol.ListDirAsync(path);
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
string fullPath = path.TrimEnd('/') + "/" + entry.Name;
|
||||
if (entry.IsDirectory) fullPath += "/";
|
||||
|
||||
files.Add(new FileItem
|
||||
{
|
||||
Name = entry.Name,
|
||||
Size = entry.IsDirectory ? "" : FormatFileSize(entry.Size),
|
||||
Type = entry.IsDirectory ? "DIR" : "FILE",
|
||||
Icon = entry.IsDirectory ? "📁" : "📄",
|
||||
IsDirectory = entry.IsDirectory,
|
||||
FullPath = fullPath
|
||||
});
|
||||
}
|
||||
|
||||
LogMessage?.Invoke(this, $"Listed {files.Count} items in {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"List error: {ex.Message}");
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public async Task<bool> UploadFileAsync(string localPath, string remotePath)
|
||||
{
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
LogMessage?.Invoke(this, "Not connected to PS5");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(localPath);
|
||||
LogMessage?.Invoke(this, $"Uploading {fileInfo.Name} ({FormatFileSize(fileInfo.Length)})...");
|
||||
|
||||
var progress = new Progress<UploadProgress>(p =>
|
||||
{
|
||||
int percent = p.TotalBytes > 0 ? (int)((p.BytesSent * 100) / p.TotalBytes) : 0;
|
||||
ProgressChanged?.Invoke(this, percent);
|
||||
});
|
||||
|
||||
bool success = await _protocol.UploadFileAsync(localPath, remotePath, progress);
|
||||
|
||||
if (success)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Upload complete: {fileInfo.Name}");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage?.Invoke(this, "Upload failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Upload error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadFileAsync(string remotePath, string localPath)
|
||||
{
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
LogMessage?.Invoke(this, "Not connected to PS5");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Downloading {Path.GetFileName(remotePath)}...");
|
||||
|
||||
var progress = new Progress<UploadProgress>(p =>
|
||||
{
|
||||
int percent = p.TotalBytes > 0 ? (int)((p.BytesSent * 100) / p.TotalBytes) : 0;
|
||||
ProgressChanged?.Invoke(this, percent);
|
||||
});
|
||||
|
||||
bool success = await _protocol.DownloadFileAsync(remotePath, localPath, progress);
|
||||
|
||||
if (success)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Download complete: {Path.GetFileName(remotePath)}");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage?.Invoke(this, "Download failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Download error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFileAsync(string remotePath)
|
||||
{
|
||||
if (_protocol == null || !_protocol.IsConnected)
|
||||
{
|
||||
LogMessage?.Invoke(this, "Not connected to PS5");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
bool success = await _protocol.DeleteFileAsync(remotePath);
|
||||
|
||||
if (success)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Deleted: {remotePath}");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMessage?.Invoke(this, "Delete failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMessage?.Invoke(this, $"Delete error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatFileSize(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB" };
|
||||
double len = bytes;
|
||||
int order = 0;
|
||||
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len /= 1024;
|
||||
}
|
||||
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
}
|
||||
|
||||
public class FileItem
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Size { get; set; } = "";
|
||||
public string Type { get; set; } = "";
|
||||
public string Icon { get; set; } = "";
|
||||
public bool IsDirectory { get; set; }
|
||||
public string FullPath { get; set; } = "";
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "================================================"
|
||||
echo "PS5 Upload Server - Compilation"
|
||||
echo "By Manos"
|
||||
echo "================================================"
|
||||
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
|
||||
|
||||
if [ -f "ps5_upload_server.elf" ]; then
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "[+] SUCCESS! Compiled ps5_upload_server.elf"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
ls -lh ps5_upload_server.elf
|
||||
file ps5_upload_server.elf
|
||||
echo ""
|
||||
echo "Upload to PS5 and run with elfldr"
|
||||
echo "Server will listen on port 9113"
|
||||
else
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "[-] COMPILATION FAILED"
|
||||
echo "================================================"
|
||||
exit 1
|
||||
fi
|
||||
-875
@@ -1,875 +0,0 @@
|
||||
/* PS5 Upload Server - Custom High-Speed Protocol
|
||||
* By Manos
|
||||
* Port: 9113
|
||||
* Protocol: Custom binary for maximum speed
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#define SERVER_PORT 9113
|
||||
#define BUFFER_SIZE (8 * 1024 * 1024)
|
||||
#define MAX_PATH 2048
|
||||
#define DISK_WORKER_COUNT 4
|
||||
#define QUEUE_MAX_SIZE 32
|
||||
|
||||
// Protocol commands
|
||||
#define CMD_PING 0x01
|
||||
#define CMD_LIST_STORAGE 0x02
|
||||
#define CMD_LIST_DIR 0x03
|
||||
#define CMD_CREATE_DIR 0x04
|
||||
#define CMD_DELETE_FILE 0x05
|
||||
#define CMD_DELETE_DIR 0x06
|
||||
#define CMD_RENAME 0x07
|
||||
#define CMD_COPY_FILE 0x08
|
||||
#define CMD_MOVE_FILE 0x09
|
||||
#define CMD_START_UPLOAD 0x10
|
||||
#define CMD_UPLOAD_CHUNK 0x11
|
||||
#define CMD_END_UPLOAD 0x12
|
||||
#define CMD_SHUTDOWN 0xFF
|
||||
|
||||
// Protocol responses
|
||||
#define RESP_OK 0x01
|
||||
#define RESP_ERROR 0x02
|
||||
#define RESP_DATA 0x03
|
||||
#define RESP_READY 0x04
|
||||
#define RESP_PROGRESS 0x05
|
||||
|
||||
typedef struct notify_request {
|
||||
char useless1[45];
|
||||
char message[3075];
|
||||
} notify_request_t;
|
||||
|
||||
int sceKernelSendNotificationRequest(int, notify_request_t*, size_t, int);
|
||||
|
||||
void send_notification(const char *msg) {
|
||||
notify_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
strncpy(req.message, msg, sizeof(req.message) - 1);
|
||||
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int sock;
|
||||
FILE *upload_fp;
|
||||
char upload_path[MAX_PATH];
|
||||
uint64_t upload_size;
|
||||
uint64_t upload_received;
|
||||
} client_session_t;
|
||||
|
||||
// Disk write job for queue
|
||||
typedef struct write_job {
|
||||
uint8_t *data;
|
||||
size_t len;
|
||||
FILE *fp;
|
||||
struct write_job *next;
|
||||
} write_job_t;
|
||||
|
||||
// Job queue (producer-consumer pattern)
|
||||
typedef struct {
|
||||
write_job_t *head;
|
||||
write_job_t *tail;
|
||||
size_t count;
|
||||
size_t max;
|
||||
int closed;
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t not_empty;
|
||||
pthread_cond_t not_full;
|
||||
} job_queue_t;
|
||||
|
||||
// Global queue and workers
|
||||
static job_queue_t g_queue;
|
||||
static pthread_t g_workers[DISK_WORKER_COUNT];
|
||||
static int g_workers_initialized = 0;
|
||||
|
||||
// Queue operations
|
||||
void queue_init(job_queue_t *q, size_t max) {
|
||||
memset(q, 0, sizeof(*q));
|
||||
q->max = max;
|
||||
pthread_mutex_init(&q->mutex, NULL);
|
||||
pthread_cond_init(&q->not_empty, NULL);
|
||||
pthread_cond_init(&q->not_full, NULL);
|
||||
}
|
||||
|
||||
int queue_push(job_queue_t *q, write_job_t *job) {
|
||||
pthread_mutex_lock(&q->mutex);
|
||||
while (!q->closed && q->count >= q->max) {
|
||||
pthread_cond_wait(&q->not_full, &q->mutex);
|
||||
}
|
||||
if (q->closed) {
|
||||
pthread_mutex_unlock(&q->mutex);
|
||||
return -1;
|
||||
}
|
||||
job->next = NULL;
|
||||
if (!q->tail) {
|
||||
q->head = job;
|
||||
q->tail = job;
|
||||
} else {
|
||||
q->tail->next = job;
|
||||
q->tail = job;
|
||||
}
|
||||
q->count++;
|
||||
pthread_cond_signal(&q->not_empty);
|
||||
pthread_mutex_unlock(&q->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
write_job_t *queue_pop(job_queue_t *q) {
|
||||
pthread_mutex_lock(&q->mutex);
|
||||
while (!q->closed && q->count == 0) {
|
||||
pthread_cond_wait(&q->not_empty, &q->mutex);
|
||||
}
|
||||
if (q->count == 0 && q->closed) {
|
||||
pthread_mutex_unlock(&q->mutex);
|
||||
return NULL;
|
||||
}
|
||||
write_job_t *job = q->head;
|
||||
q->head = job->next;
|
||||
if (!q->head) {
|
||||
q->tail = NULL;
|
||||
}
|
||||
q->count--;
|
||||
pthread_cond_signal(&q->not_full);
|
||||
pthread_mutex_unlock(&q->mutex);
|
||||
return job;
|
||||
}
|
||||
|
||||
// Disk worker thread
|
||||
void *disk_worker(void *arg) {
|
||||
(void)arg;
|
||||
while (1) {
|
||||
write_job_t *job = queue_pop(&g_queue);
|
||||
if (!job) break;
|
||||
|
||||
if (job->fp && job->data && job->len > 0) {
|
||||
fwrite(job->data, 1, job->len, job->fp);
|
||||
// No fflush here - let setvbuf handle buffering
|
||||
}
|
||||
|
||||
free(job->data);
|
||||
free(job);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void init_workers() {
|
||||
if (g_workers_initialized) return;
|
||||
|
||||
queue_init(&g_queue, QUEUE_MAX_SIZE);
|
||||
for (int i = 0; i < DISK_WORKER_COUNT; i++) {
|
||||
pthread_create(&g_workers[i], NULL, disk_worker, NULL);
|
||||
}
|
||||
g_workers_initialized = 1;
|
||||
}
|
||||
|
||||
// Send response - combined header+data in single send for speed
|
||||
void send_response(int sock, uint8_t response, const void *data, uint32_t data_len) {
|
||||
// Combine header and data into single buffer for single send()
|
||||
size_t total_len = 5 + data_len;
|
||||
uint8_t *combined = malloc(total_len);
|
||||
if (!combined) {
|
||||
// Fallback to separate sends
|
||||
uint8_t header[5];
|
||||
header[0] = response;
|
||||
memcpy(header + 1, &data_len, 4);
|
||||
send(sock, header, 5, 0);
|
||||
if (data && data_len > 0) {
|
||||
send(sock, data, data_len, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
combined[0] = response;
|
||||
memcpy(combined + 1, &data_len, 4);
|
||||
if (data && data_len > 0) {
|
||||
memcpy(combined + 5, data, data_len);
|
||||
}
|
||||
|
||||
send(sock, combined, total_len, 0);
|
||||
free(combined);
|
||||
}
|
||||
|
||||
// Send OK response
|
||||
void send_ok(int sock, const char *msg) {
|
||||
uint32_t len = msg ? strlen(msg) : 0;
|
||||
send_response(sock, RESP_OK, msg, len);
|
||||
}
|
||||
|
||||
// Send error response
|
||||
void send_error(int sock, const char *msg) {
|
||||
uint32_t len = msg ? strlen(msg) : 0;
|
||||
send_response(sock, RESP_ERROR, msg, len);
|
||||
}
|
||||
|
||||
// Recursive directory creation
|
||||
int mkdir_recursive(const char *path) {
|
||||
char tmp[MAX_PATH];
|
||||
char *p = NULL;
|
||||
size_t len;
|
||||
|
||||
snprintf(tmp, sizeof(tmp), "%s", path);
|
||||
len = strlen(tmp);
|
||||
if (tmp[len - 1] == '/') {
|
||||
tmp[len - 1] = 0;
|
||||
}
|
||||
|
||||
for (p = tmp + 1; *p; p++) {
|
||||
if (*p == '/') {
|
||||
*p = 0;
|
||||
if (mkdir(tmp, 0777) != 0 && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
chmod(tmp, 0777);
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
if (mkdir(tmp, 0777) != 0 && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
chmod(tmp, 0777);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Recursive directory deletion
|
||||
int rmdir_recursive(const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
if (!dir) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
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)) {
|
||||
rmdir_recursive(child);
|
||||
} else {
|
||||
unlink(child);
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
// Handle PING
|
||||
void handle_ping(client_session_t *session) {
|
||||
send_ok(session->sock, "PONG");
|
||||
}
|
||||
|
||||
// Handle LIST_STORAGE
|
||||
void handle_list_storage(client_session_t *session) {
|
||||
const char *mounts[] = {"/data", "/mnt/usb0", "/mnt/usb1", "/system", "/system_ex"};
|
||||
int mount_count = 0;
|
||||
|
||||
// Count available mounts
|
||||
for (int i = 0; i < 5; i++) {
|
||||
struct stat st;
|
||||
if (stat(mounts[i], &st) == 0) {
|
||||
mount_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
size_t buf_size = 4 + (mount_count * (2 + MAX_PATH + 16));
|
||||
uint8_t *buffer = malloc(buf_size);
|
||||
if (!buffer) {
|
||||
send_error(session->sock, "Memory allocation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *ptr = buffer;
|
||||
memcpy(ptr, &mount_count, 4);
|
||||
ptr += 4;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
struct statvfs vfs;
|
||||
if (statvfs(mounts[i], &vfs) == 0) {
|
||||
uint16_t path_len = strlen(mounts[i]);
|
||||
uint64_t total = (uint64_t)vfs.f_blocks * vfs.f_frsize;
|
||||
uint64_t free = (uint64_t)vfs.f_bfree * vfs.f_frsize;
|
||||
|
||||
memcpy(ptr, &path_len, 2);
|
||||
ptr += 2;
|
||||
memcpy(ptr, mounts[i], path_len);
|
||||
ptr += path_len;
|
||||
memcpy(ptr, &total, 8);
|
||||
ptr += 8;
|
||||
memcpy(ptr, &free, 8);
|
||||
ptr += 8;
|
||||
}
|
||||
}
|
||||
|
||||
send_response(session->sock, RESP_DATA, buffer, ptr - buffer);
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
// Handle LIST_DIR - Optimized version using d_type only (no stat for dirs)
|
||||
void handle_list_dir(client_session_t *session, const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
if (!dir) {
|
||||
int32_t count = 0;
|
||||
send_response(session->sock, RESP_DATA, &count, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t buf_size = 256 * 1024;
|
||||
uint8_t *buffer = malloc(buf_size);
|
||||
if (!buffer) {
|
||||
closedir(dir);
|
||||
int32_t count = 0;
|
||||
send_response(session->sock, RESP_DATA, &count, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *ptr = buffer + 4;
|
||||
int32_t entry_count = 0;
|
||||
|
||||
struct dirent *entry;
|
||||
char full_path[MAX_PATH];
|
||||
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint16_t name_len = (uint16_t)strlen(entry->d_name);
|
||||
size_t needed = 1 + 2 + name_len + 8 + 8;
|
||||
|
||||
if ((size_t)(ptr - buffer) + needed > buf_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Determine type - use d_type if available, otherwise fall back to stat()
|
||||
uint8_t type = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
ptr += 2;
|
||||
memcpy(ptr, entry->d_name, name_len);
|
||||
ptr += name_len;
|
||||
memcpy(ptr, &size, 8);
|
||||
ptr += 8;
|
||||
memcpy(ptr, ×tamp, 8);
|
||||
ptr += 8;
|
||||
|
||||
entry_count++;
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
memcpy(buffer, &entry_count, 4);
|
||||
send_response(session->sock, RESP_DATA, buffer, (uint32_t)(ptr - buffer));
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
// Handle CREATE_DIR
|
||||
void handle_create_dir(client_session_t *session, const char *path) {
|
||||
if (mkdir_recursive(path) == 0) {
|
||||
send_ok(session->sock, "Directory created");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to create directory");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DELETE_FILE
|
||||
void handle_delete_file(client_session_t *session, const char *path) {
|
||||
if (unlink(path) == 0) {
|
||||
send_ok(session->sock, "File deleted");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to delete file");
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to delete directory");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle RENAME
|
||||
void handle_rename(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
const char *old_path = (const char *)data;
|
||||
uint32_t old_len = strlen(old_path);
|
||||
if (old_len + 2 > data_len) {
|
||||
send_error(session->sock, "Invalid rename request");
|
||||
return;
|
||||
}
|
||||
const char *new_path = (const char *)(data + old_len + 1);
|
||||
|
||||
if (rename(old_path, new_path) == 0) {
|
||||
send_ok(session->sock, "Renamed successfully");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to rename");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle COPY_FILE
|
||||
void handle_copy_file(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
const char *src = (const char *)data;
|
||||
uint32_t src_len = strlen(src);
|
||||
if (src_len + 2 > data_len) {
|
||||
send_error(session->sock, "Invalid copy request");
|
||||
return;
|
||||
}
|
||||
const char *dst = (const char *)(data + src_len + 1);
|
||||
|
||||
int src_fd = open(src, O_RDONLY);
|
||||
if (src_fd < 0) {
|
||||
send_error(session->sock, "Cannot open source file");
|
||||
return;
|
||||
}
|
||||
|
||||
int dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0777);
|
||||
if (dst_fd < 0) {
|
||||
close(src_fd);
|
||||
send_error(session->sock, "Cannot create destination file");
|
||||
return;
|
||||
}
|
||||
|
||||
char *buf = malloc(BUFFER_SIZE);
|
||||
if (!buf) {
|
||||
close(src_fd);
|
||||
close(dst_fd);
|
||||
send_error(session->sock, "Memory allocation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
ssize_t n;
|
||||
int success = 1;
|
||||
while ((n = read(src_fd, buf, BUFFER_SIZE)) > 0) {
|
||||
if (write(dst_fd, buf, n) != n) {
|
||||
success = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
close(src_fd);
|
||||
close(dst_fd);
|
||||
chmod(dst, 0777);
|
||||
|
||||
if (success) {
|
||||
send_ok(session->sock, "File copied");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to copy file");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MOVE_FILE
|
||||
void handle_move_file(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
const char *src = (const char *)data;
|
||||
uint32_t src_len = strlen(src);
|
||||
if (src_len + 2 > data_len) {
|
||||
send_error(session->sock, "Invalid move request");
|
||||
return;
|
||||
}
|
||||
const char *dst = (const char *)(data + src_len + 1);
|
||||
|
||||
if (rename(src, dst) == 0) {
|
||||
send_ok(session->sock, "File moved");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to move file");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle START_UPLOAD (with optional chunk offset for parallel upload)
|
||||
void handle_start_upload(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
}
|
||||
|
||||
// Parse path, size, and optional offset
|
||||
const char *path = (const char *)data;
|
||||
uint32_t path_len = strlen(path);
|
||||
if (path_len + 9 > data_len) {
|
||||
send_error(session->sock, "Invalid upload request");
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t file_size;
|
||||
memcpy(&file_size, data + path_len + 1, 8);
|
||||
|
||||
// Check for optional offset (for chunked parallel upload)
|
||||
uint64_t chunk_offset = 0;
|
||||
if (path_len + 17 <= data_len) {
|
||||
memcpy(&chunk_offset, data + path_len + 9, 8);
|
||||
}
|
||||
|
||||
// Create parent directories
|
||||
char parent[MAX_PATH];
|
||||
strncpy(parent, path, sizeof(parent) - 1);
|
||||
parent[sizeof(parent) - 1] = '\0';
|
||||
char *last_slash = strrchr(parent, '/');
|
||||
if (last_slash) {
|
||||
*last_slash = '\0';
|
||||
mkdir_recursive(parent);
|
||||
}
|
||||
|
||||
// Open file for writing (r+b for chunk mode, wb for normal)
|
||||
if (chunk_offset > 0) {
|
||||
// Chunk mode: open existing file or create if needed
|
||||
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
|
||||
session->upload_fp = fopen(path, "wb");
|
||||
}
|
||||
|
||||
if (!session->upload_fp) {
|
||||
send_error(session->sock, "Cannot create file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use HUGE buffer (8MB) for maximum write speed
|
||||
setvbuf(session->upload_fp, NULL, _IOFBF, BUFFER_SIZE);
|
||||
|
||||
strncpy(session->upload_path, path, sizeof(session->upload_path) - 1);
|
||||
session->upload_size = file_size;
|
||||
session->upload_received = chunk_offset;
|
||||
|
||||
// Increase socket receive buffer for this upload session
|
||||
int huge_buf = 8 * 1024 * 1024; // 8MB receive buffer
|
||||
setsockopt(session->sock, SOL_SOCKET, SO_RCVBUF, &huge_buf, sizeof(huge_buf));
|
||||
|
||||
send_response(session->sock, RESP_READY, NULL, 0);
|
||||
}
|
||||
|
||||
// Handle UPLOAD_CHUNK
|
||||
void handle_upload_chunk(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
if (!session->upload_fp) {
|
||||
send_error(session->sock, "No upload in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Direct write with periodic flush for stability
|
||||
size_t written = fwrite(data, 1, data_len, session->upload_fp);
|
||||
if (written != data_len) {
|
||||
send_error(session->sock, "Write failed");
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
session->upload_received += written;
|
||||
|
||||
// Flush every 32MB to prevent buffer overflow with multiple connections
|
||||
if (session->upload_received % (32 * 1024 * 1024) < data_len) {
|
||||
fflush(session->upload_fp);
|
||||
}
|
||||
// No response - zero blocking for maximum speed
|
||||
}
|
||||
|
||||
// Handle END_UPLOAD
|
||||
void handle_end_upload(client_session_t *session) {
|
||||
if (!session->upload_fp) {
|
||||
send_error(session->sock, "No upload in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
fflush(session->upload_fp);
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
chmod(session->upload_path, 0777);
|
||||
|
||||
send_ok(session->sock, "Upload complete");
|
||||
}
|
||||
|
||||
// Handle client
|
||||
void *client_thread(void *arg) {
|
||||
client_session_t *session = (client_session_t *)arg;
|
||||
uint8_t *buffer = malloc(BUFFER_SIZE);
|
||||
|
||||
if (!buffer) {
|
||||
close(session->sock);
|
||||
free(session);
|
||||
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));
|
||||
|
||||
while (1) {
|
||||
// Read command header (5 bytes: 1 cmd + 4 data_len)
|
||||
uint8_t header[5];
|
||||
ssize_t n = recv(session->sock, header, 5, MSG_WAITALL);
|
||||
if (n != 5) {
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t cmd = header[0];
|
||||
uint32_t data_len;
|
||||
memcpy(&data_len, header + 1, 4);
|
||||
|
||||
// Read data if present
|
||||
uint8_t *data = NULL;
|
||||
if (data_len > 0) {
|
||||
if (data_len > BUFFER_SIZE) {
|
||||
send_error(session->sock, "Data too large");
|
||||
break;
|
||||
}
|
||||
data = buffer;
|
||||
ssize_t received = 0;
|
||||
while (received < data_len) {
|
||||
n = recv(session->sock, data + received, data_len - received, 0);
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
received += n;
|
||||
}
|
||||
if (received != data_len) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle command
|
||||
switch (cmd) {
|
||||
case CMD_PING:
|
||||
handle_ping(session);
|
||||
break;
|
||||
case CMD_LIST_STORAGE:
|
||||
handle_list_storage(session);
|
||||
break;
|
||||
case CMD_LIST_DIR:
|
||||
if (data) {
|
||||
handle_list_dir(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_CREATE_DIR:
|
||||
if (data) {
|
||||
handle_create_dir(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_DELETE_FILE:
|
||||
if (data) {
|
||||
handle_delete_file(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_DELETE_DIR:
|
||||
if (data) {
|
||||
handle_delete_dir(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_RENAME:
|
||||
if (data) {
|
||||
handle_rename(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_COPY_FILE:
|
||||
if (data) {
|
||||
handle_copy_file(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_MOVE_FILE:
|
||||
if (data) {
|
||||
handle_move_file(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_START_UPLOAD:
|
||||
if (data) {
|
||||
handle_start_upload(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_UPLOAD_CHUNK:
|
||||
if (data) {
|
||||
handle_upload_chunk(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_END_UPLOAD:
|
||||
handle_end_upload(session);
|
||||
break;
|
||||
case CMD_SHUTDOWN:
|
||||
send_ok(session->sock, "Shutting down");
|
||||
free(buffer);
|
||||
close(session->sock);
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
}
|
||||
free(session);
|
||||
exit(0);
|
||||
default:
|
||||
send_error(session->sock, "Unknown command");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
close(session->sock);
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
}
|
||||
free(session);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Initialize worker threads for async disk I/O
|
||||
init_workers();
|
||||
|
||||
int server_sock;
|
||||
struct sockaddr_in server_addr;
|
||||
|
||||
server_sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (server_sock < 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int opt = 1;
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
// Prevent SIGPIPE
|
||||
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;
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));
|
||||
|
||||
memset(&server_addr, 0, sizeof(server_addr));
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
server_addr.sin_port = htons(SERVER_PORT);
|
||||
|
||||
if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
|
||||
close(server_sock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (listen(server_sock, 10) < 0) {
|
||||
close(server_sock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get IP address
|
||||
char ip_str[INET_ADDRSTRLEN] = "0.0.0.0";
|
||||
struct ifaddrs *ifaddr, *ifa;
|
||||
if (getifaddrs(&ifaddr) == 0) {
|
||||
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
|
||||
if (ifa->ifa_addr == NULL) continue;
|
||||
if (ifa->ifa_addr->sa_family == AF_INET) {
|
||||
struct sockaddr_in *addr = (struct sockaddr_in *)ifa->ifa_addr;
|
||||
inet_ntop(AF_INET, &addr->sin_addr, ip_str, INET_ADDRSTRLEN);
|
||||
if (strcmp(ip_str, "127.0.0.1") != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
freeifaddrs(ifaddr);
|
||||
}
|
||||
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "PS5 Upload Server v3.0: %s:%d - By Manos", ip_str, SERVER_PORT);
|
||||
send_notification(msg);
|
||||
|
||||
while (1) {
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
|
||||
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);
|
||||
if (client_sock < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_RCVBUF, &large_buf, sizeof(large_buf));
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_SNDBUF, &large_buf, sizeof(large_buf));
|
||||
|
||||
// TCP optimizations - TCP_NODELAY for immediate send
|
||||
int nodelay = 1;
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
|
||||
|
||||
// NOTE: Removed TCP_NOPUSH - it was causing buffering delays!
|
||||
|
||||
// Set keepalive to prevent connection drops
|
||||
int keepalive = 1;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
|
||||
|
||||
client_session_t *session = malloc(sizeof(client_session_t));
|
||||
if (!session) {
|
||||
close(client_sock);
|
||||
continue;
|
||||
}
|
||||
|
||||
memset(session, 0, sizeof(client_session_t));
|
||||
session->sock = client_sock;
|
||||
|
||||
pthread_t thread;
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
|
||||
if (pthread_create(&thread, &attr, client_thread, session) != 0) {
|
||||
close(client_sock);
|
||||
free(session);
|
||||
}
|
||||
|
||||
pthread_attr_destroy(&attr);
|
||||
}
|
||||
|
||||
close(server_sock);
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,343 @@
|
||||
# 🚀 PS5 Upload Suite - High-Speed File Transfer
|
||||
|
||||
**By Manos**
|
||||
|
||||
**Version 4.1.0 - Memory Optimization & Large File Handling**
|
||||
|
||||
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **104+ MB/s** upload speeds for large files using parallel chunked uploads!
|
||||
|
||||
⭐ **NEW in v4.1.0:**
|
||||
- 🧠 **Memory Optimization** - Handles 156K+ files without crashes
|
||||
- ⚡ **Reduced UI Stuttering** - Batch logging for smoother experience
|
||||
- 🔧 **Lock-Free Operations** - ConcurrentDictionary for thread-safety
|
||||
- 🧹 **Auto Memory Cleanup** - Completed files removed from tracking
|
||||
- � **Incremental Processing** - No massive dictionary allocations
|
||||
|
||||
📱 **Android Mobile Client available!**
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📦 Downloads
|
||||
|
||||
Download the latest release from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page:
|
||||
|
||||
### 1. PS5 Server Payload
|
||||
- **File:** `ps5_upload_server.elf`
|
||||
- **Port:** 9113
|
||||
- High-speed custom binary protocol
|
||||
- 16MB socket buffers for maximum throughput
|
||||
- Multi-threaded client handling
|
||||
|
||||
### 2. Windows GUI Client
|
||||
- **File:** `PS5Upload.exe`
|
||||
- Self-contained executable (no .NET installation required)
|
||||
- Modern dark theme UI
|
||||
- Drag & drop file/folder upload
|
||||
- Real-time upload progress with speed tracking
|
||||
- Transfer History with success/failed tracking
|
||||
- Privacy-focused (no storage monitoring)
|
||||
|
||||
### 3. 📱 Android Mobile Client
|
||||
- **File:** `PS5UploadMobile.apk`
|
||||
- Upload/download files from your phone
|
||||
- Multi-PS5 Profiles support
|
||||
- Browse PS5 filesystem
|
||||
- Favorites for quick navigation
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Step 1: Load PS5 Payload
|
||||
|
||||
1. Download `ps5_upload_server.elf` from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page
|
||||
|
||||
2. Copy it to your PS5: `/data/etaHEN/payloads/ps5_upload_server.elf`
|
||||
|
||||
3. Load the payload with elfldr
|
||||
|
||||
4. You should see notification: `PS5 Upload Server: 192.168.0.XXX:9113 - By Manos`
|
||||
|
||||
### Step 2: Run Windows Client
|
||||
|
||||
1. Run `PS5Upload.exe` on your PC
|
||||
|
||||
2. Enter your PS5's IP address (e.g., `192.168.0.160`)
|
||||
|
||||
3. Click **Connect**
|
||||
|
||||
4. Browse files/folders or drag & drop
|
||||
|
||||
5. Click **Upload to PS5**
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Features
|
||||
|
||||
### PS5 Server
|
||||
✅ **High-Speed Protocol** - Custom binary protocol
|
||||
✅ **4MB Buffers** - Maximum throughput
|
||||
✅ **Direct Disk I/O** - No temp files
|
||||
✅ **Multi-threaded** - Handle multiple clients
|
||||
✅ **Robust Error Handling** - Graceful failures
|
||||
|
||||
### Windows Client
|
||||
✅ **Modern UI** - Dark theme, maximized window for better visibility
|
||||
✅ **Drag & Drop** - Files and folders
|
||||
✅ **Browse PS5** - Navigate filesystem
|
||||
✅ **Real-time Progress** - Speed & percentage tracking
|
||||
✅ **Optimized Upload** - 8 parallel connections for 88-110 MB/s aggregate speed
|
||||
✅ **Transfer History** - Track all uploads/downloads with success/failed status
|
||||
✅ **Auto-Clear History** - Optional auto-clear on startup
|
||||
✅ **Folder Upload** - Recursive directory upload
|
||||
✅ **Download Files** - Download from PS5 to PC
|
||||
✅ **🔍 Smart Search** - Full filesystem indexing with instant search (NEW v3.3)
|
||||
- Wildcard support: `*.pkg`, `*loader*`, `game*.bin`
|
||||
- Size filters: `size:>1GB`, `size:<100MB`
|
||||
- Case-insensitive matching
|
||||
- Search both filename and full path
|
||||
- Double-click to navigate to folder
|
||||
✅ **💻 Shell Terminal** - Execute commands directly on PS5 (NEW v3.3)
|
||||
- Run system commands remotely
|
||||
- Real-time output display
|
||||
- Command history
|
||||
- Working directory support
|
||||
✅ **Favorites/Bookmarks** - Quick navigation to saved paths
|
||||
✅ **Multi-PS5 Support** - Save and switch between multiple PS5 profiles
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
### v3.1.0 Optimized Results:
|
||||
- ✅ **104+ MB/s** upload speed for large files (parallel chunked uploads)
|
||||
- ✅ **4 parallel chunk connections** for maximum throughput
|
||||
- ✅ **500MB chunks** - optimal size for PS5 disk performance
|
||||
- ✅ **Real-time progress** - Speed/ETA updates during upload
|
||||
- ✅ **Zero chunk failures** - Fixed race conditions
|
||||
- ✅ **Fully responsive UI** throughout upload
|
||||
- ✅ **No memory leaks** - stable operation
|
||||
|
||||
### Previous Results (v1.3.0):
|
||||
- ✅ **42,801 files** uploaded successfully
|
||||
- ✅ **60-150 MB/s** sustained throughput
|
||||
|
||||
| Network | Expected Upload Speed | Expected Download Speed |
|
||||
|---------|----------------------|------------------------|
|
||||
| **Gigabit Ethernet** | 88-110 MB/s (aggregate) | 100-120 MB/s |
|
||||
| **WiFi 6 (5GHz)** | 32-70 MB/s (aggregate) | 60-80 MB/s |
|
||||
| **WiFi 5 (5GHz)** | 20-50 MB/s (aggregate) | 40-60 MB/s |
|
||||
|
||||
**Note:** Upload speeds depend on network quality and PS5 disk write performance. WiFi has ~40-60% overhead compared to Ethernet.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Connection Failed
|
||||
- Make sure PS5 payload is running
|
||||
- Check PS5 IP address is correct
|
||||
- Verify both devices are on same network
|
||||
- Check firewall settings
|
||||
|
||||
### Slow Upload Speed
|
||||
- Use wired Ethernet connection (not WiFi)
|
||||
- Close other network applications
|
||||
- Check PS5 disk health
|
||||
|
||||
### Upload Fails
|
||||
- Check PS5 has enough free space
|
||||
- Verify destination path exists
|
||||
- Check file permissions
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Notes
|
||||
|
||||
- Server only accepts connections from local network
|
||||
- No authentication required (local network only)
|
||||
- SHUTDOWN command only works from localhost
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.2.0
|
||||
|
||||
### 🎯 Improvements:
|
||||
|
||||
#### 1. 💾 Accurate Storage Display
|
||||
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
|
||||
- **Accounts for reserved space** - Same calculation as PS5 system
|
||||
|
||||
#### 2. 📱 Mobile App Improvements
|
||||
- **Multi-select mode** - Toggle button to select multiple files/folders
|
||||
- **Folder browser** - Visual folder picker for Copy/Move destinations
|
||||
- **Sorted file list** - Folders always displayed at top
|
||||
- **Exit confirmation** - Prompt when pressing back button
|
||||
- **Delete folders** - Now supports folder deletion (not just files)
|
||||
- **Batch operations** - Delete/download multiple items at once
|
||||
|
||||
#### 3. 🐛 Bug Fixes
|
||||
- **Fixed empty folder deletion** - No more "Unexpected response" error
|
||||
- **Fixed Copy/Move crash** - No longer crashes when selecting destination
|
||||
- **Fixed multi-select count** - Correct item count in delete confirmation
|
||||
- **Fixed folder navigation** - Can enter folders after actions complete
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.1.0
|
||||
|
||||
### 🚀 Performance Improvements:
|
||||
|
||||
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
|
||||
- **Large files (>100MB)** are now split into 500MB chunks
|
||||
- **4 parallel connections** upload chunks simultaneously
|
||||
- **Result:** ~104 MB/s upload speed (2x faster than before!)
|
||||
- **Fixed race condition:** First chunk creates file before others start
|
||||
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
|
||||
|
||||
#### 2. 📊 Real-time Progress Display
|
||||
- **Speed display** now updates in real-time during chunked uploads
|
||||
- **ETA display** shows accurate time remaining
|
||||
- **Progress bar** correctly shows 0-100% for chunked files
|
||||
- No more speed dropping to 0 during large file uploads
|
||||
|
||||
#### 3. 💾 Storage Display Improvements
|
||||
- **Changed label** to "Data Storage (/data)" for clarity
|
||||
- **Uses statfs with f_bavail** for more accurate available space
|
||||
- **Real-time updates** every 5 seconds when connected
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.0.0
|
||||
|
||||
### 🐛 Critical Bug Fixes:
|
||||
|
||||
#### 1. 🔧 Path Normalization Fix
|
||||
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
|
||||
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
|
||||
- **Solution:** Added `NormalizePath()` function in both client and server
|
||||
- **Result:** 100% reliable directory creation and file uploads
|
||||
|
||||
#### 2. 📱 Android Mobile Client
|
||||
- **NEW:** Full-featured Android app for PS5 file management
|
||||
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
|
||||
- Upload files from phone to PS5
|
||||
- Download files from PS5 with Share option
|
||||
- Browse PS5 filesystem
|
||||
- Favorites for quick navigation
|
||||
- Debug Log with Copy to clipboard
|
||||
- Transfer History tracking
|
||||
|
||||
#### 3. 🛠️ Server Improvements
|
||||
- Path normalization in all handler functions
|
||||
- More robust error handling
|
||||
- Improved stability for parallel uploads
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v2.1.0
|
||||
|
||||
### 🚀 Performance Optimizations:
|
||||
|
||||
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
|
||||
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
|
||||
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
|
||||
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
|
||||
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
|
||||
- **Per-file:** 11-14 MB/s sustained per connection
|
||||
|
||||
#### 2. 📊 Transfer History
|
||||
- Complete history of all uploads and downloads
|
||||
- Success/Failed status tracking with error messages
|
||||
- Speed statistics (average, min, max)
|
||||
- Export to CSV/JSON for analysis
|
||||
- Persistent storage across sessions
|
||||
|
||||
#### 3. 🔄 Auto-Clear History on Startup
|
||||
- Optional checkbox to clear history automatically
|
||||
- Useful for keeping UI clean between sessions
|
||||
- Setting saved in `ps5_upload_settings.json`
|
||||
|
||||
#### 4. 🖥️ Maximized Window UI
|
||||
- Application opens in full-screen mode by default
|
||||
- Better visibility for large file transfers
|
||||
- Can be resized/restored as needed
|
||||
|
||||
### Technical Improvements:
|
||||
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
|
||||
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
|
||||
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
|
||||
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
|
||||
|
||||
---
|
||||
|
||||
## 📝 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
|
||||
|
||||
This software is provided as-is for personal use.
|
||||
|
||||
---
|
||||
|
||||
## 👤 Author
|
||||
|
||||
**Manos**
|
||||
|
||||
Created with ❤️ for the PS5 homebrew community
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- PS5 SDK
|
||||
- etaHEN
|
||||
- Inspired by ps5upload by PhantomPtr
|
||||
- PS5 homebrew community
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For issues or questions, please open an issue on GitHub.
|
||||
|
||||
---
|
||||
|
||||
**Enjoy blazing-fast file transfers on your PS5!** 🚀
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Reference in new issue
Block a user