19 Commits
Author SHA1 Message Date
Manos a837ea9fe1 v4.1.0 - Memory optimization, Buy Me a Coffee button, and updated mobile Protocol.cs 2026-02-05 02:26:13 +02:00
Manos ac7cea2e4b Add Shell Terminal to NEW in v3.3.0 section 2026-01-26 01:12:10 +02:00
Manos abaa028d28 Update documentation for v3.3.0 - Smart Search & Shell Terminal 2026-01-26 01:09:29 +02:00
Manos 45afca892c Update CHANGELOG.md with versions 2.0.0 through 3.2.0 2026-01-24 01:38:00 +02:00
Manos f065b79b33 Update mobile screenshot 2026-01-24 01:29:29 +02:00
Manos 4403d86be5 Update README for v3.2.0 - Storage display fix, mobile improvements, bug fixes 2026-01-24 01:24:50 +02:00
Manos af252efc9a Clean up README - remove source code references and building instructions 2026-01-20 04:11:59 +02:00
Manos 2da27aed13 Remove old release files and zips 2026-01-20 04:05:11 +02:00
Manos 264ad6210d Remove source code - keep only compiled binaries in releases 2026-01-20 04:01:47 +02:00
Manos db48bc6fb2 Update README.md for v3.1.0 - parallel chunked uploads, real-time progress 2026-01-20 04:00:11 +02:00
Manos 4ac43ebb8b v3.1.0: Parallel chunked uploads (104 MB/s), real-time speed/ETA, storage display fixes 2026-01-20 03:54:44 +02:00
Manos 20caced5f4 Keep only screenshot3 and mobile screenshot in README 2026-01-19 17:24:25 +02:00
Manos 285279788f Add mobile app screenshot to README 2026-01-19 17:22:41 +02:00
Manos 70d174cecc v3.0.0 - Rename release to version 3.0.0 2026-01-19 17:14:53 +02:00
manos555555 f128c6a6b4 Delete .gitignore 2026-01-19 17:00:00 +02:00
Manos c211c086ac v2.2.0 - Critical path fix, Android Mobile App with Multi-PS5 Profiles 2026-01-19 16:58:51 +02:00
manos555555 4ca30876c7 Update README.md 2026-01-19 01:26:59 +02:00
manos555555 5ae3c1709e Delete PROTOCOL.md 2026-01-19 01:25:27 +02:00
Manos 3374e87bfe Update to v2.1.0: Add screenshot, update README, remove old release files 2026-01-19 01:23:51 +02:00
37 changed files with 4558 additions and 2671 deletions

No files matched your search

