mirror of
https://github.com/manos555555/PS5-Suite.git
synced 2026-10-06 18:00:31 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe741b8e9f | ||
|
|
0123a55f52 | ||
|
|
eb6003c8d4 | ||
|
|
5022381eeb | ||
|
|
8cdd392576 | ||
|
|
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 |
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
|
||||
+385
@@ -1,5 +1,390 @@
|
||||
# Changelog - PS5 Upload Suite
|
||||
|
||||
## Version 4.2.1 - Bugfix Release (February 8, 2026)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
#### UI Scrolling Fix
|
||||
- **Fixed connection panel eating screen space** - Added `MaxHeight` constraint (250px) and `ScrollViewer` to the connection/payload settings panel
|
||||
- **Fixed header overflow** - Added `MaxHeight="120"` to header row
|
||||
- **Fixed content area too small** - Added `MinHeight="200"` to main content area
|
||||
- When the Auto-send Payload Expander was open, it consumed half the screen leaving no room for file lists
|
||||
|
||||
#### File Counting Fix
|
||||
- **Fixed chunked files never counting as completed** - `fileChunkCounts` was set to the actual number of chunks (e.g., 4 for a 2GB file), but `UploadFileParallelAsync` handles all chunks internally as a single Task. The main loop only saw 1 task completion per file, so chunked files never reached their completion threshold
|
||||
- Now correctly sets `fileChunkCounts = 1` per file since each file = 1 task
|
||||
|
||||
#### Progress Display Fix
|
||||
- **Fixed progress appearing frozen during heavy uploads** - All UI updates used `DispatcherPriority.Background` (lowest priority), which got starved when 24 parallel connections flooded the dispatcher queue
|
||||
- Upgraded `UpdateUploadStats` to `DispatcherPriority.Normal`
|
||||
- Upgraded per-file progress bars to `DispatcherPriority.Render`
|
||||
- Progress now updates smoothly even at 95%+ network saturation
|
||||
|
||||
---
|
||||
|
||||
## Version 4.2.0 - Game Mounter Integration (February 7, 2026)
|
||||
|
||||
### 🎮 Game Mounter Integration
|
||||
- **Mount Games Button** - New green "🎮 Mount Games" button in the client UI
|
||||
- One-click mounting of all uploaded games directly from the PS5 Upload Suite
|
||||
- Button disabled when disconnected, shows "⏳ Mounting..." during operation
|
||||
|
||||
### 🔍 Multi-Path Game Scanning
|
||||
- Scans **6 locations** for games:
|
||||
- `/data/etaHEN/games` (internal storage)
|
||||
- `/mnt/usb0/games` through `/mnt/usb3/games` (USB drives)
|
||||
- `/mnt/ext0/games` (M.2 SSD)
|
||||
- First pass counts total games, second pass processes them
|
||||
- PS5 notifications show real-time progress: "Mounting 3/10 (30%) - Game Name"
|
||||
|
||||
### 🔄 Smart Mount Features
|
||||
- **Duplicate Detection** - Same title ID across multiple drives mounted only once
|
||||
- **Auto-Cleanup** - `auto_unmount_deleted_games()` removes stale mount entries
|
||||
- **Already Mounted Detection** - Skips games that are already mounted via nullfs
|
||||
- **sce_sys Validation** - Verifies game directory has valid `sce_sys/` before mounting
|
||||
|
||||
### 🔧 Payload Changes
|
||||
- **New Command** - `CMD_MOUNT_GAMES` (0x30) added to protocol
|
||||
- **nullfs Mount** - Games mounted to `/system_ex/app/<title_id>`
|
||||
- **DRM Patching** - Automatic `applicationDrmType` fix in `param.json`
|
||||
- **Metadata Copy** - `sce_sys` copied to `/user/app/` and `/user/appmeta/`
|
||||
- **mount.lnk** - Tracks mount source path for cleanup and remount
|
||||
- **Safe Delete** - Replaced `system("rm -rf ...")` with `rmdir_recursive()` to prevent hangs
|
||||
- **remount_system_ex()** - Remounts `/system_ex` as writable for game installation
|
||||
|
||||
### 🖥️ Client Changes
|
||||
- **Protocol.cs** - Added `MountGames = 0x30` enum and `MountGamesAsync()` method
|
||||
- **MainWindow.xaml** - Green "🎮 Mount Games" button between Upload and Cancel
|
||||
- **MainWindow.xaml.cs** - Click handler with progress display and auto-registration
|
||||
- **Auto Registration** - If new games detected, automatically runs `game_mounter.elf` via shell for home screen registration
|
||||
|
||||
### ⚡ Client Improvements
|
||||
- **Real-Time Speed Display** - New sliding window algorithm (10 samples over 5 seconds) for accurate real-time upload speed
|
||||
- **Smoother ETA** - Blended speed calculation (60% real-time + 40% average) with better time formatting (hours/minutes/seconds)
|
||||
- **Speed Display** - Shows both real-time and average speed: `Speed: 95 MB/s (avg 88 MB/s)`
|
||||
- **Duplicate File Dialog** - New `DuplicateFileDialog` for handling file conflicts during upload
|
||||
- **Move Dialog Refactor** - Replaced inline move dialog code with reusable `ShowPathPickerDialogAsync()`
|
||||
- **FormatFileSize Refactor** - Moved to static `FileUtils.FormatFileSize()` class for code reuse
|
||||
- **ETA Smoothing** - Reduced `ETASmoothingFactor` from 0.3 to 0.15 for less jumpy ETA display
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
#### Payload Fixes
|
||||
- **Fixed IOVEC_ENTRY(NULL) crash** - `strlen(NULL)` with `-O3` optimization caused segfault in `remount_system_ex()`
|
||||
- **Fixed libSceAppInstUtil startup crash** - Library cannot be loaded in etaHEN payload context (crashes dynamic linker). Switched to standalone `game_mounter.elf` for registration
|
||||
- **Fixed dlopen crash** - `dlopen("libSceAppInstUtil.sprx")` crashes payload thread. Removed dynamic loading approach
|
||||
- **Removed system() calls** - `system("rm -rf ...")` replaced with safe recursive `rmdir_recursive()` to prevent payload hangs
|
||||
- **Fixed compile warnings** - Removed unused `#include <dlfcn.h>`, re-enabled `auto_unmount_deleted_games()`
|
||||
|
||||
#### Client Fixes
|
||||
- **Fixed Shell Terminal hardcoded IP** - Shell was connecting to hardcoded `192.168.0.160` instead of the configured PS5 IP address
|
||||
- **Fixed ETA jumping** - ETA no longer jumps erratically during uploads due to improved smoothing
|
||||
- **Fixed speed dropping to 0** - Sliding window prevents speed display from showing 0 during brief pauses
|
||||
- **Fixed files remaining counter** - Simplified calculation with `Math.Max(0, ...)` to prevent negative values
|
||||
|
||||
### 📝 Technical Notes
|
||||
- `libSceAppInstUtil.sprx` is **not available** in etaHEN payload context (both static linking and dlopen crash)
|
||||
- Game registration (`sceAppInstUtilAppInstallTitleDir`) is handled by the standalone `game_mounter.elf` which runs as a separate ELF with its own library dependencies
|
||||
- The upload suite payload handles nullfs mounting, metadata copy, and DRM patching — everything except the final system registration
|
||||
|
||||
---
|
||||
|
||||
## 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 - **53 minutes total**
|
||||
- **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
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
# GitHub Upload Instructions - PS5 Upload Suite
|
||||
|
||||
## 📁 Correct Folder to Upload
|
||||
|
||||
**Source Path:**
|
||||
```
|
||||
C:\Users\HACKMAN\Desktop\ps5 test\ps5_rom_keys\ps5_upload_suite\client\bin\Release\net6.0-windows\win-x64\
|
||||
```
|
||||
|
||||
This folder contains the **complete, self-contained Windows executable** with all dependencies.
|
||||
|
||||
---
|
||||
|
||||
## 🗑️ Step 1: Delete Old Release on GitHub
|
||||
|
||||
1. Go to your GitHub repository
|
||||
2. Navigate to **Releases** tab
|
||||
3. Find the old/incorrect release
|
||||
4. Click **Delete** on the old release
|
||||
5. Confirm deletion
|
||||
|
||||
**Or delete via Git:**
|
||||
```bash
|
||||
# Delete old tag
|
||||
git tag -d v1.0
|
||||
git push origin :refs/tags/v1.0
|
||||
|
||||
# Delete old files from repository
|
||||
git rm -r ps5upload-windows-x64/
|
||||
git commit -m "Remove incorrect Windows build"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Step 2: Create Release Package
|
||||
|
||||
### Option A: Via PowerShell (Automated)
|
||||
|
||||
```powershell
|
||||
# Navigate to project root
|
||||
cd "C:\Users\HACKMAN\Desktop\ps5 test\ps5_rom_keys\ps5_upload_suite"
|
||||
|
||||
# Create release directory
|
||||
New-Item -ItemType Directory -Force -Path ".\github_release"
|
||||
|
||||
# Copy correct files
|
||||
Copy-Item -Path ".\client\bin\Release\net6.0-windows\win-x64\*" -Destination ".\github_release\" -Recurse
|
||||
|
||||
# Copy payload
|
||||
Copy-Item -Path ".\payload\ps5_upload_server.elf" -Destination ".\github_release\"
|
||||
|
||||
# Create ZIP
|
||||
Compress-Archive -Path ".\github_release\*" -DestinationPath ".\PS5Upload-Windows-x64-v1.0.zip" -Force
|
||||
|
||||
Write-Host "Release package created: PS5Upload-Windows-x64-v1.0.zip"
|
||||
```
|
||||
|
||||
### Option B: Manual
|
||||
|
||||
1. Create new folder: `PS5Upload-Windows-x64`
|
||||
2. Copy **ALL files** from `win-x64\` folder
|
||||
3. Add `ps5_upload_server.elf` from `payload\` folder
|
||||
4. Create ZIP: `PS5Upload-Windows-x64-v1.0.zip`
|
||||
|
||||
---
|
||||
|
||||
## ⬆️ Step 3: Upload to GitHub
|
||||
|
||||
### Via GitHub Web Interface:
|
||||
|
||||
1. Go to your repository on GitHub
|
||||
2. Click **Releases** → **Create a new release**
|
||||
3. Fill in:
|
||||
- **Tag:** `v1.0`
|
||||
- **Title:** `PS5 Upload Suite v1.0 - Windows x64`
|
||||
- **Description:**
|
||||
```markdown
|
||||
# PS5 Upload Suite v1.0
|
||||
|
||||
## Windows x64 Release
|
||||
|
||||
Self-contained executable for Windows 10/11 (64-bit)
|
||||
No .NET Runtime installation required!
|
||||
|
||||
## What's Included:
|
||||
- PS5Upload.exe (Windows GUI client)
|
||||
- ps5_upload_server.elf (PS5 server payload)
|
||||
- All required dependencies
|
||||
|
||||
## How to Use:
|
||||
1. Extract ZIP file
|
||||
2. Run PS5Upload.exe
|
||||
3. Load ps5_upload_server.elf to PS5 (via etaHEN)
|
||||
4. Connect and upload files!
|
||||
|
||||
## Requirements:
|
||||
- Windows 10/11 (64-bit)
|
||||
- PS5 with etaHEN 2.0+ or GoldHEN
|
||||
- Network connection to PS5
|
||||
|
||||
## File Size:
|
||||
- Full package: ~50 MB
|
||||
- Includes all .NET 6.0 dependencies
|
||||
```
|
||||
4. **Attach files:**
|
||||
- Upload `PS5Upload-Windows-x64-v1.0.zip`
|
||||
5. Click **Publish release**
|
||||
|
||||
### Via Git Command Line:
|
||||
|
||||
```bash
|
||||
# Navigate to repository
|
||||
cd "C:\Users\HACKMAN\Desktop\ps5 test\ps5_rom_keys\ps5_upload_suite"
|
||||
|
||||
# Add correct files
|
||||
git add client/bin/Release/net6.0-windows/win-x64/
|
||||
git commit -m "Add correct Windows x64 build with all dependencies"
|
||||
git push origin main
|
||||
|
||||
# Create and push tag
|
||||
git tag -a v1.0 -m "PS5 Upload Suite v1.0 - Windows x64"
|
||||
git push origin v1.0
|
||||
|
||||
# Upload release asset via GitHub CLI (if installed)
|
||||
gh release create v1.0 PS5Upload-Windows-x64-v1.0.zip --title "PS5 Upload Suite v1.0" --notes "Self-contained Windows x64 release"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Step 4: Update Repository README
|
||||
|
||||
Add download link to main README.md:
|
||||
|
||||
```markdown
|
||||
## Download
|
||||
|
||||
### Latest Release: v1.0
|
||||
|
||||
**Windows (x64):**
|
||||
- [PS5Upload-Windows-x64-v1.0.zip](https://github.com/YOUR_USERNAME/ps5_upload_suite/releases/download/v1.0/PS5Upload-Windows-x64-v1.0.zip)
|
||||
- Size: ~50 MB
|
||||
- Self-contained (no .NET installation required)
|
||||
|
||||
**PS5 Server:**
|
||||
- Included in Windows package: `ps5_upload_server.elf`
|
||||
- Load via etaHEN or GoldHEN
|
||||
|
||||
### System Requirements:
|
||||
- **Windows:** 10/11 (64-bit)
|
||||
- **PS5:** Firmware with etaHEN 2.0+ or GoldHEN
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Verification Checklist
|
||||
|
||||
Before uploading, verify:
|
||||
|
||||
- [ ] Folder contains `PS5Upload.exe` (8+ MB)
|
||||
- [ ] Folder contains `PS5Upload.dll`
|
||||
- [ ] Folder contains `PS5Upload.runtimeconfig.json`
|
||||
- [ ] Folder contains all DLL dependencies (100+ files)
|
||||
- [ ] `ps5_upload_server.elf` is included
|
||||
- [ ] ZIP file is created correctly
|
||||
- [ ] Test: Extract ZIP and run `PS5Upload.exe` - should work without errors
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Common Issues
|
||||
|
||||
### "The application requires .NET Runtime"
|
||||
**Problem:** Wrong folder uploaded (missing dependencies)
|
||||
**Solution:** Use the `win-x64` folder, not `release` folder
|
||||
|
||||
### "Missing DLL files"
|
||||
**Problem:** Incomplete upload
|
||||
**Solution:** Make sure ALL files from `win-x64` folder are included
|
||||
|
||||
### "Application won't start"
|
||||
**Problem:** Corrupted ZIP or missing files
|
||||
**Solution:** Re-create ZIP from scratch, include all files
|
||||
|
||||
---
|
||||
|
||||
## 📊 File Structure for GitHub
|
||||
|
||||
```
|
||||
PS5Upload-Windows-x64-v1.0.zip
|
||||
├── PS5Upload.exe (Main executable)
|
||||
├── PS5Upload.dll (Application library)
|
||||
├── PS5Upload.runtimeconfig.json (Runtime configuration)
|
||||
├── ps5_upload_server.elf (PS5 server payload)
|
||||
├── Accessibility.dll (Dependency)
|
||||
├── D3DCompiler_47_cor3.dll (Dependency)
|
||||
├── DirectWriteForwarder.dll (Dependency)
|
||||
├── Microsoft.*.dll (100+ Microsoft dependencies)
|
||||
├── PresentationCore.dll (WPF dependency)
|
||||
├── PresentationFramework.dll (WPF dependency)
|
||||
├── ReachFramework.dll (WPF dependency)
|
||||
├── System.*.dll (System dependencies)
|
||||
├── UIAutomationClient.dll (UI dependency)
|
||||
├── UIAutomationProvider.dll (UI dependency)
|
||||
├── UIAutomationTypes.dll (UI dependency)
|
||||
├── WindowsBase.dll (Windows dependency)
|
||||
├── WindowsFormsIntegration.dll (Windows dependency)
|
||||
├── api-ms-win-*.dll (Windows API dependencies)
|
||||
├── clrcompression.dll (Compression)
|
||||
├── clretwrc.dll (ETW)
|
||||
├── clrjit.dll (JIT compiler)
|
||||
├── coreclr.dll (CoreCLR runtime)
|
||||
├── createdump.exe (Debug tool)
|
||||
├── hostfxr.dll (Host)
|
||||
├── hostpolicy.dll (Host policy)
|
||||
├── mscordaccore.dll (DAC)
|
||||
├── mscordaccore_amd64_amd64_*.dll (DAC)
|
||||
├── mscordbi.dll (Debugger)
|
||||
├── mscorlib.dll (Core library)
|
||||
├── msquic.dll (QUIC)
|
||||
├── ucrtbase.dll (C runtime)
|
||||
├── vcruntime140.dll (VC runtime)
|
||||
└── vcruntime140_cor3.dll (VC runtime)
|
||||
```
|
||||
|
||||
**Total:** ~150 files, ~50 MB
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Commands Summary
|
||||
|
||||
```powershell
|
||||
# 1. Create release package
|
||||
cd "C:\Users\HACKMAN\Desktop\ps5 test\ps5_rom_keys\ps5_upload_suite"
|
||||
Compress-Archive -Path ".\client\bin\Release\net6.0-windows\win-x64\*" -DestinationPath ".\PS5Upload-Windows-x64-v1.0.zip" -Force
|
||||
|
||||
# 2. Test locally
|
||||
Expand-Archive -Path ".\PS5Upload-Windows-x64-v1.0.zip" -DestinationPath ".\test_extract" -Force
|
||||
.\test_extract\PS5Upload.exe
|
||||
|
||||
# 3. Upload to GitHub (manual via web interface)
|
||||
# Go to: https://github.com/YOUR_USERNAME/ps5_upload_suite/releases/new
|
||||
# Upload: PS5Upload-Windows-x64-v1.0.zip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Steps
|
||||
|
||||
1. **Delete old release** on GitHub
|
||||
2. **Create ZIP** from `win-x64` folder
|
||||
3. **Test ZIP** locally (extract and run)
|
||||
4. **Upload to GitHub** as new release
|
||||
5. **Update README** with download link
|
||||
6. **Announce** the correct release
|
||||
|
||||
---
|
||||
|
||||
**The correct folder is:**
|
||||
```
|
||||
C:\Users\HACKMAN\Desktop\ps5 test\ps5_rom_keys\ps5_upload_suite\client\bin\Release\net6.0-windows\win-x64\
|
||||
```
|
||||
|
||||
**This contains everything needed for a working Windows release!**
|
||||
-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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,41 +2,56 @@
|
||||
|
||||
**By Manos**
|
||||
|
||||
**Version 1.3.0 - Stable Release**
|
||||
**Version 4.2.1 - Bugfix Release**
|
||||
|
||||
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s** transfer speeds using a custom binary protocol.
|
||||
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 v1.3.0:** Complete stability overhaul - **42,801+ files uploaded with ZERO errors!**
|
||||
⭐ **NEW in v4.2.1 (Bugfix):**
|
||||
- 🖥️ **Fixed UI scrolling** - Connection panel no longer eats half the screen
|
||||
- 🔢 **Fixed file counting** - Chunked files now count correctly as completed
|
||||
- 📊 **Fixed frozen progress** - Progress updates no longer freeze during heavy uploads
|
||||
|
||||

|
||||

|
||||
⭐ **v4.2.0 Features:**
|
||||
- 🎮 **Mount Games Button** - Mount uploaded games directly from the client
|
||||
- 🔍 **Multi-Path Scanning** - Scans internal storage, USB drives (0-3), and M.2 SSD
|
||||
- 🔄 **Duplicate Detection** - Skips already-mounted games automatically
|
||||
- 📊 **Mount Summary** - Detailed results with PS5 notifications
|
||||
- 🧹 **Auto-Cleanup** - Removes mount entries for deleted games
|
||||
|
||||
📱 **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 sliding window speed tracking
|
||||
- Transfer History with success/failed tracking and retry
|
||||
- PS5 storage display (free/used/total)
|
||||
- Game mounting with one-click Mount Games button
|
||||
|
||||
### 3. 📱 Android Mobile Client
|
||||
- **File:** `PS5UploadMobile.apk`
|
||||
- Upload/download files from your phone
|
||||
- Multi-PS5 Profiles support
|
||||
- Browse PS5 filesystem
|
||||
- Favorites for quick navigation
|
||||
|
||||
---
|
||||
|
||||
@@ -44,17 +59,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
|
||||
|
||||
@@ -72,89 +83,81 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
|
||||
|
||||
## 🎨 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
|
||||
### PS5 Server Payload
|
||||
✅ **High-Speed Protocol** - Custom binary protocol for maximum speed
|
||||
✅ **8MB Buffer** - Large buffer for high throughput
|
||||
✅ **16MB Socket Buffers** - Maximum network throughput
|
||||
✅ **Direct Disk I/O** - No temp files, direct write syscalls
|
||||
✅ **Multi-threaded** - Handle multiple clients simultaneously
|
||||
✅ **Per-File Mutex** - Safe parallel uploads without corruption
|
||||
✅ **5-Minute Socket Timeout** - No connection drops on large files
|
||||
✅ **Game Mounting** - nullfs mount, DRM patching, metadata copy
|
||||
✅ **Shell Commands** - Built-in shell with ls, cd, cat, mkdir, rm, etc.
|
||||
✅ **Filesystem Indexing** - In-memory index for instant search
|
||||
|
||||
### Windows Client
|
||||
✅ **Modern UI** - Dark theme, clean design
|
||||
✅ **Modern UI** - Dark theme, maximized window
|
||||
✅ **Drag & Drop** - Files and folders
|
||||
✅ **Browse PS5** - Navigate filesystem
|
||||
✅ **Real-time Progress** - Speed & percentage
|
||||
✅ **Browse PS5** - Navigate filesystem with favorites
|
||||
✅ **Real-time Speed** - Sliding window algorithm for accurate speed display
|
||||
✅ **Parallel Uploads** - 4 parallel chunked connections for 104+ MB/s
|
||||
✅ **Transfer History** - Track all uploads/downloads with success/failed status
|
||||
✅ **Retry Failed Transfers** - Right-click to retry failed uploads
|
||||
✅ **Duplicate File Dialog** - Handle file conflicts during upload
|
||||
✅ **Folder Upload** - Recursive directory upload
|
||||
✅ **Storage Info** - Free space display
|
||||
✅ **Download Files** - Download from PS5 to PC
|
||||
✅ **💾 Storage Display** - Real-time PS5 storage info (free/used/total)
|
||||
✅ **🔍 Smart Search** - Full filesystem indexing with instant search
|
||||
- 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
|
||||
- Run system commands remotely
|
||||
- Real-time output display
|
||||
- Command history
|
||||
- Working directory support
|
||||
✅ **⭐ Favorites/Bookmarks** - Quick navigation to saved paths
|
||||
✅ **👥 Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
|
||||
✅ **🎮 Mount Games** - Mount uploaded games directly from the client
|
||||
- Scans all game paths: internal, USB drives (0-3), M.2 SSD
|
||||
- Duplicate detection across storage locations
|
||||
- Auto-cleanup of deleted game mounts
|
||||
- PS5 notifications with mount progress
|
||||
- Automatic game registration via game_mounter.elf
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
### v1.3.0 Tested Results:
|
||||
- ✅ **42,801 files** uploaded successfully
|
||||
- ✅ **Zero errors** - 100% success rate
|
||||
- ✅ **60-150 MB/s** sustained throughput
|
||||
- ✅ **Peak:** 190 MB/s per connection
|
||||
### 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
|
||||
|
||||
| Network | Expected Speed |
|
||||
|---------|----------------|
|
||||
| **Gigabit Ethernet** | 100-120 MB/s |
|
||||
| **WiFi 6 (5GHz)** | 60-80 MB/s |
|
||||
| **WiFi 5 (5GHz)** | 40-60 MB/s |
|
||||
### v4.1.0 Real-World Test (Astrobot):
|
||||
- ✅ **156,253 files** uploaded successfully in **53 minutes**
|
||||
- ✅ **1,724 directories** created
|
||||
- ✅ **Zero crashes** - stable memory usage throughout
|
||||
- ✅ **Smooth UI** - no stuttering with massive file counts
|
||||
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
@@ -186,22 +189,191 @@ See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v1.3.0
|
||||
## 📝 What's New in v4.2.0
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete list of all 15 bug fixes.
|
||||
### 🎮 Game Mounter Integration
|
||||
- **Mount Games Button** - New green "🎮 Mount Games" button in the client UI
|
||||
- **Multi-Path Scanning** - Scans 6 locations: `/data/etaHEN/games`, USB drives (0-3), M.2 SSD (`/mnt/ext0/games`)
|
||||
- **Duplicate Detection** - If the same game exists on multiple drives, it's mounted only once
|
||||
- **Auto-Cleanup** - Automatically unmounts entries for games that have been deleted
|
||||
- **PS5 Notifications** - Real-time progress notifications on PS5 screen during mounting
|
||||
- **Detailed Summary** - Shows new mounts, already mounted, duplicates skipped, and failures
|
||||
- **Auto Registration** - Automatically runs `game_mounter.elf` for home screen registration when new games are detected
|
||||
|
||||
### Highlights:
|
||||
### 🔧 Payload Improvements
|
||||
- **New Protocol Command** - `CMD_MOUNT_GAMES` (0x30) for triggering game mount from client
|
||||
- **nullfs Mounting** - Games mounted via nullfs to `/system_ex/app/`
|
||||
- **DRM Patching** - Automatic DRM type fix in param.json
|
||||
- **sce_sys Copy** - Copies game metadata to `/user/app/` and `/user/appmeta/`
|
||||
- **mount.lnk Tracking** - Creates mount link files for persistent mount state
|
||||
|
||||
### ⚡ Client Improvements
|
||||
- **Real-Time Speed Display** - Sliding window algorithm for accurate real-time upload speed
|
||||
- **Smoother ETA** - Blended speed calculation (60% real-time + 40% average) with better formatting
|
||||
- **Duplicate File Dialog** - New dialog for handling file conflicts during upload
|
||||
- **Move Dialog Refactor** - Reusable `ShowPathPickerDialogAsync()` replaces inline code
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- **Fixed IOVEC_ENTRY(NULL) crash** - `strlen(NULL)` with `-O3` optimization caused payload crash
|
||||
- **Fixed libSceAppInstUtil crash** - Library not available in etaHEN payload context, switched to standalone registration
|
||||
- **Removed system() calls** - Replaced `system("rm -rf ...")` with safe recursive delete to prevent hangs
|
||||
- **Fixed Shell Terminal hardcoded IP** - Shell was connecting to hardcoded IP instead of configured PS5 address
|
||||
- **Fixed ETA jumping** - ETA no longer jumps erratically during uploads
|
||||
- **Fixed speed dropping to 0** - Sliding window prevents speed display from showing 0 during pauses
|
||||
- **Fixed files remaining counter** - Prevented negative values in remaining files display
|
||||
|
||||
---
|
||||
|
||||
## 📝 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
|
||||
- ✅ **Optimal stability** - 6 parallel single-connection uploads
|
||||
- ✅ **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 +1,61 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace PS5Upload
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
private static readonly string LogFilePath = Path.Combine(
|
||||
AppDomain.CurrentDomain.BaseDirectory,
|
||||
$"ps5upload_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log");
|
||||
|
||||
private static readonly object _logLock = new object();
|
||||
|
||||
public App()
|
||||
{
|
||||
// Global exception handlers
|
||||
this.DispatcherUnhandledException += App_DispatcherUnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||
|
||||
LogToFile("=== Application Started ===");
|
||||
}
|
||||
|
||||
private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
LogToFile($"DISPATCHER EXCEPTION: {e.Exception}");
|
||||
e.Handled = true; // Prevent crash, but log it
|
||||
|
||||
MessageBox.Show($"Error: {e.Exception.Message}\n\nCheck log file: {LogFilePath}",
|
||||
"PS5 Upload Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
var ex = e.ExceptionObject as Exception;
|
||||
LogToFile($"UNHANDLED EXCEPTION: {ex}");
|
||||
}
|
||||
|
||||
private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
LogToFile($"TASK EXCEPTION: {e.Exception}");
|
||||
e.SetObserved(); // Prevent crash
|
||||
}
|
||||
|
||||
public static void LogToFile(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_logLock)
|
||||
{
|
||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
File.AppendAllText(LogFilePath, $"[{timestamp}] {message}\n");
|
||||
}
|
||||
}
|
||||
catch { /* Ignore logging errors */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
+835
-126
File diff suppressed because it is too large.
Load diff
+2456
-730
File diff suppressed because it is too large.
Load diff
+595
-89
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
@@ -22,6 +23,16 @@ namespace PS5Upload
|
||||
StartUpload = 0x10,
|
||||
UploadChunk = 0x11,
|
||||
EndUpload = 0x12,
|
||||
DownloadFile = 0x13,
|
||||
ShellOpen = 0x20,
|
||||
ShellExec = 0x21,
|
||||
ShellInterrupt = 0x22,
|
||||
ShellClose = 0x23,
|
||||
IndexStart = 0x40,
|
||||
IndexStatus = 0x41,
|
||||
SearchIndex = 0x42,
|
||||
MountGames = 0x30,
|
||||
IndexCancel = 0x43,
|
||||
Shutdown = 0xFF
|
||||
}
|
||||
|
||||
@@ -39,7 +50,7 @@ namespace PS5Upload
|
||||
{
|
||||
private TcpClient? _client;
|
||||
private NetworkStream? _stream;
|
||||
private const int BufferSize = 32 * 1024 * 1024; // 32MB for maximum throughput
|
||||
private const int BufferSize = 8 * 1024 * 1024; // 8MB buffer - matches payload BUFFER_SIZE for maximum throughput
|
||||
|
||||
public bool IsConnected => _client?.Connected ?? false;
|
||||
|
||||
@@ -48,8 +59,8 @@ namespace PS5Upload
|
||||
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.ReceiveBufferSize = 16 * 1024 * 1024; // 16MB - matches payload SO_RCVBUF setting
|
||||
_client.SendBufferSize = 16 * 1024 * 1024; // 16MB - matches payload SO_RCVBUF setting
|
||||
_client.NoDelay = true;
|
||||
_client.LingerState = new System.Net.Sockets.LingerOption(false, 0);
|
||||
|
||||
@@ -67,6 +78,9 @@ namespace PS5Upload
|
||||
_client = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Await the connect task to observe any exception it produced (prevents unobserved task crashes)
|
||||
await connectTask.ConfigureAwait(false);
|
||||
|
||||
// Check if connection actually succeeded
|
||||
if (!_client.Connected)
|
||||
@@ -130,7 +144,7 @@ namespace PS5Upload
|
||||
return (response, data);
|
||||
}
|
||||
|
||||
private async Task ReadExactAsync(byte[] buffer, int count, int timeoutMs = 30000)
|
||||
private async Task ReadExactAsync(byte[] buffer, int count, int timeoutMs = 120000)
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
|
||||
@@ -142,7 +156,13 @@ namespace PS5Upload
|
||||
while (offset < count)
|
||||
{
|
||||
int read = await _stream.ReadAsync(buffer, offset, count - offset, cts.Token);
|
||||
if (read == 0) throw new IOException("Connection closed");
|
||||
if (read == 0)
|
||||
{
|
||||
// Give PS5 a moment to recover before declaring connection dead
|
||||
await Task.Delay(100);
|
||||
read = await _stream.ReadAsync(buffer, offset, count - offset, cts.Token);
|
||||
if (read == 0) throw new IOException("Connection closed");
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
@@ -159,39 +179,71 @@ namespace PS5Upload
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<StorageInfo[]> ListStorageAsync()
|
||||
public async Task<PS5StorageInfo?> 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++)
|
||||
try
|
||||
{
|
||||
ushort pathLen = br.ReadUInt16();
|
||||
string path = Encoding.UTF8.GetString(br.ReadBytes(pathLen));
|
||||
long total = br.ReadInt64();
|
||||
long free = br.ReadInt64();
|
||||
await SendCommandAsync(Command.ListStorage);
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
|
||||
result[i] = new StorageInfo { Path = path, TotalBytes = total, FreeBytes = free };
|
||||
if (response != Response.Data)
|
||||
{
|
||||
Console.WriteLine($"[DEBUG] ListStorage: Response is not Data, got {response}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse response: total|free|available|reserved|mounted_games|user_data|real_free
|
||||
string responseStr = Encoding.UTF8.GetString(data);
|
||||
Console.WriteLine($"[DEBUG] ListStorage raw response: '{responseStr}'");
|
||||
|
||||
string[] parts = responseStr.Split('|');
|
||||
Console.WriteLine($"[DEBUG] ListStorage parts count: {parts.Length}");
|
||||
|
||||
if (parts.Length < 7)
|
||||
{
|
||||
Console.WriteLine($"[DEBUG] ListStorage: Expected at least 7 parts, got {parts.Length}");
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
Console.WriteLine($"[DEBUG] Part[{i}]: '{parts[i]}'");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get path if available (part 8)
|
||||
string storagePath = (parts.Length >= 8) ? parts[7].Trim() : "unknown";
|
||||
|
||||
return new PS5StorageInfo
|
||||
{
|
||||
TotalBytes = ulong.Parse(parts[0].Trim()),
|
||||
FreeBytes = ulong.Parse(parts[1].Trim()),
|
||||
AvailableBytes = ulong.Parse(parts[2].Trim()),
|
||||
ReservedBytes = ulong.Parse(parts[3].Trim()),
|
||||
MountedGamesSize = ulong.Parse(parts[4].Trim()),
|
||||
UserDataSize = ulong.Parse(parts[5].Trim()),
|
||||
RealFreeSpace = ulong.Parse(parts[6].Trim()),
|
||||
StoragePath = storagePath
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[DEBUG] ListStorage exception: {ex.Message}");
|
||||
Console.WriteLine($"[DEBUG] Stack trace: {ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<FileEntry[]> ListDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.ListDir, pathBytes);
|
||||
|
||||
// Payload sends ONLY RESP_DATA - no RESP_OK after!
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
|
||||
if (response != Response.Data) return Array.Empty<FileEntry>();
|
||||
if (response != Response.Data)
|
||||
{
|
||||
return Array.Empty<FileEntry>();
|
||||
}
|
||||
|
||||
using var ms = new MemoryStream(data);
|
||||
using var br = new BinaryReader(ms);
|
||||
@@ -199,7 +251,10 @@ namespace PS5Upload
|
||||
int count = br.ReadInt32();
|
||||
|
||||
// Handle empty directory
|
||||
if (count <= 0) return Array.Empty<FileEntry>();
|
||||
if (count <= 0)
|
||||
{
|
||||
return Array.Empty<FileEntry>();
|
||||
}
|
||||
|
||||
var result = new FileEntry[count];
|
||||
|
||||
@@ -257,21 +312,14 @@ namespace PS5Upload
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public event Action<string> OnProgressMessage;
|
||||
public event Action<string>? OnProgressMessage;
|
||||
|
||||
public async Task<bool> DeleteDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.DeleteDir, pathBytes);
|
||||
|
||||
// Receive initial OK response
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
if (response != Response.Ok)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now listen for progress messages synchronously
|
||||
// Payload does NOT send initial OK - goes straight to progress messages
|
||||
// Keep reading until we get the final OK/ERROR response
|
||||
bool deletionComplete = false;
|
||||
|
||||
@@ -313,7 +361,8 @@ namespace PS5Upload
|
||||
}
|
||||
|
||||
// Give server a moment to fully close the deletion thread
|
||||
await Task.Delay(100);
|
||||
// CRITICAL: Increased delay to prevent race condition with next delete
|
||||
await Task.Delay(500);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -357,7 +406,7 @@ namespace PS5Upload
|
||||
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)
|
||||
public async Task<bool> UploadFileAsync(string localPath, string remotePath, IProgress<UploadProgress>? progress = null, CancellationToken cancellationToken = default, long chunkOffset = 0, long chunkSize = 0, Action? onReadyCallback = null)
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(localPath);
|
||||
if (!fileInfo.Exists) return false;
|
||||
@@ -373,80 +422,481 @@ namespace PS5Upload
|
||||
BitConverter.GetBytes(chunkOffset).CopyTo(startData, pathBytes.Length + 9);
|
||||
|
||||
await SendCommandAsync(Command.StartUpload, startData);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
var (response, responseData) = await ReceiveResponseAsync();
|
||||
|
||||
if (response != Response.Ready) return false;
|
||||
// Debug logging
|
||||
OnProgressMessage?.Invoke($"[DEBUG] START_UPLOAD Response: {response} (Expected: {Response.Ready})");
|
||||
|
||||
if (response != Response.Ready)
|
||||
{
|
||||
OnProgressMessage?.Invoke($"[DEBUG] Upload failed - wrong response code");
|
||||
onReadyCallback?.Invoke(); // Signal even on failure so waiters don't deadlock
|
||||
return false;
|
||||
}
|
||||
|
||||
// Signal that file is created/pre-allocated on PS5 (for parallel chunk synchronization)
|
||||
onReadyCallback?.Invoke();
|
||||
|
||||
// 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;
|
||||
byte[][] sendBuffers = new byte[2][];
|
||||
sendBuffers[0] = ArrayPool<byte>.Shared.Rent(5 + BufferSize);
|
||||
sendBuffers[1] = ArrayPool<byte>.Shared.Rent(5 + BufferSize);
|
||||
|
||||
using (FileStream fs = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.SequentialScan))
|
||||
try
|
||||
{
|
||||
// Seek to chunk offset if specified
|
||||
if (chunkOffset > 0)
|
||||
// Use RandomAccess for chunked uploads to avoid disk I/O contention when multiple workers read same file
|
||||
FileOptions fileOptions = chunkOffset > 0 ? FileOptions.RandomAccess : FileOptions.SequentialScan;
|
||||
using (FileStream fs = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, fileOptions))
|
||||
{
|
||||
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)
|
||||
if (chunkOffset > 0)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
double currentSpeed = elapsed.TotalSeconds > 0 ? totalSent / elapsed.TotalSeconds : 0;
|
||||
avgSpeed = currentSpeed;
|
||||
fs.Seek(chunkOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
int activeBufferIndex = 0;
|
||||
long bytesRemaining = uploadSize;
|
||||
int bytesRead = await fs.ReadAsync(sendBuffers[activeBufferIndex], 5, (int)Math.Min(BufferSize, bytesRemaining), cancellationToken);
|
||||
int chunksSent = 0;
|
||||
|
||||
while (bytesRemaining > 0 && bytesRead > 0)
|
||||
{
|
||||
if (_stream == null) return false;
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
|
||||
TimeSpan eta = currentSpeed > 0
|
||||
? TimeSpan.FromSeconds(bytesRemaining / currentSpeed)
|
||||
: TimeSpan.Zero;
|
||||
|
||||
progress?.Report(new UploadProgress
|
||||
byte[] writeBuffer = sendBuffers[activeBufferIndex];
|
||||
writeBuffer[0] = (byte)Command.UploadChunk;
|
||||
BitConverter.GetBytes((uint)bytesRead).CopyTo(writeBuffer, 1);
|
||||
|
||||
long remainingAfterCurrent = bytesRemaining - bytesRead;
|
||||
int nextBufferIndex = 1 - activeBufferIndex;
|
||||
Task<int>? pendingReadTask = null;
|
||||
if (remainingAfterCurrent > 0)
|
||||
{
|
||||
BytesSent = chunkOffset + totalSent,
|
||||
TotalBytes = fileInfo.Length,
|
||||
SpeedBytesPerSecond = currentSpeed,
|
||||
AverageSpeedBytesPerSecond = avgSpeed,
|
||||
ElapsedTime = elapsed,
|
||||
EstimatedTimeRemaining = eta,
|
||||
CurrentFileName = fileInfo.Name
|
||||
});
|
||||
int nextReadLength = (int)Math.Min(BufferSize, remainingAfterCurrent);
|
||||
pendingReadTask = fs.ReadAsync(sendBuffers[nextBufferIndex], 5, nextReadLength, cancellationToken);
|
||||
}
|
||||
|
||||
using var writeTimeout = new CancellationTokenSource(TimeSpan.FromMinutes(15));
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, writeTimeout.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await _stream.WriteAsync(writeBuffer, 0, 5 + bytesRead, linkedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (writeTimeout.IsCancellationRequested)
|
||||
{
|
||||
if (pendingReadTask != null)
|
||||
{
|
||||
try { await pendingReadTask; } catch { }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
if (pendingReadTask == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
activeBufferIndex = 1 - activeBufferIndex;
|
||||
bytesRead = await pendingReadTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(sendBuffers[0]);
|
||||
ArrayPool<byte>.Shared.Return(sendBuffers[1]);
|
||||
}
|
||||
|
||||
// Send END_UPLOAD - fire-and-forget for maximum speed
|
||||
// Don't wait for response to avoid PS5 payload bottleneck
|
||||
// Send END_UPLOAD and wait for response
|
||||
// CRITICAL: Must wait for response to avoid protocol desync on chunked uploads
|
||||
try
|
||||
{
|
||||
await SendCommandAsync(Command.EndUpload);
|
||||
// Immediately return success without waiting for response
|
||||
// This eliminates the PS5 response handling bottleneck
|
||||
return true;
|
||||
var (endResponse, _) = await ReceiveResponseAsync();
|
||||
return endResponse == Response.Ok;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // Assume success if connection issues
|
||||
return false; // Connection issue means upload failed
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadFileAsync(string remotePath, string localPath, IProgress<UploadProgress>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Send download command (must include null terminator for C strlen())
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(remotePath + "\0");
|
||||
await SendCommandAsync(Command.DownloadFile, pathBytes);
|
||||
|
||||
// Receive response header (5 bytes: 1 response + 4 data_len)
|
||||
byte[] header = new byte[5];
|
||||
await ReadExactAsync(header, 5);
|
||||
|
||||
Response response = (Response)header[0];
|
||||
uint dataLen = BitConverter.ToUInt32(header, 1);
|
||||
|
||||
// Check if error response
|
||||
if (response == Response.Error)
|
||||
{
|
||||
if (dataLen > 0)
|
||||
{
|
||||
byte[] errorMsg = new byte[dataLen];
|
||||
await ReadExactAsync(errorMsg, (int)dataLen);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expecting RESP_DATA with 8-byte file size
|
||||
if (response != Response.Data || dataLen != 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read file size (8 bytes)
|
||||
byte[] sizeBytes = new byte[8];
|
||||
await ReadExactAsync(sizeBytes, 8);
|
||||
long fileSize = BitConverter.ToInt64(sizeBytes, 0);
|
||||
|
||||
// Now read raw file data directly from socket
|
||||
using var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize);
|
||||
|
||||
byte[] buffer = new byte[BufferSize];
|
||||
long totalReceived = 0;
|
||||
var startTime = DateTime.Now;
|
||||
|
||||
while (totalReceived < fileSize)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
|
||||
int toRead = (int)Math.Min(BufferSize, fileSize - totalReceived);
|
||||
int received = await _stream!.ReadAsync(buffer, 0, toRead, cancellationToken);
|
||||
|
||||
if (received == 0) break;
|
||||
|
||||
await fs.WriteAsync(buffer, 0, received, cancellationToken);
|
||||
totalReceived += received;
|
||||
|
||||
// Report progress every 5MB or at completion
|
||||
if (totalReceived % (5 * 1024 * 1024) < BufferSize || totalReceived == fileSize)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
double speed = elapsed.TotalSeconds > 0 ? totalReceived / elapsed.TotalSeconds : 0;
|
||||
|
||||
progress?.Report(new UploadProgress
|
||||
{
|
||||
BytesSent = totalReceived,
|
||||
TotalBytes = fileSize,
|
||||
SpeedBytesPerSecond = speed,
|
||||
AverageSpeedBytesPerSecond = speed,
|
||||
ElapsedTime = elapsed,
|
||||
EstimatedTimeRemaining = speed > 0 ? TimeSpan.FromSeconds((fileSize - totalReceived) / speed) : TimeSpan.Zero,
|
||||
CurrentFileName = Path.GetFileName(localPath)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return totalReceived == fileSize;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Download error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> OpenShellAsync()
|
||||
{
|
||||
await SendCommandAsync(Command.ShellOpen);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<string> ExecuteShellCommandAsync(string command)
|
||||
{
|
||||
byte[] cmdBytes = Encoding.UTF8.GetBytes(command + "\0");
|
||||
await SendCommandAsync(Command.ShellExec, cmdBytes);
|
||||
|
||||
// Payload sends multiple RESP_DATA responses (e.g., ls sends one per file)
|
||||
// Read all RESP_DATA until we get RESP_OK or RESP_ERROR
|
||||
var outputBuilder = new System.Text.StringBuilder();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
|
||||
if (response == Response.Data)
|
||||
{
|
||||
// Accumulate output from multiple RESP_DATA responses
|
||||
string chunk = Encoding.UTF8.GetString(data).TrimEnd('\0');
|
||||
outputBuilder.Append(chunk);
|
||||
}
|
||||
else if (response == Response.Ok)
|
||||
{
|
||||
// End of output - return accumulated data
|
||||
return outputBuilder.ToString();
|
||||
}
|
||||
else if (response == Response.Error)
|
||||
{
|
||||
string error = data.Length > 0 ? Encoding.UTF8.GetString(data).TrimEnd('\0') : "Command failed";
|
||||
return $"Error: {error}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unexpected response
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return outputBuilder.ToString();
|
||||
}
|
||||
|
||||
public async Task<bool> CloseShellAsync()
|
||||
{
|
||||
await SendCommandAsync(Command.ShellClose);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
// Index methods
|
||||
public async Task<bool> StartIndexAsync(string paths)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(paths + "\0");
|
||||
await SendCommandAsync(Command.IndexStart, pathBytes);
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<string> GetIndexStatusAsync()
|
||||
{
|
||||
await SendCommandAsync(Command.IndexStatus);
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
if (response == Response.Ok && data.Length > 0)
|
||||
{
|
||||
return Encoding.UTF8.GetString(data).TrimEnd('\0');
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
public async Task<SearchResult[]> SearchIndexAsync(string query)
|
||||
{
|
||||
byte[] queryBytes = Encoding.UTF8.GetBytes(query + "\0");
|
||||
await SendCommandAsync(Command.SearchIndex, queryBytes);
|
||||
|
||||
var results = new System.Collections.Generic.List<SearchResult>();
|
||||
|
||||
// Payload sends: RESP_DATA(1) + raw_data for each result, then RESP_OK(1) + data_len(4) + message
|
||||
if (_stream == null) return Array.Empty<SearchResult>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Read response byte
|
||||
byte[] respBuf = new byte[1];
|
||||
try { await ReadExactAsync(respBuf, 1); }
|
||||
catch { break; }
|
||||
|
||||
Response response = (Response)respBuf[0];
|
||||
|
||||
if (response == Response.Data)
|
||||
{
|
||||
// Read raw data: path_len(4) + path + name_len(4) + name + size(8) + mtime(8) + is_dir(1)
|
||||
byte[] pathLenBuf = new byte[4];
|
||||
await ReadExactAsync(pathLenBuf, 4);
|
||||
uint pathLen = BitConverter.ToUInt32(pathLenBuf, 0);
|
||||
|
||||
byte[] pathBuf = new byte[pathLen];
|
||||
await ReadExactAsync(pathBuf, (int)pathLen);
|
||||
string path = Encoding.UTF8.GetString(pathBuf);
|
||||
|
||||
byte[] nameLenBuf = new byte[4];
|
||||
await ReadExactAsync(nameLenBuf, 4);
|
||||
uint nameLen = BitConverter.ToUInt32(nameLenBuf, 0);
|
||||
|
||||
byte[] nameBuf = new byte[nameLen];
|
||||
await ReadExactAsync(nameBuf, (int)nameLen);
|
||||
string name = Encoding.UTF8.GetString(nameBuf);
|
||||
|
||||
byte[] sizeBuf = new byte[8];
|
||||
await ReadExactAsync(sizeBuf, 8);
|
||||
long size = BitConverter.ToInt64(sizeBuf, 0);
|
||||
|
||||
byte[] mtimeBuf = new byte[8];
|
||||
await ReadExactAsync(mtimeBuf, 8);
|
||||
long mtime = BitConverter.ToInt64(mtimeBuf, 0);
|
||||
|
||||
byte[] isDirBuf = new byte[1];
|
||||
await ReadExactAsync(isDirBuf, 1);
|
||||
bool isDir = isDirBuf[0] == 1;
|
||||
|
||||
results.Add(new SearchResult
|
||||
{
|
||||
Path = path,
|
||||
Name = name,
|
||||
Size = size,
|
||||
Modified = DateTimeOffset.FromUnixTimeSeconds(mtime).DateTime,
|
||||
IsDirectory = isDir
|
||||
});
|
||||
}
|
||||
else if (response == Response.Ok)
|
||||
{
|
||||
// Read and discard the OK message
|
||||
byte[] dataLenBuf = new byte[4];
|
||||
await ReadExactAsync(dataLenBuf, 4);
|
||||
uint dataLen = BitConverter.ToUInt32(dataLenBuf, 0);
|
||||
if (dataLen > 0)
|
||||
{
|
||||
byte[] msgBuf = new byte[dataLen];
|
||||
await ReadExactAsync(msgBuf, (int)dataLen);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (response == Response.Error)
|
||||
{
|
||||
// Read error message
|
||||
byte[] dataLenBuf = new byte[4];
|
||||
await ReadExactAsync(dataLenBuf, 4);
|
||||
uint dataLen = BitConverter.ToUInt32(dataLenBuf, 0);
|
||||
if (dataLen > 0)
|
||||
{
|
||||
byte[] msgBuf = new byte[dataLen];
|
||||
await ReadExactAsync(msgBuf, (int)dataLen);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
// Mount all games on PS5 (scans /data/etaHEN/games, USB drives, M.2 SSD)
|
||||
// Response pattern: RESP_OK with summary text
|
||||
public async Task<string?> MountGamesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendCommandAsync(Command.MountGames);
|
||||
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
|
||||
if (response == Response.Ok && data != null)
|
||||
{
|
||||
return Encoding.UTF8.GetString(data);
|
||||
}
|
||||
else if (response == Response.Error && data != null)
|
||||
{
|
||||
return "ERROR: " + Encoding.UTF8.GetString(data);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ERROR] MountGamesAsync: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Send payload ELF file to PS5 (port 9021)
|
||||
public static async Task<bool> SendPayloadAsync(string ipAddress, string payloadPath, int port = 9021, IProgress<long>? progress = null)
|
||||
{
|
||||
TcpClient? client = null;
|
||||
try
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(payloadPath);
|
||||
if (!fileInfo.Exists) return false;
|
||||
|
||||
// Validate file extension
|
||||
string ext = fileInfo.Extension.ToLower();
|
||||
if (ext != ".elf" && ext != ".bin")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
client = new TcpClient();
|
||||
client.SendBufferSize = 8 * 1024 * 1024;
|
||||
client.ReceiveBufferSize = 8 * 1024 * 1024;
|
||||
client.NoDelay = true;
|
||||
client.SendTimeout = 10000;
|
||||
|
||||
// Connect with 10 second timeout
|
||||
var connectTask = client.ConnectAsync(ipAddress, port);
|
||||
var timeoutTask = Task.Delay(10000);
|
||||
var completedTask = await Task.WhenAny(connectTask, timeoutTask);
|
||||
|
||||
if (completedTask == timeoutTask)
|
||||
{
|
||||
client?.Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for connect task to complete
|
||||
await connectTask;
|
||||
|
||||
if (!client.Connected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var stream = client.GetStream();
|
||||
using var fs = new FileStream(payloadPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
|
||||
// Use larger buffer for faster transfer
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
long totalSent = 0;
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
await stream.WriteAsync(buffer, 0, bytesRead);
|
||||
totalSent += bytesRead;
|
||||
progress?.Report(totalSent);
|
||||
}
|
||||
|
||||
await stream.FlushAsync();
|
||||
|
||||
// Give PS5 time to process and execute payload
|
||||
await Task.Delay(500);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
client?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,6 +906,29 @@ namespace PS5Upload
|
||||
}
|
||||
}
|
||||
|
||||
public class SearchResult
|
||||
{
|
||||
public string Path { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public long Size { get; set; }
|
||||
public DateTime Modified { get; set; }
|
||||
public bool IsDirectory { get; set; }
|
||||
public string SizeText => FormatFileSize(Size);
|
||||
|
||||
private static string FormatFileSize(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
|
||||
double len = bytes;
|
||||
int order = 0;
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len = len / 1024;
|
||||
}
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
}
|
||||
|
||||
public class StorageInfo
|
||||
{
|
||||
public string Path { get; set; } = "";
|
||||
@@ -463,6 +936,39 @@ namespace PS5Upload
|
||||
public long FreeBytes { get; set; }
|
||||
}
|
||||
|
||||
public class PS5StorageInfo
|
||||
{
|
||||
public ulong TotalBytes { get; set; }
|
||||
public ulong FreeBytes { get; set; }
|
||||
public ulong AvailableBytes { get; set; }
|
||||
public ulong ReservedBytes { get; set; }
|
||||
public ulong MountedGamesSize { get; set; }
|
||||
public ulong UserDataSize { get; set; }
|
||||
public ulong RealFreeSpace { get; set; }
|
||||
public string StoragePath { get; set; } = "unknown";
|
||||
|
||||
public string TotalGB => FormatBytes(TotalBytes);
|
||||
public string FreeGB => FormatBytes(FreeBytes);
|
||||
public string RealFreeGB => FormatBytes(RealFreeSpace);
|
||||
public string MountedGamesGB => FormatBytes(MountedGamesSize);
|
||||
public string UserDataGB => FormatBytes(UserDataSize);
|
||||
public string ReservedGB => FormatBytes(ReservedBytes);
|
||||
|
||||
public static string FormatBytes(ulong bytes)
|
||||
{
|
||||
// Use 1000-based (decimal) like PS5, not 1024-based (binary)
|
||||
if (bytes >= 1000UL * 1000 * 1000 * 1000)
|
||||
return $"{bytes / (1000.0 * 1000 * 1000 * 1000):F1} TB";
|
||||
if (bytes >= 1000UL * 1000 * 1000)
|
||||
return $"{bytes / (1000.0 * 1000 * 1000):F1} GB";
|
||||
if (bytes >= 1000UL * 1000)
|
||||
return $"{bytes / (1000.0 * 1000):F1} MB";
|
||||
if (bytes >= 1000UL)
|
||||
return $"{bytes / 1000.0:F1} KB";
|
||||
return $"{bytes} bytes";
|
||||
}
|
||||
}
|
||||
|
||||
public class FileEntry
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
# PS5 Upload Suite - GitHub Release Package Creator
|
||||
# Creates a clean, ready-to-upload ZIP file for GitHub releases
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "PS5 Upload Suite - Release Packager" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Configuration
|
||||
$sourceFolder = ".\client\bin\Release\net6.0-windows\win-x64"
|
||||
$payloadFile = ".\payload\ps5_upload_server.elf"
|
||||
$outputZip = ".\PS5Upload-Windows-x64-v1.0.zip"
|
||||
$tempFolder = ".\github_release_temp"
|
||||
|
||||
# Check if source folder exists
|
||||
if (-not (Test-Path $sourceFolder)) {
|
||||
Write-Host "[ERROR] Source folder not found: $sourceFolder" -ForegroundColor Red
|
||||
Write-Host "Please build the project first!" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[1/5] Checking source files..." -ForegroundColor Yellow
|
||||
|
||||
# Count files in source
|
||||
$fileCount = (Get-ChildItem -Path $sourceFolder -File).Count
|
||||
Write-Host " Found $fileCount files in source folder" -ForegroundColor Green
|
||||
|
||||
# Check for main executable
|
||||
$mainExe = Join-Path $sourceFolder "PS5Upload.exe"
|
||||
if (-not (Test-Path $mainExe)) {
|
||||
Write-Host "[ERROR] PS5Upload.exe not found!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$exeSize = (Get-Item $mainExe).Length / 1MB
|
||||
Write-Host " PS5Upload.exe: $([math]::Round($exeSize, 2)) MB" -ForegroundColor Green
|
||||
|
||||
# Check for payload
|
||||
if (Test-Path $payloadFile) {
|
||||
$payloadSize = (Get-Item $payloadFile).Length / 1KB
|
||||
Write-Host " ps5_upload_server.elf: $([math]::Round($payloadSize, 2)) KB" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [WARN] ps5_upload_server.elf not found - will skip" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[2/5] Creating temporary folder..." -ForegroundColor Yellow
|
||||
|
||||
# Clean up old temp folder
|
||||
if (Test-Path $tempFolder) {
|
||||
Remove-Item -Path $tempFolder -Recurse -Force
|
||||
}
|
||||
|
||||
# Create temp folder
|
||||
New-Item -ItemType Directory -Path $tempFolder -Force | Out-Null
|
||||
Write-Host " Created: $tempFolder" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[3/5] Copying files..." -ForegroundColor Yellow
|
||||
|
||||
# Copy all files from source
|
||||
Copy-Item -Path "$sourceFolder\*" -Destination $tempFolder -Recurse -Force
|
||||
Write-Host " Copied $fileCount files from win-x64 build" -ForegroundColor Green
|
||||
|
||||
# Copy payload if exists
|
||||
if (Test-Path $payloadFile) {
|
||||
Copy-Item -Path $payloadFile -Destination $tempFolder -Force
|
||||
Write-Host " Copied ps5_upload_server.elf" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Create README for the release
|
||||
$readmeContent = @"
|
||||
# PS5 Upload Suite v1.0 - Windows x64
|
||||
|
||||
## What's This?
|
||||
Self-contained Windows application for uploading files to PS5 console via network.
|
||||
|
||||
## Requirements
|
||||
- Windows 10/11 (64-bit)
|
||||
- PS5 with etaHEN 2.0+ or GoldHEN
|
||||
- Network connection between PC and PS5
|
||||
|
||||
## Installation
|
||||
1. Extract all files to a folder
|
||||
2. Run PS5Upload.exe
|
||||
|
||||
## First Time Setup
|
||||
1. Load ps5_upload_server.elf to your PS5 (via etaHEN payload injector)
|
||||
2. Note the IP address shown on PS5 screen
|
||||
3. Open PS5Upload.exe on Windows
|
||||
4. Enter PS5 IP address
|
||||
5. Click Connect
|
||||
|
||||
## Usage
|
||||
1. Click "Browse" to select files
|
||||
2. Click "Upload" to transfer to PS5
|
||||
3. Files will be saved to /data/ on PS5
|
||||
|
||||
## Troubleshooting
|
||||
- **Connection failed:** Make sure PS5 server is running and IP is correct
|
||||
- **Upload failed:** Check PS5 has enough free space
|
||||
- **App won't start:** Make sure all files are extracted
|
||||
|
||||
## Support
|
||||
GitHub: https://github.com/YOUR_USERNAME/ps5_upload_suite
|
||||
Issues: https://github.com/YOUR_USERNAME/ps5_upload_suite/issues
|
||||
|
||||
## License
|
||||
MIT License - See repository for details
|
||||
"@
|
||||
|
||||
Set-Content -Path "$tempFolder\README.txt" -Value $readmeContent -Encoding UTF8
|
||||
Write-Host " Created README.txt" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[4/5] Creating ZIP archive..." -ForegroundColor Yellow
|
||||
|
||||
# Remove old ZIP if exists
|
||||
if (Test-Path $outputZip) {
|
||||
Remove-Item -Path $outputZip -Force
|
||||
Write-Host " Removed old ZIP file" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Create ZIP
|
||||
Compress-Archive -Path "$tempFolder\*" -DestinationPath $outputZip -CompressionLevel Optimal -Force
|
||||
|
||||
if (Test-Path $outputZip) {
|
||||
$zipSize = (Get-Item $outputZip).Length / 1MB
|
||||
Write-Host " Created: $outputZip" -ForegroundColor Green
|
||||
Write-Host " Size: $([math]::Round($zipSize, 2)) MB" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[ERROR] Failed to create ZIP file!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[5/5] Cleaning up..." -ForegroundColor Yellow
|
||||
|
||||
# Remove temp folder
|
||||
Remove-Item -Path $tempFolder -Recurse -Force
|
||||
Write-Host " Removed temporary files" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "SUCCESS!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Release package created: $outputZip" -ForegroundColor Green
|
||||
Write-Host "Size: $([math]::Round($zipSize, 2)) MB" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Yellow
|
||||
Write-Host "1. Test the ZIP file locally (extract and run PS5Upload.exe)" -ForegroundColor White
|
||||
Write-Host "2. Go to GitHub: https://github.com/YOUR_USERNAME/ps5_upload_suite/releases/new" -ForegroundColor White
|
||||
Write-Host "3. Create new release with tag 'v1.0'" -ForegroundColor White
|
||||
Write-Host "4. Upload: $outputZip" -ForegroundColor White
|
||||
Write-Host "5. Publish release" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "Press any key to exit..."
|
||||
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||
@@ -1,33 +0,0 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
"C:\Program Files\GitHub CLI\gh.exe" release create v1.2 PS5Upload-Windows-x64-v1.2-Complete.zip --title "PS5 Upload Suite v1.2 - Complete Bug Fix" --notes "# PS5 Upload Suite v1.2 - Complete Bug Fix Release
|
||||
|
||||
## What's Fixed:
|
||||
|
||||
### CLIENT FIXES:
|
||||
- Bug #1: Connection timeout reduced to 5 seconds (fast fail on wrong IP)
|
||||
- Bug #2: Folder deletion timeout increased to 60 seconds
|
||||
|
||||
### SERVER FIXES:
|
||||
- Bug #3: Async folder deletion - NO MORE TIMEOUT on large folders!
|
||||
- Server responds immediately
|
||||
- Deletion happens in background thread
|
||||
- PS5 notification when complete
|
||||
|
||||
## What's Included:
|
||||
- PS5Upload.exe - Windows client with bug fixes
|
||||
- ps5_upload_server.elf - PS5 server with async deletion
|
||||
- All dependencies included
|
||||
|
||||
## Installation:
|
||||
1. Extract ZIP file
|
||||
2. Run PS5Upload.exe on Windows
|
||||
3. Upload ps5_upload_server.elf to PS5 via FTP
|
||||
4. Load with etaHEN payload injector
|
||||
|
||||
## IMPORTANT:
|
||||
You MUST update BOTH files (client and server)
|
||||
|
||||
## Requirements:
|
||||
- Windows 10/11 (64-bit)
|
||||
- PS5 with etaHEN 2.0+ or GoldHEN"
|
||||
@@ -1,122 +0,0 @@
|
||||
# PS5 Upload Suite v1.1 - Bug Fix Release Package Creator
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "PS5 Upload Suite v1.1 - Bug Fix Release" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Configuration
|
||||
$sourceFolder = ".\client\bin\Release\net6.0-windows\win-x64"
|
||||
$payloadFile = ".\payload\ps5_upload_server.elf"
|
||||
$outputZip = ".\PS5Upload-Windows-x64-v1.1-BugFix.zip"
|
||||
$tempFolder = ".\github_release_v1.1_temp"
|
||||
|
||||
Write-Host "[INFO] Bug Fixes in v1.1:" -ForegroundColor Yellow
|
||||
Write-Host " 1. Connection timeout: 5 seconds (fast fail on wrong IP)" -ForegroundColor Green
|
||||
Write-Host " 2. Folder deletion timeout: 60 seconds (handles large folders)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Check if source folder exists
|
||||
if (-not (Test-Path $sourceFolder)) {
|
||||
Write-Host "[ERROR] Source folder not found: $sourceFolder" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[1/5] Checking source files..." -ForegroundColor Yellow
|
||||
$fileCount = (Get-ChildItem -Path $sourceFolder -File).Count
|
||||
Write-Host " Found $fileCount files" -ForegroundColor Green
|
||||
|
||||
# Clean up old temp folder
|
||||
if (Test-Path $tempFolder) {
|
||||
Remove-Item -Path $tempFolder -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[2/5] Creating temporary folder..." -ForegroundColor Yellow
|
||||
New-Item -ItemType Directory -Path $tempFolder -Force | Out-Null
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[3/5] Copying files..." -ForegroundColor Yellow
|
||||
Copy-Item -Path "$sourceFolder\*" -Destination $tempFolder -Recurse -Force
|
||||
Write-Host " Copied $fileCount files" -ForegroundColor Green
|
||||
|
||||
if (Test-Path $payloadFile) {
|
||||
Copy-Item -Path $payloadFile -Destination $tempFolder -Force
|
||||
Write-Host " Copied ps5_upload_server.elf" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Create CHANGELOG
|
||||
$changelogContent = @"
|
||||
# PS5 Upload Suite v1.1 - Bug Fix Release
|
||||
|
||||
## What's Fixed:
|
||||
|
||||
### Bug #1: Connection Timeout
|
||||
- **Problem:** Application took 30+ seconds to detect wrong IP address
|
||||
- **Fix:** Added 5-second timeout for connection attempts
|
||||
- **Result:** Fast failure on incorrect IP - immediate feedback
|
||||
|
||||
### Bug #2: Folder Deletion Timeout
|
||||
- **Problem:** Deleting large game folders caused timeout errors
|
||||
- **Fix:** Increased timeout to 60 seconds for folder operations
|
||||
- **Result:** Successfully handles large folders (10GB+ games)
|
||||
|
||||
## Installation:
|
||||
Same as v1.0 - extract and run PS5Upload.exe
|
||||
|
||||
## Upgrading from v1.0:
|
||||
Simply replace your old PS5Upload.exe with the new one.
|
||||
No other changes needed.
|
||||
|
||||
## Requirements:
|
||||
- Windows 10/11 (64-bit)
|
||||
- PS5 with etaHEN 2.0+ or GoldHEN
|
||||
- Network connection
|
||||
|
||||
## Support:
|
||||
GitHub: https://github.com/YOUR_USERNAME/ps5_upload_suite
|
||||
"@
|
||||
|
||||
Set-Content -Path "$tempFolder\CHANGELOG.txt" -Value $changelogContent -Encoding UTF8
|
||||
Write-Host " Created CHANGELOG.txt" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[4/5] Creating ZIP archive..." -ForegroundColor Yellow
|
||||
|
||||
if (Test-Path $outputZip) {
|
||||
Remove-Item -Path $outputZip -Force
|
||||
}
|
||||
|
||||
Compress-Archive -Path "$tempFolder\*" -DestinationPath $outputZip -CompressionLevel Optimal -Force
|
||||
|
||||
if (Test-Path $outputZip) {
|
||||
$zipSize = (Get-Item $outputZip).Length / 1MB
|
||||
Write-Host " Created: $outputZip" -ForegroundColor Green
|
||||
Write-Host " Size: $([math]::Round($zipSize, 2)) MB" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[ERROR] Failed to create ZIP!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[5/5] Cleaning up..." -ForegroundColor Yellow
|
||||
Remove-Item -Path $tempFolder -Recurse -Force
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "SUCCESS!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Release package created: $outputZip" -ForegroundColor Green
|
||||
Write-Host "Size: $([math]::Round($zipSize, 2)) MB" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Bug Fixes:" -ForegroundColor Yellow
|
||||
Write-Host " [FIXED] Connection timeout: 5 seconds" -ForegroundColor Green
|
||||
Write-Host " [FIXED] Folder deletion timeout: 60 seconds" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Yellow
|
||||
Write-Host "1. Test locally: Extract and run PS5Upload.exe" -ForegroundColor White
|
||||
Write-Host "2. Upload to GitHub as v1.1 release" -ForegroundColor White
|
||||
Write-Host "3. Tag: v1.1" -ForegroundColor White
|
||||
Write-Host ""
|
||||
@@ -1,199 +0,0 @@
|
||||
# PS5 Upload Suite v1.2 - Complete Bug Fix Release
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "PS5 Upload Suite v1.2 - Complete Fix" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Configuration
|
||||
$sourceFolder = ".\client\bin\Release\net6.0-windows\win-x64"
|
||||
$payloadFile = ".\payload\ps5_upload_server.elf"
|
||||
$outputZip = ".\PS5Upload-Windows-x64-v1.2-Complete.zip"
|
||||
$tempFolder = ".\github_release_v1.2_temp"
|
||||
|
||||
Write-Host "[INFO] Bug Fixes in v1.2:" -ForegroundColor Yellow
|
||||
Write-Host " CLIENT:" -ForegroundColor Cyan
|
||||
Write-Host " 1. Connection timeout: 5 seconds (fast fail on wrong IP)" -ForegroundColor Green
|
||||
Write-Host " 2. Folder deletion timeout: 60 seconds client-side" -ForegroundColor Green
|
||||
Write-Host " SERVER:" -ForegroundColor Cyan
|
||||
Write-Host " 3. Async folder deletion (no more timeout on large folders!)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Check files
|
||||
if (-not (Test-Path $sourceFolder)) {
|
||||
Write-Host "[ERROR] Client folder not found!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $payloadFile)) {
|
||||
Write-Host "[ERROR] Server payload not found!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "[1/5] Checking files..." -ForegroundColor Yellow
|
||||
$fileCount = (Get-ChildItem -Path $sourceFolder -File).Count
|
||||
$payloadSize = (Get-Item $payloadFile).Length / 1KB
|
||||
Write-Host " Client files: $fileCount" -ForegroundColor Green
|
||||
Write-Host " Server payload: $([math]::Round($payloadSize, 2)) KB" -ForegroundColor Green
|
||||
|
||||
# Clean up
|
||||
if (Test-Path $tempFolder) {
|
||||
Remove-Item -Path $tempFolder -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[2/5] Creating package..." -ForegroundColor Yellow
|
||||
New-Item -ItemType Directory -Path $tempFolder -Force | Out-Null
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[3/5] Copying files..." -ForegroundColor Yellow
|
||||
Copy-Item -Path "$sourceFolder\*" -Destination $tempFolder -Recurse -Force
|
||||
Write-Host " Copied client files" -ForegroundColor Green
|
||||
|
||||
Copy-Item -Path $payloadFile -Destination $tempFolder -Force
|
||||
Write-Host " Copied server payload" -ForegroundColor Green
|
||||
|
||||
# Create detailed changelog
|
||||
$changelogContent = @"
|
||||
# PS5 Upload Suite v1.2 - Complete Bug Fix Release
|
||||
|
||||
## What's Fixed:
|
||||
|
||||
### CLIENT FIXES:
|
||||
|
||||
#### Bug #1: Connection Timeout
|
||||
- **Problem:** Application took 30+ seconds to detect wrong IP address
|
||||
- **Fix:** Added 5-second connection timeout
|
||||
- **Result:** Immediate feedback on connection failure
|
||||
|
||||
#### Bug #2: Client-Side Timeout
|
||||
- **Problem:** Client timeout on folder operations
|
||||
- **Fix:** Increased client timeout to 60 seconds
|
||||
- **Result:** Better handling of server operations
|
||||
|
||||
### SERVER FIXES:
|
||||
|
||||
#### Bug #3: Folder Deletion Timeout (MAJOR FIX!)
|
||||
- **Problem:** Deleting large game folders (10GB+) caused "Read timeout" error
|
||||
- **Fix:** Folder deletion now runs asynchronously in background thread
|
||||
- **How it works:**
|
||||
1. Server responds immediately: "Deleting folder in background..."
|
||||
2. Deletion happens in separate thread
|
||||
3. PS5 notification appears when complete
|
||||
- **Result:** NO MORE TIMEOUT! Works with any folder size!
|
||||
|
||||
## Installation:
|
||||
1. Extract ZIP file
|
||||
2. Run PS5Upload.exe on Windows
|
||||
3. Upload ps5_upload_server.elf to PS5 (via FTP or USB)
|
||||
4. Load ps5_upload_server.elf with etaHEN payload injector
|
||||
5. Connect and enjoy!
|
||||
|
||||
## Upgrading from v1.0/v1.1:
|
||||
Replace both:
|
||||
- PS5Upload.exe (Windows client)
|
||||
- ps5_upload_server.elf (PS5 server)
|
||||
|
||||
IMPORTANT: You MUST update BOTH files for the fixes to work!
|
||||
|
||||
## Testing the Fixes:
|
||||
1. **Connection timeout:** Try wrong IP - should fail in 5 seconds
|
||||
2. **Folder deletion:** Delete large game folder - should work without timeout
|
||||
|
||||
## Requirements:
|
||||
- Windows 10/11 (64-bit)
|
||||
- PS5 with etaHEN 2.0+ or GoldHEN
|
||||
- Network connection
|
||||
|
||||
## Support:
|
||||
GitHub: https://github.com/YOUR_USERNAME/ps5_upload_suite
|
||||
Issues: Report bugs on GitHub Issues page
|
||||
|
||||
---
|
||||
**Version:** 1.2
|
||||
**Release Date:** January 12, 2026
|
||||
**Build:** Complete Bug Fix Release
|
||||
"@
|
||||
|
||||
Set-Content -Path "$tempFolder\CHANGELOG_v1.2.txt" -Value $changelogContent -Encoding UTF8
|
||||
Write-Host " Created CHANGELOG_v1.2.txt" -ForegroundColor Green
|
||||
|
||||
# Create quick start guide
|
||||
$quickStartContent = @"
|
||||
# PS5 Upload Suite v1.2 - Quick Start
|
||||
|
||||
## Step 1: Load Server on PS5
|
||||
1. Copy ps5_upload_server.elf to USB drive
|
||||
2. Insert USB into PS5
|
||||
3. Open etaHEN payload injector
|
||||
4. Load ps5_upload_server.elf
|
||||
5. Note the IP address shown on screen
|
||||
|
||||
## Step 2: Connect from Windows
|
||||
1. Run PS5Upload.exe
|
||||
2. Enter PS5 IP address
|
||||
3. Click Connect
|
||||
4. You're ready!
|
||||
|
||||
## Step 3: Upload Files
|
||||
1. Click "Browse Folder" to select files
|
||||
2. Click "Upload" to transfer
|
||||
3. Files appear in /data/etaHEN/games on PS5
|
||||
|
||||
## Deleting Large Folders:
|
||||
- Now works perfectly with async deletion!
|
||||
- Server responds immediately
|
||||
- Deletion happens in background
|
||||
- PS5 notification when complete
|
||||
|
||||
Enjoy! 🎮
|
||||
"@
|
||||
|
||||
Set-Content -Path "$tempFolder\QUICK_START.txt" -Value $quickStartContent -Encoding UTF8
|
||||
Write-Host " Created QUICK_START.txt" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[4/5] Creating ZIP..." -ForegroundColor Yellow
|
||||
|
||||
if (Test-Path $outputZip) {
|
||||
Remove-Item -Path $outputZip -Force
|
||||
}
|
||||
|
||||
Compress-Archive -Path "$tempFolder\*" -DestinationPath $outputZip -CompressionLevel Optimal -Force
|
||||
|
||||
if (Test-Path $outputZip) {
|
||||
$zipSize = (Get-Item $outputZip).Length / 1MB
|
||||
Write-Host " Created: $outputZip" -ForegroundColor Green
|
||||
Write-Host " Size: $([math]::Round($zipSize, 2)) MB" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[ERROR] Failed to create ZIP!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[5/5] Cleaning up..." -ForegroundColor Yellow
|
||||
Remove-Item -Path $tempFolder -Recurse -Force
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "SUCCESS!" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Release: $outputZip" -ForegroundColor Green
|
||||
Write-Host "Size: $([math]::Round($zipSize, 2)) MB" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Bug Fixes Applied:" -ForegroundColor Yellow
|
||||
Write-Host " [CLIENT] Connection timeout: 5 seconds" -ForegroundColor Green
|
||||
Write-Host " [CLIENT] Folder deletion timeout: 60 seconds" -ForegroundColor Green
|
||||
Write-Host " [SERVER] Async folder deletion (NO MORE TIMEOUT!)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "IMPORTANT: Update BOTH client and server!" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:" -ForegroundColor Cyan
|
||||
Write-Host "1. Test locally" -ForegroundColor White
|
||||
Write-Host "2. Upload ps5_upload_server.elf to PS5" -ForegroundColor White
|
||||
Write-Host "3. Load with etaHEN" -ForegroundColor White
|
||||
Write-Host "4. Test folder deletion with large folder" -ForegroundColor White
|
||||
Write-Host "5. Upload to GitHub as v1.2" -ForegroundColor White
|
||||
Write-Host ""
|
||||
@@ -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
+5
-1
@@ -8,7 +8,11 @@ echo ""
|
||||
|
||||
echo "[+] Compiling..."
|
||||
rm -f ps5_upload_server.elf
|
||||
/home/hackman/ps5sdk_copy/bin/prospero-clang -Wall -O3 -pthread -o ps5_upload_server.elf main.c
|
||||
/opt/ps5-payload-sdk/bin/prospero-clang -Wall -O3 -pthread \
|
||||
-I/opt/ps5-payload-sdk/target/include_bsd \
|
||||
-I/opt/ps5-payload-sdk/target/include \
|
||||
-L/opt/ps5-payload-sdk/target/lib \
|
||||
-o ps5_upload_server.elf main.c
|
||||
|
||||
if [ -f "ps5_upload_server.elf" ]; then
|
||||
echo ""
|
||||
|
||||
+2093
-140
File diff suppressed because it is too large.
Load diff
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.
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
@@ -1,195 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GitHub Release Uploader for PS5 Upload Suite
|
||||
Uploads v1.2 release to GitHub using GitHub API
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# GitHub Configuration
|
||||
GITHUB_TOKEN = "YOUR_GITHUB_TOKEN_HERE" # Replace with your token
|
||||
GITHUB_REPO = "manos555555/PS5-Upload-Suite"
|
||||
RELEASE_TAG = "v1.2"
|
||||
RELEASE_NAME = "PS5 Upload Suite v1.2 - Complete Bug Fix"
|
||||
RELEASE_BODY = """# PS5 Upload Suite v1.2 - Complete Bug Fix Release
|
||||
|
||||
## 🐛 What's Fixed:
|
||||
|
||||
### CLIENT FIXES:
|
||||
- **Bug #1:** Connection timeout reduced to 5 seconds (fast fail on wrong IP)
|
||||
- **Bug #2:** Folder deletion timeout increased to 60 seconds
|
||||
|
||||
### SERVER FIXES:
|
||||
- **Bug #3:** Async folder deletion - NO MORE TIMEOUT on large folders! ✅
|
||||
- Server responds immediately
|
||||
- Deletion happens in background thread
|
||||
- PS5 notification when complete
|
||||
|
||||
## 📦 What's Included:
|
||||
- `PS5Upload.exe` - Windows client with bug fixes
|
||||
- `ps5_upload_server.elf` - PS5 server with async deletion
|
||||
- All dependencies included
|
||||
- Self-contained (no .NET installation required)
|
||||
|
||||
## 🚀 Installation:
|
||||
1. Extract ZIP file
|
||||
2. Run `PS5Upload.exe` on Windows
|
||||
3. Upload `ps5_upload_server.elf` to PS5 via FTP
|
||||
4. Load with etaHEN payload injector
|
||||
5. Connect and enjoy!
|
||||
|
||||
## ⚠️ IMPORTANT:
|
||||
**You MUST update BOTH files:**
|
||||
- Windows client: `PS5Upload.exe`
|
||||
- PS5 server: `ps5_upload_server.elf`
|
||||
|
||||
## 💡 Testing:
|
||||
- **Connection timeout:** Try wrong IP - fails in 5 seconds
|
||||
- **Folder deletion:** Delete large game folder - works without timeout!
|
||||
|
||||
## 📋 Requirements:
|
||||
- Windows 10/11 (64-bit)
|
||||
- PS5 with etaHEN 2.0+ or GoldHEN
|
||||
- Network connection
|
||||
|
||||
## 🔗 Links:
|
||||
- [Report Issues](https://github.com/manos555555/PS5-Upload-Suite/issues)
|
||||
- [Documentation](https://github.com/manos555555/PS5-Upload-Suite/blob/main/README.md)
|
||||
|
||||
---
|
||||
**Version:** 1.2
|
||||
**Release Date:** January 12, 2026
|
||||
**Build:** Complete Bug Fix Release
|
||||
"""
|
||||
|
||||
def create_github_release(token, repo, tag, name, body):
|
||||
"""Create a GitHub release"""
|
||||
|
||||
url = f"https://api.github.com/repos/{repo}/releases"
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": "application/vnd.github.v3+json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"tag_name": tag,
|
||||
"name": name,
|
||||
"body": body,
|
||||
"draft": False,
|
||||
"prerelease": False
|
||||
}
|
||||
|
||||
print(f"Creating release {tag}...")
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
|
||||
if response.status_code == 201:
|
||||
print(f"✅ Release created successfully!")
|
||||
return response.json()
|
||||
else:
|
||||
print(f"❌ Failed to create release: {response.status_code}")
|
||||
print(f"Response: {response.text}")
|
||||
return None
|
||||
|
||||
def upload_release_asset(token, upload_url, file_path):
|
||||
"""Upload a file to GitHub release"""
|
||||
|
||||
# Remove the {?name,label} template from upload_url
|
||||
upload_url = upload_url.split("{")[0]
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
file_size = os.path.getsize(file_path) / (1024 * 1024) # MB
|
||||
|
||||
print(f"\nUploading {file_name} ({file_size:.2f} MB)...")
|
||||
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Content-Type": "application/zip"
|
||||
}
|
||||
|
||||
params = {"name": file_name}
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
response = requests.post(
|
||||
upload_url,
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=f
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
print(f"✅ {file_name} uploaded successfully!")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Failed to upload {file_name}: {response.status_code}")
|
||||
print(f"Response: {response.text}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("PS5 Upload Suite - GitHub Release Uploader")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# Check token
|
||||
if GITHUB_TOKEN == "YOUR_GITHUB_TOKEN_HERE":
|
||||
print("❌ ERROR: Please set your GitHub token in the script!")
|
||||
print()
|
||||
print("To get a token:")
|
||||
print("1. Go to: https://github.com/settings/tokens")
|
||||
print("2. Generate new token (classic)")
|
||||
print("3. Select 'repo' scope")
|
||||
print("4. Copy token and paste in this script")
|
||||
return 1
|
||||
|
||||
# Check if release file exists
|
||||
release_file = Path("PS5Upload-Windows-x64-v1.2-Complete.zip")
|
||||
if not release_file.exists():
|
||||
print(f"❌ ERROR: Release file not found: {release_file}")
|
||||
return 1
|
||||
|
||||
print(f"📦 Release file: {release_file}")
|
||||
print(f"📊 Size: {release_file.stat().st_size / (1024*1024):.2f} MB")
|
||||
print()
|
||||
|
||||
# Create release
|
||||
release_data = create_github_release(
|
||||
GITHUB_TOKEN,
|
||||
GITHUB_REPO,
|
||||
RELEASE_TAG,
|
||||
RELEASE_NAME,
|
||||
RELEASE_BODY
|
||||
)
|
||||
|
||||
if not release_data:
|
||||
return 1
|
||||
|
||||
# Upload asset
|
||||
upload_url = release_data.get("upload_url")
|
||||
if not upload_url:
|
||||
print("❌ ERROR: No upload URL in release response")
|
||||
return 1
|
||||
|
||||
success = upload_release_asset(
|
||||
GITHUB_TOKEN,
|
||||
upload_url,
|
||||
str(release_file)
|
||||
)
|
||||
|
||||
if success:
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("✅ SUCCESS! Release published to GitHub")
|
||||
print("=" * 70)
|
||||
print()
|
||||
print(f"🔗 Release URL: {release_data.get('html_url')}")
|
||||
print()
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user