-60
View File
@@ -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
+296
View File
@@ -1,5 +1,301 @@
# Changelog - PS5 Upload Suite
## Version 4.1.0 - Memory Optimization & Large File Handling (February 5, 2026)
### 🧠 Memory & Performance Improvements
#### 1. Large File Count Support (156K+ Files)
- **Fixed crash** with games containing 156,253+ files (e.g., Astrobot)
- **Removed massive dictionary** - No longer creates 156K FileInfo objects at once
- **Incremental calculation** - Total bytes calculated progressively during scan
- **Memory efficient** - Handles unlimited file counts without OutOfMemoryException
#### 2. Lock-Free Progress Tracking
- **ConcurrentDictionary** - Replaced Dictionary with thread-safe collections
- **Zero lock contention** - Eliminated `lock (_progressLock)` blocks
- **Atomic operations** - Uses `Interlocked.Add()` for progress updates
- **Smooth UI** - No stuttering from lock contention
#### 3. Auto Memory Cleanup
- **Completed files removed** - Dictionaries cleaned after file completion
- **Reduced memory footprint** - Only active uploads tracked
- **Prevents memory overflow** - Long-running uploads stay stable
#### 4. Reduced UI Stuttering
- **Batch logging** - Small files logged every 50 completions (up from 25)
- **Less UI pressure** - Fewer `Dispatcher.Invoke()` calls
- **Smoother experience** - Especially noticeable with 100K+ files
### 🔧 Technical Changes
- Changed `_fileProgressBytes` from Dictionary to ConcurrentDictionary
- Changed `_fileChunkProgressBytes` from Dictionary to ConcurrentDictionary
- Changed `_chunkLogLastBytes` from Dictionary to ConcurrentDictionary
- Removed `fileSizeLookup` dictionary creation (caused OutOfMemoryException)
- Increased `SmallFileLogBatchSize` from 25 to 50
- Added dictionary cleanup in file completion handlers
### 🐛 Bug Fixes
- Fixed application crash when uploading games with 156K+ files
- Fixed UI stuttering during small file uploads
- Fixed memory overflow from massive dictionary allocations
- Fixed lock contention causing UI freezes
### 📊 Tested With
- **Astrobot**: 156,253 files, 1,724 directories
- **Result**: Zero crashes, smooth upload, stable memory usage
---
## Version 4.0.0 - Retry Feature & Stability Improvements (January 27, 2026)
### ✨ New Features
#### 🔄 Retry Failed Transfers
- **Right-click context menu** on failed transfers
- **Retry Upload** - Re-queue failed files for upload
- **Remove from List** - Clear individual failed items
- **Persistent paths** - Failed transfers store local and remote paths
- **Smart retry** - Validates file existence before retry
#### 🗑️ Clear All Button Fix
- **Transfer History Clear All** now works correctly
- **Confirmation dialog** before clearing history
- **Clears both** completed and failed transfers
### 🔧 Performance & Stability
#### Connection Stability
- **Reverted 16MB buffer** - Caused connection drops with 16 parallel connections
- **Stable 8MB buffer** - Proven reliable for high-speed transfers
- **TCP NoDelay** - Already enabled for optimal performance
- **16MB socket buffers** - Maintained for maximum throughput
#### Upload Performance
- **Large files (>100MB)**: 30-36 MB/s per file
- **Small files (<100MB)**: 8-12 MB/s per file
- **4 parallel large files** - Prevents PS5 memory exhaustion
- **16 total connections** - Optimal for mixed workloads
### 🐛 Bug Fixes
- Fixed Clear All button not clearing transfer history
- Fixed connection drops when using oversized buffers
- Added proper event handlers for failed transfer context menu
- Improved error handling for retry operations
### 📝 Code Quality
- Added comprehensive inline documentation
- Improved code organization and readability
- Better error messages for failed operations
- Enhanced UI feedback for user actions
---
## Version 3.3.0 - Smart Search & Enhanced Mobile (January 26, 2026)
### 🔍 Smart Search Feature
- **Full Filesystem Indexing** - Index entire PS5 filesystem (200,000+ files)
- **Instant Search** - Lightning-fast search with wildcards (`*.pkg`, `*loader*`)
- **Size Filters** - Search by file size (`size:>1GB`, `size:<100MB`)
- **Case-Insensitive** - Matches both filename and full path
- **Navigate from Search** - Double-click results to jump to folder (Desktop) / Tap to navigate (Mobile)
- **Index Status** - Real-time indexing progress with file/directory counts
- **Smart Indexing** - Skips problematic directories (`/dev`, `/proc`, `/sys`)
### 📱 Mobile App v1.2
- **🔍 Search Tab** - Full Smart Search functionality on Android
- **Navigate to Location** - Tap search results to navigate to folder
- **Copy Path** - Copy file paths to clipboard from search results
- **Connection Manager** - Singleton pattern for reliable IP/port management
- **Improved Navigation** - Fixed MainPage reference for seamless navigation
### 🖥️ Desktop Client Updates
- **Search Tab** - New dedicated tab for Smart Search
- **💻 Shell Terminal** - Execute commands directly on PS5
- Run system commands remotely
- Real-time output display
- Command history
- Working directory support
- **Equal Panel Layout** - Local Files, PS5 Files, and Right panel now equal width
- **Improved UI** - Index Status and buttons positioned above search box
- **Debounced Search** - Smooth typing experience with 300ms debounce
### 🔧 Payload Updates
- **Indexing Commands** - `CMD_INDEX_START` (0x40), `CMD_INDEX_STATUS` (0x41), `CMD_SEARCH_INDEX` (0x42)
- **In-Memory Index** - Linked list structure for fast search
- **Wildcard Matching** - Support for `*` and `?` wildcards
- **Thread-Safe** - Mutex-protected index operations
- **Root Path Support** - Can index from `/` (entire filesystem)
### 🐛 Bug Fixes
- Fixed indexing hangs when scanning root directory
- Fixed search protocol compatibility between mobile and desktop
- Fixed navigation stack issues in mobile app
- Fixed nullable reference warnings in mobile code
- Added proper error handling for index operations
---
## Version 3.2.1 - Privacy & Bug Fixes (January 24, 2026)
### 🔒 Privacy Improvements
- **Removed disk space display** - No longer shows PS5 storage capacity (Free/Used/Total)
- **Fixed storage query bug** - Resolved syntax errors in statvfs code that could cause crashes
- **Privacy-focused** - Application no longer queries or displays sensitive storage information
### 🐛 Bug Fixes
- Fixed potential crash from malformed storage query code
- Removed unused protocol busy flag that caused compiler warnings
- Cleaned up all storage-related UI elements and backend code
---
## Version 3.2.0 - Mobile Improvements & Bug Fixes (January 24, 2026)
### 🎯 Improvements
#### 💾 Accurate Storage Display
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
- **Accounts for reserved space** - Same calculation as PS5 system
- Uses proper calculation: `(total_blocks - reserved) * block_size`
#### 📱 Mobile App Improvements
- **Multi-select mode** - Toggle button (☑️/✅) to select multiple files/folders
- **Folder browser** - Visual folder picker for Copy/Move destinations with recursive navigation
- **Sorted file list** - Folders always displayed at top, then files (alphabetically)
- **Exit confirmation** - Prompt dialog when pressing back button
- **Delete folders** - Now supports folder deletion (not just files)
- **Batch operations** - Delete/download multiple items at once
#### 3. 🐛 Bug Fixes
- **Fixed empty folder deletion** - No more "Unexpected response" error when deleting empty directories
- **Fixed Copy/Move crash** - No longer crashes when selecting destination folder (saved file info before browsing)
- **Fixed multi-select count** - Correct item count in delete confirmation dialog
- **Fixed folder navigation** - Can enter folders after actions complete (auto-exit multi-select mode)
---
## Version 3.1.0 - Parallel Chunked Uploads (January 2026)
### 🚀 Performance Improvements
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
- **Large files (>100MB)** are now split into 500MB chunks
- **4 parallel connections** upload chunks simultaneously
- **Result:** ~104 MB/s upload speed (2x faster than before!)
- **Fixed race condition:** First chunk creates file before others start
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
#### 2. 📊 Real-time Progress Display
- **Speed display** now updates in real-time during chunked uploads
- **ETA display** shows accurate time remaining
- **Progress bar** correctly shows 0-100% for chunked files
- No more speed dropping to 0 during large file uploads
#### 3. 💾 Storage Display Improvements
- **Changed label** to "Data Storage (/data)" for clarity
- **Uses statfs with f_bavail** for more accurate available space
- **Real-time updates** every 5 seconds when connected
---
## Version 3.0.0 - Mobile Client & Path Fixes (January 2026)
### 🐛 Critical Bug Fixes
#### 1. 🔧 Path Normalization Fix
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
- **Solution:** Added `NormalizePath()` function in both client and server
- **Result:** 100% reliable directory creation and file uploads
#### 2. 📱 Android Mobile Client
- **NEW:** Full-featured Android app for PS5 file management
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
- Upload files from phone to PS5
- Download files from PS5 with Share option
- Browse PS5 filesystem
- Create new folders
- Rename files/folders
- Copy files/folders
- Move files/folders
- Delete files
- Favorites for quick navigation
- Debug Log with Copy to clipboard
- Transfer History tracking
#### 3. 🛠️ Server Improvements
- Path normalization in all handler functions
- More robust error handling
- Improved stability for parallel uploads
---
## Version 2.1.0 - Performance & History (January 2026)
### 🚀 Performance Optimizations
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
- **Per-file:** 11-14 MB/s sustained per connection
#### 2. 📊 Transfer History
- Complete history of all uploads and downloads
- Success/Failed status tracking with error messages
- Speed statistics (average, min, max)
- Export to CSV/JSON for analysis
- Persistent storage across sessions
#### 3. 🔄 Auto-Clear History on Startup
- Optional checkbox to clear history automatically
- Useful for keeping UI clean between sessions
- Setting saved in `ps5_upload_settings.json`
#### 4. 🖥️ Maximized Window UI
- Application opens in full-screen mode by default
- Better visibility for large file transfers
- Can be resized/restored as needed
### Technical Improvements
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
---
## Version 2.0.0 - Download & Multi-PS5 (January 2026)
### 🎉 4 Major New Features
#### 1. 📥 Download Files (PS5 → PC)
- Right-click any file → "⬇️ Download to PC"
- Save file dialog for destination selection
- Real-time progress tracking with speed display
- Optimized with sendfile for maximum speed
#### 2. 🔍 File Search
- Search box in PS5 Files panel
- Real-time filtering as you type
- Case-insensitive search
- Quick "Clear" button to reset
#### 3. ⭐ Favorites/Bookmarks
- Save frequently used PS5 paths
- Quick dropdown navigation
- Add/Remove favorite paths
- Persistent storage in JSON
#### 4. 🎮 Multi-PS5 Support
- Save multiple PS5 profiles (IP + name)
- Quick switch between different PS5 consoles
- Dropdown profile selector
- Persistent profile storage
---
## Version 1.3.0 - Stable Release (January 13, 2026)
### 🎯 Major Achievement
-99
View File
@@ -1,99 +0,0 @@
# PS5 Upload Suite v2.0.0 - Feature-Rich Release
## 🎉 What's New - 4 Major Features Added!
### 1. 📥 Download Files (PS5 → PC)
- Right-click any file → "⬇️ Download to PC"
- Save file dialog for destination
- Real-time progress with speed tracking
- Optimized with sendfile for maximum speed
### 2. 🔍 File Search
- Search box in PS5 Files panel
- Real-time filtering as you type
- Case-insensitive search
- Quick "Clear" button
### 3. ⭐ Favorites/Bookmarks
- Save frequently used PS5 paths
- Quick dropdown navigation
- Add/Remove favorite paths
- Persistent JSON storage
### 4. 🎮 Multi-PS5 Support
- Save multiple PS5 profiles (IP + name)
- Quick switch between consoles
- Dropdown profile selector
- Persistent profile storage
---
## 📦 Installation
### Server (PS5)
1. Copy `ps5_upload_server.elf` to `/data/etaHEN/payloads/`
2. Load with elfldr
3. Server starts on port 9113
### Client (Windows)
1. Download `PS5Upload.exe`
2. Run on Windows 10/11
3. Enter PS5 IP and connect
---
## 🚀 Performance
- **Upload Speed:** 100-120 MB/s (Gigabit Ethernet)
- **Download Speed:** Same high-speed protocol
- **Stability:** Zero connection drops (from v1.3.0)
- **Tested:** 42,801+ files uploaded successfully
---
## 🔧 Technical Details
### New Protocol Command
- `CMD_DOWNLOAD_FILE` (0x13) - Download files from PS5
### Server Changes
- Added sendfile-based download handler
- Fixed LIST_DIR to return actual file sizes
- Zero-copy file transfer
### Client Changes
- 4 new UI panels (Search, Favorites, Profiles, Download)
- JSON-based persistence (ps5_profiles.json, ps5_favorites.json)
- Custom dark theme for all ComboBox dropdowns
- ~500+ lines of new code
---
## ⬆️ Upgrading from v1.3.0
1. Replace server: Copy new `ps5_upload_server.elf` to PS5
2. Replace client: Download new `PS5Upload.exe`
3. Your settings will be preserved
**New files created:**
- `ps5_profiles.json` - Saved PS5 profiles
- `ps5_favorites.json` - Saved favorite paths
---
## 📋 Files in This Release
- `ps5_upload_server.elf` (129 KB) - PS5 server payload
- `PS5Upload.exe` (8.2 MB) - Windows client (self-contained)
---
## 🙏 Credits
**By Manos**
Special thanks to the PS5 homebrew community!
---
**Enjoy the new features!** 🚀
-83
View File
@@ -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.
+184 -100
View File
@@ -2,41 +2,50 @@
**By Manos**
**Version 2.0.0 - Feature-Rich Release**
**Version 4.1.0 - Memory Optimization & Large File Handling**
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 v2.0.0:** 4 Major Features Added - Download Files, File Search, Favorites/Bookmarks, Multi-PS5 Support!
⭐ **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
![PS5 Upload Client](screenshots/screenshot1.png)
![Upload in Progress](screenshots/screenshot2.png)
📱 **Android Mobile Client available!**
![Transfer History & Multi-PS5 Support](screenshots/screenshot3.png)
![Mobile App](screenshots/mobile_screenshot.jpg)
---
## 📦 What's Included
## 📦 Downloads
Download the latest release from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page:
### 1. PS5 Server Payload
- **File:** `payload/ps5_upload_server.elf`
- **File:** `ps5_upload_server.elf`
- **Port:** 9113
- **Protocol:** Custom binary (optimized for speed)
- **Features:**
- 4MB socket buffers
- SO_NOSIGPIPE enabled
- TCP_NODELAY for low latency
- Direct disk writes (no temp files)
- Multi-threaded client handling
- High-speed custom binary protocol
- 16MB socket buffers for maximum throughput
- Multi-threaded client handling
### 2. Windows GUI Client
- **File:** `client/bin/Release/net6.0-windows/win-x64/publish/PS5Upload.exe`
- **Framework:** .NET 6.0 WPF
- **Features:**
- Modern dark theme UI
- Drag & drop file/folder upload
- Browse PS5 filesystem
- Real-time upload progress
- Speed indicator (MB/s)
- Storage space display
- Recursive folder upload
- **File:** `PS5Upload.exe`
- Self-contained executable (no .NET installation required)
- Modern dark theme UI
- Drag & drop file/folder upload
- Real-time upload progress with speed tracking
- Transfer History with success/failed tracking
- Privacy-focused (no storage monitoring)
### 3. 📱 Android Mobile Client
- **File:** `PS5UploadMobile.apk`
- Upload/download files from your phone
- Multi-PS5 Profiles support
- Browse PS5 filesystem
- Favorites for quick navigation
---
@@ -44,17 +53,13 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
### Step 1: Load PS5 Payload
1. Copy `payload/ps5_upload_server.elf` to your PS5:
```
/data/etaHEN/payloads/ps5_upload_server.elf
```
1. Download `ps5_upload_server.elf` from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page
2. Load the payload with elfldr
2. Copy it to your PS5: `/data/etaHEN/payloads/ps5_upload_server.elf`
3. You should see notification:
```
PS5 Upload Server: 192.168.0.XXX:9113 - By Manos
```
3. Load the payload with elfldr
4. You should see notification: `PS5 Upload Server: 192.168.0.XXX:9113 - By Manos`
### Step 2: Run Windows Client
@@ -80,86 +85,54 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
✅ **Robust Error Handling** - Graceful failures
### Windows Client
✅ **Modern UI** - Dark theme, clean design
✅ **Modern UI** - Dark theme, maximized window for better visibility
✅ **Drag & Drop** - Files and folders
✅ **Browse PS5** - Navigate filesystem
✅ **Real-time Progress** - Speed & percentage
✅ **Real-time Progress** - Speed & percentage tracking
✅ **Optimized Upload** - 8 parallel connections for 88-110 MB/s aggregate speed
✅ **Transfer History** - Track all uploads/downloads with success/failed status
✅ **Auto-Clear History** - Optional auto-clear on startup
✅ **Folder Upload** - Recursive directory upload
✅ **Storage Info** - Free space display
✅ **Download Files** - Download from PS5 to PC (NEW v2.0)
✅ **File Search** - Real-time filtering of PS5 files (NEW v2.0)
✅ **Favorites/Bookmarks** - Quick navigation to saved paths (NEW v2.0)
✅ **Multi-PS5 Support** - Save and switch between multiple PS5 profiles (NEW v2.0)
✅ **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
### 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 |
### Previous Results (v1.3.0):
- ✅ **42,801 files** uploaded successfully
- ✅ **60-150 MB/s** sustained throughput
**Note:** Speeds depend on network quality and PS5 disk performance.
| 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.
## 🔧 Building from Source
### 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
- `DOWNLOAD_FILE` - Download file from PS5 (NEW v2.0)
- `SHUTDOWN` - Shutdown server
---
@@ -191,6 +164,117 @@ See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
---
## 📝 What's New in v3.2.0
### 🎯 Improvements:
#### 1. 💾 Accurate Storage Display
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
- **Accounts for reserved space** - Same calculation as PS5 system
#### 2. 📱 Mobile App Improvements
- **Multi-select mode** - Toggle button to select multiple files/folders
- **Folder browser** - Visual folder picker for Copy/Move destinations
- **Sorted file list** - Folders always displayed at top
- **Exit confirmation** - Prompt when pressing back button
- **Delete folders** - Now supports folder deletion (not just files)
- **Batch operations** - Delete/download multiple items at once
#### 3. 🐛 Bug Fixes
- **Fixed empty folder deletion** - No more "Unexpected response" error
- **Fixed Copy/Move crash** - No longer crashes when selecting destination
- **Fixed multi-select count** - Correct item count in delete confirmation
- **Fixed folder navigation** - Can enter folders after actions complete
---
## 📝 What's New in v3.1.0
### 🚀 Performance Improvements:
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
- **Large files (>100MB)** are now split into 500MB chunks
- **4 parallel connections** upload chunks simultaneously
- **Result:** ~104 MB/s upload speed (2x faster than before!)
- **Fixed race condition:** First chunk creates file before others start
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
#### 2. 📊 Real-time Progress Display
- **Speed display** now updates in real-time during chunked uploads
- **ETA display** shows accurate time remaining
- **Progress bar** correctly shows 0-100% for chunked files
- No more speed dropping to 0 during large file uploads
#### 3. 💾 Storage Display Improvements
- **Changed label** to "Data Storage (/data)" for clarity
- **Uses statfs with f_bavail** for more accurate available space
- **Real-time updates** every 5 seconds when connected
---
## 📝 What's New in v3.0.0
### 🐛 Critical Bug Fixes:
#### 1. 🔧 Path Normalization Fix
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
- **Solution:** Added `NormalizePath()` function in both client and server
- **Result:** 100% reliable directory creation and file uploads
#### 2. 📱 Android Mobile Client
- **NEW:** Full-featured Android app for PS5 file management
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
- Upload files from phone to PS5
- Download files from PS5 with Share option
- Browse PS5 filesystem
- Favorites for quick navigation
- Debug Log with Copy to clipboard
- Transfer History tracking
#### 3. 🛠️ Server Improvements
- Path normalization in all handler functions
- More robust error handling
- Improved stability for parallel uploads
---
## 📝 What's New in v2.1.0
### 🚀 Performance Optimizations:
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
- **Per-file:** 11-14 MB/s sustained per connection
#### 2. 📊 Transfer History
- Complete history of all uploads and downloads
- Success/Failed status tracking with error messages
- Speed statistics (average, min, max)
- Export to CSV/JSON for analysis
- Persistent storage across sessions
#### 3. 🔄 Auto-Clear History on Startup
- Optional checkbox to clear history automatically
- Useful for keeping UI clean between sessions
- Setting saved in `ps5_upload_settings.json`
#### 4. 🖥️ Maximized Window UI
- Application opens in full-screen mode by default
- Better visibility for large file transfers
- Can be resized/restored as needed
### Technical Improvements:
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
---
## 📝 What's New in v2.0.0
### 🎉 4 Major New Features:
@@ -229,7 +313,7 @@ See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
## 📝 License
MIT License - Free to use and modify
This software is provided as-is for personal use.
---
-166
View File
@@ -1,166 +0,0 @@
# 🎉 PS5 Upload Suite v2.0.0 - Feature-Rich Release
## 🚀 What's New
This is a **major feature release** adding 4 powerful new capabilities to the PS5 Upload Suite!
---
## ✨ New Features
### 1. 📥 Download Files (PS5 → PC)
Finally! You can now download files from your PS5 back to your PC.
**How to use:**
- Right-click any file in PS5 Files panel
- Select "⬇️ Download to PC"
- Choose where to save the file
- Watch the progress with real-time speed tracking
**Technical Details:**
- Uses sendfile optimization for maximum speed
- Zero-copy transfer on PS5 side
- Same high-speed protocol as uploads
- Full progress tracking
---
### 2. 🔍 File Search
Quickly find files in large directories without scrolling.
**How to use:**
- Type in the Search box below Current Path
- Files are filtered in real-time as you type
- Case-insensitive search
- Click "✖ Clear" to reset
**Perfect for:**
- Finding specific game saves
- Locating config files
- Searching through large mod folders
---
### 3. ⭐ Favorites/Bookmarks
Save your frequently used PS5 paths for instant navigation.
**How to use:**
- Navigate to any path (e.g., `/data/games`)
- Click "⭐ Add" to save it
- Use the Favorites dropdown to quickly jump to saved paths
- Click "🗑️ Remove" to delete a favorite
**Saved paths persist between sessions!**
---
### 4. 🎮 Multi-PS5 Support
Manage multiple PS5 consoles with ease.
**How to use:**
- Enter a PS5 IP address
- Click "💾 Save Profile" and give it a name (e.g., "Living Room PS5")
- Use the Profile dropdown to quickly switch between PS5s
- Click "🗑️ Delete Profile" to remove saved profiles
**Perfect for:**
- Users with multiple PS5 consoles
- Testing on different systems
- Switching between home and friend's PS5
---
## 🔧 Technical Improvements
### Server-Side (payload)
- Added `CMD_DOWNLOAD_FILE` (0x13) command
- Implemented sendfile-based download handler
- Fixed `LIST_DIR` to return actual file sizes (not 0)
- Zero-copy file transfer for downloads
### Client-Side
- Added `DownloadFileAsync()` method to Protocol
- Implemented search filtering with `ObservableCollection`
- Added JSON-based profile and favorites persistence
- Custom ComboBox templates for dark theme consistency
- Improved UI responsiveness
---
## 📊 Statistics
**Lines of Code Added:** ~500+ lines
**New Protocol Commands:** 1 (DOWNLOAD_FILE)
**New UI Elements:** 3 panels (Search, Favorites, Profiles)
**Persistent Storage Files:** 2 (ps5_profiles.json, ps5_favorites.json)
---
## 🎨 UI Improvements
- All ComboBox dropdowns now have proper dark theme
- All buttons show full text without cutoff
- Consistent styling across all new features
- Responsive layout that adapts to content
---
## 📦 Files Included
### Server
- `payload/ps5_upload_server.elf` - Updated with download support
### Client
- `client/bin/Release/net6.0-windows/win-x64/PS5Upload.exe` - Full v2.0 client
---
## ⬆️ Upgrading from v1.3.0
1. **Replace server payload:**
- Copy new `ps5_upload_server.elf` to `/data/etaHEN/payloads/`
- Reload with elfldr
2. **Replace client:**
- Close old PS5Upload.exe
- Replace with new version
- Your settings will be preserved
**Note:** v2.0 creates two new files:
- `ps5_profiles.json` - Saved PS5 profiles
- `ps5_favorites.json` - Saved favorite paths
---
## 🐛 Known Issues
None reported yet! This is a stable release built on top of the rock-solid v1.3.0 foundation.
---
## 🔮 Future Features (Not in v2.0)
Still on the roadmap:
- Disk Space Warning before upload
- Duplicate File Detection
- File Verification (SHA256)
- Resume Interrupted Uploads
- Queue Management (reorder, pause)
- Transfer History
- Scheduled Uploads
---
## 🙏 Thanks
Special thanks to the PS5 homebrew community for testing and feedback!
---
**Enjoy the new features!** 🚀
**- Manos**
-8
View File
@@ -1,8 +0,0 @@
<Application x:Class="PS5Upload.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
-61
View File
@@ -1,61 +0,0 @@
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 */ }
}
}
}
-79
View File
@@ -1,79 +0,0 @@
<Window x:Class="PS5Upload.DuplicateFileDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="File Already Exists"
Height="320"
Width="550"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
Background="#1E1E1E">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="#2D2D30"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#3F3F46"/>
<Setter Property="Padding" Value="15,8"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Cursor" Value="Hand"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3E3E42"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Icon and Message -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,15">
<TextBlock Text="⚠️" FontSize="32" Margin="0,0,15,0"/>
<TextBlock x:Name="MessageTextBlock"
Text="This file already exists on PS5:"
Foreground="White"
FontSize="14"
VerticalAlignment="Center"/>
</StackPanel>
<!-- File Name -->
<TextBlock Grid.Row="1"
x:Name="FileNameTextBlock"
Foreground="#4EC9B0"
FontSize="13"
FontWeight="Bold"
Margin="0,0,0,10"
TextWrapping="Wrap"/>
<!-- File Info -->
<StackPanel Grid.Row="2" Margin="0,0,0,20">
<TextBlock x:Name="LocalSizeTextBlock"
Foreground="#9CDCFE"
FontSize="12"
Margin="0,0,0,5"/>
<TextBlock x:Name="RemoteSizeTextBlock"
Foreground="#9CDCFE"
FontSize="12"/>
</StackPanel>
<!-- Buttons -->
<StackPanel Grid.Row="4" Orientation="Vertical">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,10">
<Button Content="Replace" Width="120" Click="Replace_Click"/>
<Button Content="Skip" Width="120" Click="Skip_Click"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Replace All" Width="120" Click="ReplaceAll_Click"/>
<Button Content="Skip All" Width="120" Click="SkipAll_Click"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
-67
View File
@@ -1,67 +0,0 @@
using System.Windows;
namespace PS5Upload
{
public partial class DuplicateFileDialog : Window
{
public enum FileAction
{
Replace,
Skip,
ReplaceAll,
SkipAll
}
public FileAction UserAction { get; private set; }
public DuplicateFileDialog(string fileName, long localSize, long remoteSize)
{
InitializeComponent();
FileNameTextBlock.Text = fileName;
LocalSizeTextBlock.Text = $"Local file: {FormatFileSize(localSize)}";
RemoteSizeTextBlock.Text = $"Remote file: {FormatFileSize(remoteSize)}";
}
private void Replace_Click(object sender, RoutedEventArgs e)
{
UserAction = FileAction.Replace;
DialogResult = true;
Close();
}
private void Skip_Click(object sender, RoutedEventArgs e)
{
UserAction = FileAction.Skip;
DialogResult = true;
Close();
}
private void ReplaceAll_Click(object sender, RoutedEventArgs e)
{
UserAction = FileAction.ReplaceAll;
DialogResult = true;
Close();
}
private void SkipAll_Click(object sender, RoutedEventArgs e)
{
UserAction = FileAction.SkipAll;
DialogResult = true;
Close();
}
private string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}
}
+583 -173
View File
@@ -1,8 +1,9 @@
<Window x:Class="PS5Upload.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="PS5 Upload - High-Speed File Transfer" Height="600" Width="1200"
Title="PS5 Upload - High-Speed File Transfer" Height="900" Width="1600"
WindowStartupLocation="CenterScreen"
WindowState="Maximized"
Background="#1E1E1E"
Icon="ps5_logo.png">
@@ -65,6 +66,7 @@
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
@@ -73,23 +75,42 @@
</Grid.RowDefinitions>
<!-- Header -->
<StackPanel Grid.Row="0" Margin="0,0,0,20" Orientation="Horizontal" HorizontalAlignment="Center">
<Image Source="ps5_logo.png"
Width="80"
Height="80"
Margin="0,0,15,0"
VerticalAlignment="Center"/>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="PS5 Upload - High-Speed File Transfer"
FontSize="24"
FontWeight="Bold"
Foreground="#007ACC"/>
<TextBlock Text="By Manos"
FontSize="12"
Foreground="#999999"
Margin="0,5,0,0"/>
<Grid Grid.Row="0" Margin="0,0,0,20">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Image Source="ps5_logo.png"
Width="80"
Height="80"
Margin="0,0,15,0"
VerticalAlignment="Center"/>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="PS5 Upload - High-Speed File Transfer"
FontSize="24"
FontWeight="Bold"
Foreground="#007ACC"/>
<TextBlock Text="By Manos"
FontSize="12"
Foreground="#999999"
Margin="0,5,0,0"/>
</StackPanel>
</StackPanel>
</StackPanel>
<!-- Buy Me a Coffee Button - Top Right -->
<Button x:Name="BuyMeCoffeeButton"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Background="#FFDD00"
Foreground="#000000"
Padding="12,6"
FontSize="13"
FontWeight="SemiBold"
Click="BuyMeCoffeeButton_Click"
ToolTip="Support the developer ☕">
<StackPanel Orientation="Horizontal">
<TextBlock Text="☕" FontSize="16" Margin="0,0,5,0" VerticalAlignment="Center"/>
<TextBlock Text="Buy me a coffee" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</Grid>
<!-- Connection Panel -->
<Border Grid.Row="1"
@@ -199,40 +220,218 @@
<!-- IP Address and Connect -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="PS5 IP Address:"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<TextBlock Grid.Column="0"
Text="PS5 IP Address:"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<TextBox Grid.Column="1"
x:Name="IpAddressTextBox"
Text="192.168.0.160"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<Button Grid.Column="2"
x:Name="ConnectButton"
Content="Connect"
Click="ConnectButton_Click"
Width="100"/>
</Grid>
<TextBox Grid.Column="1"
x:Name="IpAddressTextBox"
Text="192.168.0.160"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<!-- Auto-send Payload Settings -->
<Expander Grid.Row="1"
Header="⚙️ Auto-send Payload Settings"
IsExpanded="False"
Background="#2D2D30"
Foreground="White"
Margin="0,10,0,0">
<StackPanel Margin="10">
<CheckBox x:Name="AutoSendPayloadCheckBox"
Content="Auto-send payload on startup"
Foreground="White"
Margin="0,0,0,10"
Checked="AutoSendPayloadCheckBox_Changed"
Unchecked="AutoSendPayloadCheckBox_Changed"/>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="Payload File:"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<TextBox Grid.Column="1"
x:Name="PayloadPathTextBox"
Text=""
VerticalAlignment="Center"
Margin="0,0,10,0"
IsReadOnly="True"
AllowDrop="True"
PreviewDragOver="PayloadPathTextBox_DragOver"
Drop="PayloadPathTextBox_Drop"/>
<Button Grid.Column="2"
Content="📂 Browse"
Click="BrowsePayloadButton_Click"
Width="100"/>
</Grid>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="Payload Port:"
VerticalAlignment="Center"
Margin="0,0,10,0"/>
<TextBox Grid.Column="1"
x:Name="PayloadPortTextBox"
Text="9021"
VerticalAlignment="Center"
TextChanged="PayloadPortTextBox_Changed"/>
</Grid>
<!-- Manual Send Payload Button -->
<Button x:Name="SendPayloadButton"
Content="📤 Send Payload Now"
Click="SendPayloadButton_Click"
Background="#007ACC"
Foreground="White"
Padding="10,5"
HorizontalAlignment="Stretch"/>
</StackPanel>
</Expander>
</Grid>
</Grid>
</Border>
<!-- Storage Info Panel -->
<Border Grid.Row="2"
Background="#252526"
BorderBrush="#3E3E42"
BorderThickness="1"
CornerRadius="4"
Padding="15"
Margin="0,0,0,15"
x:Name="StorageInfoPanel"
Visibility="Collapsed">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text=" PS5 Storage Info"
FontSize="16"
FontWeight="Bold"
Foreground="#007ACC"
VerticalAlignment="Center"/>
<Button Content=" Refresh"
Click="RefreshStorageButton_Click"
Width="100"
Margin="15,0,0,0"
Background="#28A745"/>
</StackPanel>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="2"
x:Name="ConnectButton"
Content="Connect"
Click="ConnectButton_Click"
Width="100"/>
<!-- Column 1: Capacity Info -->
<StackPanel Grid.Column="0" Margin="0,0,20,0">
<TextBlock Text="━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Foreground="#3E3E42"
Margin="0,0,0,5"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
<TextBlock Text="Total Capacity:" Width="150" Foreground="#999999"/>
<TextBlock x:Name="TotalCapacityText" Text="---" FontWeight="Bold"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
<TextBlock Text="Real Free Space:" Width="150" Foreground="#999999"/>
<TextBlock x:Name="RealFreeSpaceText" Text="---" FontWeight="Bold" Foreground="#28A745"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
<TextBlock Text="System Reserved:" Width="150" Foreground="#999999"/>
<TextBlock x:Name="ReservedSpaceText" Text="---" FontWeight="Bold" Foreground="#FFC107"/>
</StackPanel>
</StackPanel>
<!-- Column 2: Usage Info -->
<StackPanel Grid.Column="1" Margin="0,0,20,0">
<TextBlock Text="━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Foreground="#3E3E42"
Margin="0,0,0,5"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
<TextBlock Text="Mounted Games:" Width="150" Foreground="#999999"/>
<TextBlock x:Name="MountedGamesText" Text="---" FontWeight="Bold" Foreground="#007ACC"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
<TextBlock Text="User Data:" Width="150" Foreground="#999999"/>
<TextBlock x:Name="UserDataText" Text="---" FontWeight="Bold" Foreground="#007ACC"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
<TextBlock Text="Total Used:" Width="150" Foreground="#999999"/>
<TextBlock x:Name="TotalUsedText" Text="---" FontWeight="Bold" Foreground="#DC3545"/>
</StackPanel>
</StackPanel>
<!-- Column 3: Progress Bar -->
<StackPanel Grid.Column="2">
<TextBlock Text="━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Foreground="#3E3E42"
Margin="0,0,0,5"/>
<TextBlock Text="Disk Usage" Foreground="#999999" Margin="0,0,0,5"/>
<ProgressBar x:Name="StorageProgressBar"
Height="30"
Minimum="0"
Maximum="100"
Value="0"
Background="#2D2D30"
Foreground="#007ACC"
BorderBrush="#3E3E42"
BorderThickness="1"/>
<TextBlock x:Name="StoragePercentText"
Text="0% Used"
HorizontalAlignment="Center"
Margin="0,5,0,0"
FontWeight="Bold"/>
</StackPanel>
</Grid>
</Grid>
</Border>
<!-- Main Content -->
<Grid Grid.Row="2">
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="15"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="15"/>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Local Files -->
@@ -484,149 +683,356 @@
</ListBox.ItemTemplate>
</ListBox>
<Border Grid.Row="3"
Background="#2D2D30"
BorderBrush="#3E3E42"
BorderThickness="1"
CornerRadius="4"
Margin="10"
Padding="10">
<StackPanel>
<TextBlock Text="💾 PS5 Storage"
FontSize="12"
FontWeight="Bold"
Foreground="#007ACC"
HorizontalAlignment="Center"
Margin="0,0,0,8"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="Free:"
FontSize="11"
Foreground="#999999"
Margin="0,0,8,4"/>
<TextBlock Grid.Row="0" Grid.Column="1"
x:Name="StorageFreeText"
Text="--"
FontSize="11"
Foreground="#4EC9B0"
FontWeight="Bold"
HorizontalAlignment="Right"
Margin="0,0,0,4"/>
<TextBlock Grid.Row="1" Grid.Column="0"
Text="Used:"
FontSize="11"
Foreground="#999999"
Margin="0,0,8,4"/>
<TextBlock Grid.Row="1" Grid.Column="1"
x:Name="StorageUsedText"
Text="--"
FontSize="11"
Foreground="#CE9178"
FontWeight="Bold"
HorizontalAlignment="Right"
Margin="0,0,0,4"/>
<TextBlock Grid.Row="2" Grid.Column="0"
Text="Total:"
FontSize="11"
Foreground="#999999"
Margin="0,0,8,4"/>
<TextBlock Grid.Row="2" Grid.Column="1"
x:Name="StorageTotalText"
Text="--"
FontSize="11"
Foreground="White"
FontWeight="Bold"
HorizontalAlignment="Right"
Margin="0,0,0,4"/>
<!-- Progress Bar -->
<ProgressBar Grid.Row="3" Grid.ColumnSpan="2"
x:Name="StorageProgressBar"
Height="6"
Margin="0,6,0,0"
Minimum="0"
Maximum="100"
Value="0"
Foreground="#CE9178"
Background="#3E3E42"/>
</Grid>
</StackPanel>
</Border>
</Grid>
</Border>
<!-- Log Panel -->
<!-- Tabs Panel (Debug Log, Transfer History, Shell, Search) -->
<Border Grid.Column="4"
Background="#252526"
BorderBrush="#3E3E42"
BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TabControl Background="#252526" BorderThickness="0" TabStripPlacement="Top">
<TabControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Background" Value="#2D2D30"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="15,8"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Border Background="{TemplateBinding Background}"
BorderThickness="0,0,0,2"
BorderBrush="Transparent"
Padding="{TemplateBinding Padding}">
<ContentPresenter ContentSource="Header"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#1E1E1E"/>
<Setter Property="BorderBrush" Value="#007ACC"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3E3E42"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
<Border Grid.Row="0"
Background="#2D2D30"
Padding="10"
BorderBrush="#3E3E42"
BorderThickness="0,0,0,1">
<TextBlock Text="📋 Debug Log"
FontSize="16"
FontWeight="SemiBold"/>
</Border>
<!-- Debug Log Tab -->
<TabItem Header="📋 Debug Log">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="1"
x:Name="LogTextBox"
Text=""
FontFamily="Consolas"
FontSize="11"
Foreground="#CCCCCC"
Background="#1E1E1E"
BorderThickness="0"
Padding="10"
TextWrapping="Wrap"
IsReadOnly="True"
IsReadOnlyCaretVisible="False"
Focusable="False"
Cursor="Arrow"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"/>
<TextBox Grid.Row="0"
x:Name="LogTextBox"
Text=""
FontFamily="Consolas"
FontSize="11"
Foreground="#CCCCCC"
Background="#1E1E1E"
BorderThickness="0"
Padding="10"
TextWrapping="Wrap"
IsReadOnly="True"
IsReadOnlyCaretVisible="False"
Focusable="False"
Cursor="Arrow"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"/>
<StackPanel Grid.Row="2"
Orientation="Horizontal"
Margin="10"
HorizontalAlignment="Center">
<Button Content="📋 Copy Log"
Click="CopyLogButton_Click"
Margin="0,0,10,0"
Background="#28A745"/>
<Button Content="Clear Log"
Click="ClearLogButton_Click"
Background="#3E3E42"/>
</StackPanel>
</Grid>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
Margin="10"
HorizontalAlignment="Center">
<Button Content="📋 Copy Log"
Click="CopyLogButton_Click"
Margin="0,0,10,0"
Background="#28A745"/>
<Button Content="🗑️ Clear Log"
Click="ClearLogButton_Click"
Background="#3E3E42"/>
</StackPanel>
</Grid>
</TabItem>
<!-- Transfer History Tab -->
<TabItem Header="📊 Transfer History">
<Grid Background="#1E1E1E">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Completed Transfers Section -->
<Expander Grid.Row="0"
Header="✅ Completed Transfers"
IsExpanded="True"
Background="#2D2D30"
Foreground="White"
Margin="10,10,10,5">
<ListBox x:Name="CompletedTransfersListBox"
Background="#1E1E1E"
Foreground="White"
BorderThickness="0"
MaxHeight="200"
Margin="5"/>
</Expander>
<!-- Failed Transfers Section -->
<Expander Grid.Row="1"
Header="❌ Failed Transfers"
IsExpanded="True"
Background="#2D2D30"
Foreground="White"
Margin="10,5,10,10">
<ListBox x:Name="FailedTransfersListBox"
Background="#1E1E1E"
Foreground="White"
BorderThickness="0"
MaxHeight="200"
Margin="5">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="🔄 Retry Upload" Click="RetryFailedUpload_Click"/>
<MenuItem Header="🗑️ Remove from List" Click="RemoveFailedTransfer_Click"/>
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
</Expander>
<!-- Buttons -->
<StackPanel Grid.Row="2"
Orientation="Horizontal"
Margin="10"
HorizontalAlignment="Center">
<Button Content="📤 Export"
Margin="0,0,10,0"
Background="#28A745"/>
<Button Content="🗑️ Clear All"
Click="ClearTransferHistory_Click"
Background="#DC3545"/>
<CheckBox Content="Auto Clear on Startup"
Foreground="White"
Margin="20,0,0,0"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</TabItem>
<!-- Shell Tab -->
<TabItem Header="💻 Shell">
<Grid Background="#1E1E1E">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Shell Header -->
<TextBlock Grid.Row="0"
Text="PS5 Shell Terminal - Ready&#x0a;Type 'help' for available commands"
FontFamily="Consolas"
FontSize="11"
Foreground="#28A745"
Background="#0D0D0D"
Padding="10"
Margin="0"/>
<!-- Shell Output -->
<ListBox Grid.Row="1"
x:Name="ShellOutputListBox"
ItemsSource="{Binding}"
FontFamily="Consolas"
FontSize="11"
Foreground="#CCCCCC"
Background="#0D0D0D"
BorderThickness="0"
Padding="10"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"/>
<!-- Command Input -->
<TextBox Grid.Row="2"
x:Name="ShellCommandInput"
FontFamily="Consolas"
FontSize="11"
Foreground="White"
Background="#2D2D30"
BorderBrush="#3E3E42"
BorderThickness="0,1,0,0"
Padding="10"
KeyDown="ShellCommandInput_KeyDown"/>
<!-- Shell Buttons -->
<StackPanel Grid.Row="3"
Orientation="Horizontal"
Margin="10"
HorizontalAlignment="Center">
<Button Content="🗑️ Clear"
Click="ClearShellButton_Click"
Margin="0,0,10,0"
Background="#007ACC"/>
<Button Content="💾 Save Log"
Click="SaveShellLogButton_Click"
Background="#28A745"/>
</StackPanel>
</Grid>
</TabItem>
<!-- Search Tab -->
<TabItem Header="🔍 Search">
<Grid Background="#1E1E1E">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Index Status -->
<Border Grid.Row="0"
Background="#2D2D30"
Padding="10"
Margin="10,10,10,5">
<StackPanel>
<TextBlock x:Name="IndexStatusText"
Text="Index Status: Not started"
Foreground="White"
Margin="0,0,0,10"/>
<StackPanel Orientation="Horizontal">
<Button x:Name="StartIndexButton"
Content="🔄 Start Index"
Click="StartIndexButton_Click"
Margin="0,0,10,0"
Background="#007ACC"/>
<Button x:Name="RefreshIndexButton"
Content="🔃 Refresh"
Click="RefreshIndexButton_Click"
Background="#3E3E42"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Search Input -->
<Grid Grid.Row="1" Margin="10,5,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
x:Name="SearchInputBox"
Background="#2D2D30"
Foreground="White"
BorderBrush="#3E3E42"
Padding="10"
KeyDown="SearchInputBox_KeyDown"/>
<Button Grid.Column="1"
Content="🗑️ Clear"
Click="ClearIndexSearchButton_Click"
Margin="10,0,0,0"
Background="#3E3E42"/>
</Grid>
<!-- Search Results -->
<DataGrid Grid.Row="2"
x:Name="SearchResultsGrid"
Background="#1E1E1E"
Foreground="White"
BorderBrush="#3E3E42"
BorderThickness="1"
Margin="10,0,10,10"
AutoGenerateColumns="False"
HeadersVisibility="Column"
GridLinesVisibility="None"
IsReadOnly="True"
MouseDoubleClick="SearchResultsGrid_MouseDoubleClick">
<DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#2D2D30"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Padding" Value="10,5"/>
<Setter Property="BorderBrush" Value="#3E3E42"/>
<Setter Property="BorderThickness" Value="0,0,1,1"/>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="Background" Value="#1E1E1E"/>
<Setter Property="Foreground" Value="White"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2D2D30"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#007ACC"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="5"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#007ACC"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Path" Width="2*" Binding="{Binding Path}"/>
<DataGridTextColumn Header="Size" Width="100" Binding="{Binding SizeText}"/>
<DataGridTextColumn Header="Modified" Width="150" Binding="{Binding Modified, StringFormat='{}{0:yyyy-MM-dd HH:mm}'}"/>
</DataGrid.Columns>
</DataGrid>
<!-- Search Examples -->
<Border Grid.Row="3"
Background="#2D2D30"
Padding="10"
Margin="10,0,10,10">
<StackPanel>
<TextBlock Text="💡 Search Examples:"
Foreground="#999999"
FontSize="11"
Margin="0,0,0,5"/>
<TextBlock Text="• *.pkg - Find all PKG files"
Foreground="#999999"
FontSize="10"/>
<TextBlock Text="• loader - Find files containing 'loader'"
Foreground="#999999"
FontSize="10"/>
<TextBlock Text="• *.elf size:&gt;10MB - ELF files larger than 10MB"
Foreground="#999999"
FontSize="10"/>
<TextBlock Text="• game* size:&lt;1GB - Files starting with 'game', smaller than 1GB"
Foreground="#999999"
FontSize="10"/>
</StackPanel>
</Border>
</Grid>
</TabItem>
</TabControl>
</Border>
</Grid>
<!-- Upload Progress -->
<Border Grid.Row="3"
<Border Grid.Row="4"
Background="#252526"
BorderBrush="#3E3E42"
BorderThickness="1"
@@ -722,29 +1128,33 @@
</Grid>
</Border>
<!-- Upload Buttons -->
<Grid Grid.Row="4" Margin="0,15,0,0">
<!-- Upload and Cancel Buttons -->
<Grid Grid.Row="5" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Upload Button -->
<Button Grid.Column="0"
x:Name="UploadButton"
Content="⬆️ Upload to PS5"
Content="📤 Upload to PS5"
Click="UploadButton_Click"
Height="45"
FontSize="16"
FontWeight="SemiBold"
Background="#007ACC"
IsEnabled="False"/>
<!-- Cancel Button -->
<Button Grid.Column="2"
x:Name="CancelButton"
Content="❌ Cancel"
Click="CancelButton_Click"
Height="45"
FontSize="16"
Width="150"
FontSize="14"
FontWeight="SemiBold"
Background="#DC3545"
IsEnabled="False"/>
+1403 -318
View File
File diff suppressed because it is too large. Load diff
-29
View File
@@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AssemblyName>PS5Upload</AssemblyName>
<RootNamespace>PS5Upload</RootNamespace>
<Version>1.0.0</Version>
<Authors>Manos</Authors>
<Description>High-Speed File Upload Tool for PS5</Description>
<Copyright>Copyright © 2026 Manos</Copyright>
<Nullable>enable</Nullable>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<ApplicationIcon>ps5_logo_fixed.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Resource Include="ps5_logo.png" />
<None Include="ps5_logo.ico">
<Link>ps5_logo.ico</Link>
</None>
</ItemGroup>
</Project>
-29
View File
@@ -1,29 +0,0 @@
@echo off
echo ================================================
echo PS5 Upload Client - Building
echo By Manos
echo ================================================
echo.
echo [+] Building Windows GUI Client...
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true
if exist "bin\Release\net6.0-windows\win-x64\publish\PS5Upload.exe" (
echo.
echo ================================================
echo [+] SUCCESS! Built PS5Upload.exe
echo ================================================
echo.
echo Location: bin\Release\net6.0-windows\win-x64\publish\PS5Upload.exe
echo.
echo Copy PS5Upload.exe to any location and run it!
) else (
echo.
echo ================================================
echo [-] BUILD FAILED
echo ================================================
echo.
echo Make sure .NET 6.0 SDK is installed:
echo https://dotnet.microsoft.com/download/dotnet/6.0
exit /b 1
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

+14
View File
@@ -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>
+11
View File
@@ -0,0 +1,11 @@
namespace PS5UploadMobile;
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
+14
View File
@@ -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>
+9
View File
@@ -0,0 +1,9 @@
namespace PS5UploadMobile;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
+98
View File
@@ -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>
+571
View File
@@ -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}%";
});
}
}
+18
View File
@@ -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();
}
}
+47
View File
@@ -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>
+270
View File
@@ -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;
}
}
}
+241
View File
@@ -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; } = "";
}
}
@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.IO;
using System.Net.Sockets;
using System.Text;
@@ -23,6 +24,14 @@ namespace PS5Upload
UploadChunk = 0x11,
EndUpload = 0x12,
DownloadFile = 0x13,
ShellOpen = 0x20,
ShellExec = 0x21,
ShellInterrupt = 0x22,
ShellClose = 0x23,
IndexStart = 0x40,
IndexStatus = 0x41,
SearchIndex = 0x42,
IndexCancel = 0x43,
Shutdown = 0xFF
}
@@ -40,7 +49,7 @@ namespace PS5Upload
{
private TcpClient? _client;
private NetworkStream? _stream;
private const int BufferSize = 4 * 1024 * 1024; // 4MB chunks - MUST match payload BUFFER_SIZE
private const int BufferSize = 4 * 1024 * 1024; // 4MB buffer - maximum throughput for chunk uploads
public bool IsConnected => _client?.Connected ?? false;
@@ -49,8 +58,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);
@@ -68,6 +77,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)
@@ -166,39 +178,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);
@@ -206,7 +250,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];
@@ -271,14 +318,7 @@ namespace PS5Upload
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;
@@ -381,80 +421,117 @@ 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");
return false;
}
// Upload chunks - simple async approach
long totalSent = 0;
var startTime = DateTime.Now;
double avgSpeed = 0;
byte[] sendBuffer = new byte[5 + BufferSize];
sendBuffer[0] = (byte)Command.UploadChunk;
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;
// FIX: Add timeout to WriteAsync to prevent hanging if PS5 becomes unresponsive
// 30 second timeout per chunk write - should be more than enough for 4MB
using var writeTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, writeTimeout.Token);
try
if (chunkOffset > 0)
{
await _stream.WriteAsync(sendBuffer, 0, 5 + bytesRead, linkedCts.Token);
}
catch (OperationCanceledException) when (writeTimeout.IsCancellationRequested)
{
// Write timeout - PS5 is unresponsive
return false;
fs.Seek(chunkOffset, SeekOrigin.Begin);
}
totalSent += bytesRead;
bytesRemaining -= bytesRead;
chunksSent++;
int activeBufferIndex = 0;
long bytesRemaining = uploadSize;
int bytesRead = await fs.ReadAsync(sendBuffers[activeBufferIndex], 5, (int)Math.Min(BufferSize, bytesRemaining), cancellationToken);
int chunksSent = 0;
if (chunksSent % 5 == 0 || bytesRemaining == 0)
while (bytesRemaining > 0 && bytesRead > 0)
{
var elapsed = DateTime.Now - startTime;
double currentSpeed = elapsed.TotalSeconds > 0 ? totalSent / elapsed.TotalSeconds : 0;
avgSpeed = currentSpeed;
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 % 20 == 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 and wait for response
// CRITICAL: Must wait for response to avoid protocol desync on chunked uploads
@@ -554,12 +631,271 @@ namespace PS5Upload
}
}
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];
int read = await _stream.ReadAsync(respBuf, 0, 1);
if (read == 0) 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 _stream.ReadAsync(pathLenBuf, 0, 4);
uint pathLen = BitConverter.ToUInt32(pathLenBuf, 0);
byte[] pathBuf = new byte[pathLen];
await _stream.ReadAsync(pathBuf, 0, (int)pathLen);
string path = Encoding.UTF8.GetString(pathBuf);
byte[] nameLenBuf = new byte[4];
await _stream.ReadAsync(nameLenBuf, 0, 4);
uint nameLen = BitConverter.ToUInt32(nameLenBuf, 0);
byte[] nameBuf = new byte[nameLen];
await _stream.ReadAsync(nameBuf, 0, (int)nameLen);
string name = Encoding.UTF8.GetString(nameBuf);
byte[] sizeBuf = new byte[8];
await _stream.ReadAsync(sizeBuf, 0, 8);
long size = BitConverter.ToInt64(sizeBuf, 0);
byte[] mtimeBuf = new byte[8];
await _stream.ReadAsync(mtimeBuf, 0, 8);
long mtime = BitConverter.ToInt64(mtimeBuf, 0);
byte[] isDirBuf = new byte[1];
await _stream.ReadAsync(isDirBuf, 0, 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 _stream.ReadAsync(dataLenBuf, 0, 4);
uint dataLen = BitConverter.ToUInt32(dataLenBuf, 0);
if (dataLen > 0)
{
byte[] msgBuf = new byte[dataLen];
await _stream.ReadAsync(msgBuf, 0, (int)dataLen);
}
break;
}
else if (response == Response.Error)
{
// Read error message
byte[] dataLenBuf = new byte[4];
await _stream.ReadAsync(dataLenBuf, 0, 4);
uint dataLen = BitConverter.ToUInt32(dataLenBuf, 0);
if (dataLen > 0)
{
byte[] msgBuf = new byte[dataLen];
await _stream.ReadAsync(msgBuf, 0, (int)dataLen);
}
break;
}
}
return results.ToArray();
}
// 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();
}
}
public void Dispose()
{
Disconnect();
}
}
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; } = "";
@@ -567,6 +903,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; } = "";
-30
View File
@@ -1,30 +0,0 @@
#!/bin/bash
echo "================================================"
echo "PS5 Upload Server - Compilation"
echo "By Manos"
echo "================================================"
echo ""
echo "[+] Compiling..."
rm -f ps5_upload_server.elf
/opt/ps5-payload-sdk/bin/prospero-clang -Wall -O3 -pthread -o ps5_upload_server.elf main.c
if [ -f "ps5_upload_server.elf" ]; then
echo ""
echo "================================================"
echo "[+] SUCCESS! Compiled ps5_upload_server.elf"
echo "================================================"
echo ""
ls -lh ps5_upload_server.elf
file ps5_upload_server.elf
echo ""
echo "Upload to PS5 and run with elfldr"
echo "Server will listen on port 9113"
else
echo ""
echo "================================================"
echo "[-] COMPILATION FAILED"
echo "================================================"
exit 1
fi
-1282
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
+343
View File
@@ -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!**
![Transfer History & Multi-PS5 Support](screenshots/screenshot3.png)
![Mobile App](screenshots/mobile_screenshot.jpg)
---
## 📦 Downloads
Download the latest release from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page:
### 1. PS5 Server Payload
- **File:** `ps5_upload_server.elf`
- **Port:** 9113
- High-speed custom binary protocol
- 16MB socket buffers for maximum throughput
- Multi-threaded client handling
### 2. Windows GUI Client
- **File:** `PS5Upload.exe`
- Self-contained executable (no .NET installation required)
- Modern dark theme UI
- Drag & drop file/folder upload
- Real-time upload progress with speed tracking
- Transfer History with success/failed tracking
- Privacy-focused (no storage monitoring)
### 3. 📱 Android Mobile Client
- **File:** `PS5UploadMobile.apk`
- Upload/download files from your phone
- Multi-PS5 Profiles support
- Browse PS5 filesystem
- Favorites for quick navigation
---
## 🚀 Quick Start
### Step 1: Load PS5 Payload
1. Download `ps5_upload_server.elf` from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page
2. Copy it to your PS5: `/data/etaHEN/payloads/ps5_upload_server.elf`
3. Load the payload with elfldr
4. You should see notification: `PS5 Upload Server: 192.168.0.XXX:9113 - By Manos`
### Step 2: Run Windows Client
1. Run `PS5Upload.exe` on your PC
2. Enter your PS5's IP address (e.g., `192.168.0.160`)
3. Click **Connect**
4. Browse files/folders or drag & drop
5. Click **Upload to PS5**
---
## 🎨 Features
### PS5 Server
✅ **High-Speed Protocol** - Custom binary protocol
✅ **4MB Buffers** - Maximum throughput
✅ **Direct Disk I/O** - No temp files
✅ **Multi-threaded** - Handle multiple clients
✅ **Robust Error Handling** - Graceful failures
### Windows Client
✅ **Modern UI** - Dark theme, maximized window for better visibility
✅ **Drag & Drop** - Files and folders
✅ **Browse PS5** - Navigate filesystem
✅ **Real-time Progress** - Speed & percentage tracking
✅ **Optimized Upload** - 8 parallel connections for 88-110 MB/s aggregate speed
✅ **Transfer History** - Track all uploads/downloads with success/failed status
✅ **Auto-Clear History** - Optional auto-clear on startup
✅ **Folder Upload** - Recursive directory upload
✅ **Download Files** - Download from PS5 to PC
✅ **🔍 Smart Search** - Full filesystem indexing with instant search (NEW v3.3)
- Wildcard support: `*.pkg`, `*loader*`, `game*.bin`
- Size filters: `size:>1GB`, `size:<100MB`
- Case-insensitive matching
- Search both filename and full path
- Double-click to navigate to folder
✅ **💻 Shell Terminal** - Execute commands directly on PS5 (NEW v3.3)
- Run system commands remotely
- Real-time output display
- Command history
- Working directory support
✅ **Favorites/Bookmarks** - Quick navigation to saved paths
✅ **Multi-PS5 Support** - Save and switch between multiple PS5 profiles
---
## 📊 Performance
### v3.1.0 Optimized Results:
- ✅ **104+ MB/s** upload speed for large files (parallel chunked uploads)
- ✅ **4 parallel chunk connections** for maximum throughput
- ✅ **500MB chunks** - optimal size for PS5 disk performance
- ✅ **Real-time progress** - Speed/ETA updates during upload
- ✅ **Zero chunk failures** - Fixed race conditions
- ✅ **Fully responsive UI** throughout upload
- ✅ **No memory leaks** - stable operation
### Previous Results (v1.3.0):
- ✅ **42,801 files** uploaded successfully
- ✅ **60-150 MB/s** sustained throughput
| Network | Expected Upload Speed | Expected Download Speed |
|---------|----------------------|------------------------|
| **Gigabit Ethernet** | 88-110 MB/s (aggregate) | 100-120 MB/s |
| **WiFi 6 (5GHz)** | 32-70 MB/s (aggregate) | 60-80 MB/s |
| **WiFi 5 (5GHz)** | 20-50 MB/s (aggregate) | 40-60 MB/s |
**Note:** Upload speeds depend on network quality and PS5 disk write performance. WiFi has ~40-60% overhead compared to Ethernet.
---
## 🛠️ Troubleshooting
### Connection Failed
- Make sure PS5 payload is running
- Check PS5 IP address is correct
- Verify both devices are on same network
- Check firewall settings
### Slow Upload Speed
- Use wired Ethernet connection (not WiFi)
- Close other network applications
- Check PS5 disk health
### Upload Fails
- Check PS5 has enough free space
- Verify destination path exists
- Check file permissions
---
## 🔒 Security Notes
- Server only accepts connections from local network
- No authentication required (local network only)
- SHUTDOWN command only works from localhost
---
## 📝 What's New in v3.2.0
### 🎯 Improvements:
#### 1. 💾 Accurate Storage Display
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
- **Accounts for reserved space** - Same calculation as PS5 system
#### 2. 📱 Mobile App Improvements
- **Multi-select mode** - Toggle button to select multiple files/folders
- **Folder browser** - Visual folder picker for Copy/Move destinations
- **Sorted file list** - Folders always displayed at top
- **Exit confirmation** - Prompt when pressing back button
- **Delete folders** - Now supports folder deletion (not just files)
- **Batch operations** - Delete/download multiple items at once
#### 3. 🐛 Bug Fixes
- **Fixed empty folder deletion** - No more "Unexpected response" error
- **Fixed Copy/Move crash** - No longer crashes when selecting destination
- **Fixed multi-select count** - Correct item count in delete confirmation
- **Fixed folder navigation** - Can enter folders after actions complete
---
## 📝 What's New in v3.1.0
### 🚀 Performance Improvements:
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
- **Large files (>100MB)** are now split into 500MB chunks
- **4 parallel connections** upload chunks simultaneously
- **Result:** ~104 MB/s upload speed (2x faster than before!)
- **Fixed race condition:** First chunk creates file before others start
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
#### 2. 📊 Real-time Progress Display
- **Speed display** now updates in real-time during chunked uploads
- **ETA display** shows accurate time remaining
- **Progress bar** correctly shows 0-100% for chunked files
- No more speed dropping to 0 during large file uploads
#### 3. 💾 Storage Display Improvements
- **Changed label** to "Data Storage (/data)" for clarity
- **Uses statfs with f_bavail** for more accurate available space
- **Real-time updates** every 5 seconds when connected
---
## 📝 What's New in v3.0.0
### 🐛 Critical Bug Fixes:
#### 1. 🔧 Path Normalization Fix
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
- **Solution:** Added `NormalizePath()` function in both client and server
- **Result:** 100% reliable directory creation and file uploads
#### 2. 📱 Android Mobile Client
- **NEW:** Full-featured Android app for PS5 file management
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
- Upload files from phone to PS5
- Download files from PS5 with Share option
- Browse PS5 filesystem
- Favorites for quick navigation
- Debug Log with Copy to clipboard
- Transfer History tracking
#### 3. 🛠️ Server Improvements
- Path normalization in all handler functions
- More robust error handling
- Improved stability for parallel uploads
---
## 📝 What's New in v2.1.0
### 🚀 Performance Optimizations:
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
- **Per-file:** 11-14 MB/s sustained per connection
#### 2. 📊 Transfer History
- Complete history of all uploads and downloads
- Success/Failed status tracking with error messages
- Speed statistics (average, min, max)
- Export to CSV/JSON for analysis
- Persistent storage across sessions
#### 3. 🔄 Auto-Clear History on Startup
- Optional checkbox to clear history automatically
- Useful for keeping UI clean between sessions
- Setting saved in `ps5_upload_settings.json`
#### 4. 🖥️ Maximized Window UI
- Application opens in full-screen mode by default
- Better visibility for large file transfers
- Can be resized/restored as needed
### Technical Improvements:
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
---
## 📝 What's New in v2.0.0
### 🎉 4 Major New Features:
#### 1. 📥 Download Files (PS5 → PC)
- Right-click any file → "⬇️ Download to PC"
- Save file dialog for destination selection
- Real-time progress tracking with speed display
- Optimized with sendfile for maximum speed
#### 2. 🔍 File Search
- Search box in PS5 Files panel
- Real-time filtering as you type
- Case-insensitive search
- Quick "Clear" button to reset
#### 3. ⭐ Favorites/Bookmarks
- Save frequently used PS5 paths
- Quick dropdown navigation
- Add/Remove favorite paths
- Persistent storage in JSON
#### 4. 🎮 Multi-PS5 Support
- Save multiple PS5 profiles (IP + name)
- Quick switch between different PS5 consoles
- Dropdown profile selector
- Persistent profile storage
### Previous Stability (v1.3.0):
- ✅ **Zero connection drops** - 5 minute socket timeout + aggressive keepalive
- ✅ **Fully responsive UI** - Async updates + throttled logging
- ✅ **No memory leaks** - Proper resource disposal
- ✅ **Tested:** 42,801 files uploaded with 100% success rate
---
## 📝 License
This software is provided as-is for personal use.
---
## 👤 Author
**Manos**
Created with ❤️ for the PS5 homebrew community
---
## 🙏 Credits
- PS5 SDK
- etaHEN
- Inspired by ps5upload by PhantomPtr
- PS5 homebrew community
---
## 📞 Support
For issues or questions, please open an issue on GitHub.
---
**Enjoy blazing-fast file transfers on your PS5!** 🚀
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB