mirror of
https://github.com/manos555555/PS5-Suite.git
synced 2026-10-06 11:00:32 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4403d86be5 | ||
|
|
af252efc9a | ||
|
|
2da27aed13 | ||
|
|
264ad6210d | ||
|
|
db48bc6fb2 | ||
|
|
4ac43ebb8b | ||
|
|
20caced5f4 | ||
|
|
285279788f | ||
|
|
70d174cecc | ||
|
|
f128c6a6b4 | ||
|
|
c211c086ac | ||
|
|
4ca30876c7 | ||
|
|
5ae3c1709e | ||
|
|
3374e87bfe | ||
|
|
c21d73f41d | ||
|
|
5ce9d470bc | ||
|
|
0eb159bf88 | ||
|
|
4ff160232f | ||
|
|
a5e40ce245 | ||
|
|
bb7d90411c | ||
|
|
00013aae76 | ||
|
|
6d8d802962 | ||
|
|
f3a1d5e436 | ||
|
|
bad3029e32 | ||
|
|
a51fffef83 | ||
|
|
f89eaf4e31 | ||
|
|
f34fdf8e63 | ||
|
|
8479221915 | ||
|
|
e3bfa54994 | ||
|
|
68d586de97 | ||
|
|
297edb18af |
No files matched your search
-60
@@ -1,60 +0,0 @@
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio cache/options
|
||||
.vs/
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
*.cache
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Files built by Visual Studio
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Caches
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.log
|
||||
*.bak
|
||||
*.old
|
||||
|
||||
# PowerShell script
|
||||
convert_to_ico.ps1
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
# Changelog - PS5 Upload Suite
|
||||
|
||||
## Version 1.3.0 - Stable Release (January 13, 2026)
|
||||
|
||||
### 🎯 Major Achievement
|
||||
**Complete stability overhaul** - Fixed all critical bugs causing connection drops, UI freezes, and upload failures. System now handles **42,801+ files** with **zero errors** and **full gigabit speeds**.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Bug Fixes
|
||||
|
||||
### **PAYLOAD (Server-Side) - 7 Critical Fixes**
|
||||
|
||||
#### 1. **Per-File Mutex Implementation**
|
||||
- **Issue:** Multiple threads could write to the same file simultaneously causing data corruption
|
||||
- **Fix:** Implemented per-file mutex system using hash map for file path locking
|
||||
- **Impact:** Prevents race conditions during chunked uploads
|
||||
|
||||
#### 2. **Memory/Mutex Cleanup on Disconnect**
|
||||
- **Issue:** Mutexes and memory not properly released when client disconnected
|
||||
- **Fix:** Added proper cleanup in client disconnect handler
|
||||
- **Impact:** Prevents memory leaks and mutex deadlocks
|
||||
|
||||
#### 3. **fflush() Race Condition**
|
||||
- **Issue:** `fflush()` called outside file mutex, causing potential data corruption
|
||||
- **Fix:** Moved `fflush()` inside mutex lock in `handle_upload_chunk`
|
||||
- **Location:** `main.c:869-890`
|
||||
- **Impact:** Ensures data integrity during concurrent writes
|
||||
|
||||
#### 4. **File Creation Race Condition**
|
||||
- **Issue:** Multiple threads could attempt to create/open same file simultaneously
|
||||
- **Fix:** Added `pthread_mutex_lock` around file creation and `fseeko` in `handle_start_upload`
|
||||
- **Location:** `main.c:814-844`
|
||||
- **Impact:** Prevents file corruption during parallel uploads
|
||||
|
||||
#### 5. **Global Scan Counter Issues**
|
||||
- **Issue:** Static variables in recursive `count_files_recursive` caused stale values
|
||||
- **Fix:** Moved `scan_count` and `last_scan_notify` to global scope
|
||||
- **Location:** `main.c:324-366`
|
||||
- **Impact:** Accurate progress reporting during folder deletion
|
||||
|
||||
#### 6. **Directory Counting Bug**
|
||||
- **Issue:** `count_files_recursive` only counted files, not directories
|
||||
- **Fix:** Added directory counting in recursive function
|
||||
- **Location:** `main.c:350-351`
|
||||
- **Impact:** Correct file count for folders with only subdirectories
|
||||
|
||||
#### 7. **Socket Timeout for Large Files** ⭐ **CRITICAL FIX**
|
||||
- **Issue:** PS5 closed connections during large file uploads (100MB+) due to default timeout
|
||||
- **Fix:** Added 5-minute socket timeout (`SO_RCVTIMEO`/`SO_SNDTIMEO`)
|
||||
- **Location:** `main.c:1142-1148`
|
||||
- **Impact:** **Eliminates all connection drops** - enables stable upload of 15GB+ files
|
||||
- **Additional:** Aggressive TCP keepalive (10s idle, 5s interval, 3 probes)
|
||||
|
||||
---
|
||||
|
||||
### **CLIENT (Windows App) - 8 Critical Fixes**
|
||||
|
||||
#### 1. **ConnectAsync Timeout Memory Leak**
|
||||
- **Issue:** `TcpClient` not disposed on connection timeout
|
||||
- **Fix:** Added proper `Dispose()` call in timeout exception handler
|
||||
- **Location:** `Protocol.cs:62-67`
|
||||
- **Impact:** Prevents memory leaks during connection failures
|
||||
|
||||
#### 2. **ConnectAsync Exception Memory Leak**
|
||||
- **Issue:** `TcpClient` not disposed on general connection exceptions
|
||||
- **Fix:** Added proper `Dispose()` call in exception handler
|
||||
- **Location:** `Protocol.cs:81-84`
|
||||
- **Impact:** Prevents memory leaks during network errors
|
||||
|
||||
#### 3. **Disconnect Memory Leak**
|
||||
- **Issue:** `NetworkStream` and `TcpClient` not properly disposed
|
||||
- **Fix:** Added explicit `Dispose()` calls for both objects
|
||||
- **Location:** `Protocol.cs:91-92`
|
||||
- **Impact:** Prevents memory leaks during normal disconnection
|
||||
|
||||
#### 4. **Upload Deadlock During Chunked Uploads**
|
||||
- **Issue:** Tasks never removed from `activeTasks` when connection index mismatched
|
||||
- **Fix:** Always remove completed task regardless of connection index validity
|
||||
- **Location:** `MainWindow.xaml.cs:692-734`
|
||||
- **Impact:** Prevents infinite wait during parallel chunked uploads
|
||||
|
||||
#### 5. **Chunking Disabled for Stability** ⭐ **CRITICAL FIX**
|
||||
- **Issue:** PS5 cannot handle concurrent writes to same file, even with per-file mutex
|
||||
- **Fix:** Disabled chunking completely - use single connection per file
|
||||
- **Location:** `MainWindow.xaml.cs:611-614`
|
||||
- **Impact:** **Eliminates all "connection aborted" errors**
|
||||
- **Rationale:** Even with 2 chunks, PS5 actively closes connections during concurrent writes
|
||||
|
||||
#### 6. **UI Freeze During Uploads**
|
||||
- **Issue:** `Dispatcher.Invoke()` blocked UI thread when uploading thousands of files
|
||||
- **Fix:** Replaced with `Dispatcher.InvokeAsync()` in `UpdateUploadStats`
|
||||
- **Location:** `MainWindow.xaml.cs:115-158`
|
||||
- **Impact:** **Fully responsive UI** during uploads
|
||||
|
||||
#### 7. **Log Flooding Causing UI Freeze**
|
||||
- **Issue:** LogTextBox filled with hundreds of thousands of entries, causing UI slowdown
|
||||
- **Fix:**
|
||||
- Throttle verbose messages (log every 50th message)
|
||||
- Limit log size to 1000 lines (remove oldest 200 when limit reached)
|
||||
- Use `InvokeAsync` with `Background` priority
|
||||
- **Location:** `MainWindow.xaml.cs:165-206`
|
||||
- **Impact:** **Prevents UI freeze** and memory issues
|
||||
|
||||
#### 8. **Optimal Parallel Connection Strategy**
|
||||
- **Issue:** Need to balance speed vs stability
|
||||
- **Fix:** Use **6 parallel single-connection uploads** (no chunking)
|
||||
- **Impact:**
|
||||
- **Full gigabit speeds** (60-150 MB/s total throughput)
|
||||
- **Zero connection drops**
|
||||
- **Maximum stability**
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance Improvements
|
||||
|
||||
### Before Fixes:
|
||||
- ❌ Connection drops every 100-200 files
|
||||
- ❌ UI completely frozen during uploads
|
||||
- ❌ Chunked uploads failed with "connection aborted" errors
|
||||
- ❌ Memory leaks causing application slowdown
|
||||
- ❌ Upload deadlocks requiring application restart
|
||||
|
||||
### After Fixes:
|
||||
- ✅ **42,801+ files uploaded with ZERO errors**
|
||||
- ✅ **Fully responsive UI** throughout entire upload
|
||||
- ✅ **Consistent speeds:** 11-190 MB/s per connection
|
||||
- ✅ **Total throughput:** 60-150 MB/s (full gigabit)
|
||||
- ✅ **No memory leaks** - stable memory usage
|
||||
- ✅ **No deadlocks** - perfect task management
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Technical Architecture
|
||||
|
||||
### Payload (PS5 Server):
|
||||
```c
|
||||
// 5 minute socket timeout for large files
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 300;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
// Aggressive TCP keepalive
|
||||
int keepidle = 10; // Start after 10s idle
|
||||
int keepintvl = 5; // Send probe every 5s
|
||||
int keepcnt = 3; // Drop after 3 failures
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
|
||||
|
||||
// Per-file mutex for concurrent uploads
|
||||
pthread_mutex_t* file_mutex = get_file_mutex(file_path);
|
||||
pthread_mutex_lock(file_mutex);
|
||||
fwrite(buffer, 1, bytes_received, fp);
|
||||
fflush(fp);
|
||||
pthread_mutex_unlock(file_mutex);
|
||||
```
|
||||
|
||||
### Client (Windows):
|
||||
```csharp
|
||||
// 6 parallel single-connection uploads
|
||||
const int MaxParallelUploads = 6;
|
||||
|
||||
// Chunking disabled for stability
|
||||
if (false) // Chunking permanently disabled
|
||||
{
|
||||
// PS5 cannot handle concurrent writes to same file
|
||||
}
|
||||
|
||||
// Throttled logging for responsive UI
|
||||
if (message.Contains("📊") || message.Contains("⬆️ Uploading:"))
|
||||
{
|
||||
_logCounter++;
|
||||
if (_logCounter % 50 != 0)
|
||||
return; // Skip verbose messages
|
||||
}
|
||||
|
||||
// Async UI updates
|
||||
Dispatcher.InvokeAsync(() => {
|
||||
// Update UI without blocking
|
||||
}, DispatcherPriority.Background);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Tested Configuration
|
||||
|
||||
### Test Case:
|
||||
- **Files:** 42,801 files
|
||||
- **Total Size:** ~7.52 GB
|
||||
- **File Types:** .xpps, .pak, .mp4, .bk2, .bank, .json, .prx, .sprx
|
||||
- **Largest File:** 15.96 GB (ac2-ps5.pak)
|
||||
|
||||
### Results:
|
||||
- **Success Rate:** 100% (zero errors)
|
||||
- **Average Speed:** 60-150 MB/s
|
||||
- **Peak Speed:** 190 MB/s per connection
|
||||
- **UI Responsiveness:** Fully responsive throughout
|
||||
- **Memory Usage:** Stable (no leaks)
|
||||
- **Completion Time:** ~1.7 hours for 42,801 files
|
||||
|
||||
---
|
||||
|
||||
## 📝 Known Limitations
|
||||
|
||||
1. **Chunking Disabled:** While this reduces speed for individual large files, it's necessary for PS5 stability
|
||||
2. **6 Connection Limit:** More connections cause overhead without speed benefit
|
||||
3. **PS5 Hardware Constraint:** PS5 cannot handle concurrent writes to same file, even with proper locking
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Migration Guide
|
||||
|
||||
### From v1.2 to v1.3:
|
||||
1. **Upload new payload** (`ps5_upload_server.elf`) to PS5
|
||||
2. **Restart payload** with elfldr
|
||||
3. **Use new client** (`PS5Upload.exe`)
|
||||
4. **No configuration changes needed** - works out of the box
|
||||
|
||||
### Breaking Changes:
|
||||
- None - fully backward compatible
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Developer Notes
|
||||
|
||||
### Why Chunking Was Disabled:
|
||||
Multiple attempts were made to enable chunking with various configurations:
|
||||
- 4 chunks: Connection drops
|
||||
- 3 chunks: Connection drops
|
||||
- 2 chunks: Connection drops
|
||||
|
||||
**Root Cause:** PS5 FreeBSD kernel actively closes TCP connections when multiple threads perform concurrent `fwrite()` operations on the same file, even with proper per-file mutex locking. The 5-minute socket timeout helps, but doesn't solve the fundamental issue of concurrent writes.
|
||||
|
||||
**Solution:** Single connection per file ensures only one thread writes to each file, eliminating the issue entirely.
|
||||
|
||||
### Why 6 Parallel Connections:
|
||||
- **Less than 6:** Underutilizes network bandwidth
|
||||
- **More than 6:** Increases CPU/memory overhead without speed benefit
|
||||
- **6 connections:** Perfect balance for PS5 hardware
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
**Developed by:** Manos
|
||||
**Testing:** Extensive real-world testing with 42,801+ files
|
||||
**Platform:** PS5 (FreeBSD) + Windows 10/11
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This software is provided as-is for educational and personal use.
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
# PS5 Upload Protocol Specification
|
||||
|
||||
## Overview
|
||||
Custom binary protocol for high-speed file transfers between PC and PS5.
|
||||
Port: 9113
|
||||
|
||||
## Message Format
|
||||
|
||||
All messages follow this structure:
|
||||
```
|
||||
[Command (1 byte)][Data Length (4 bytes)][Data (variable)]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Client → Server
|
||||
|
||||
| Command | Value | Description | Data Format |
|
||||
|---------|-------|-------------|-------------|
|
||||
| PING | 0x01 | Test connection | None |
|
||||
| LIST_STORAGE | 0x02 | Get storage info | None |
|
||||
| LIST_DIR | 0x03 | List directory | Path (null-terminated string) |
|
||||
| CREATE_DIR | 0x04 | Create directory | Path (null-terminated string) |
|
||||
| DELETE_FILE | 0x05 | Delete file | Path (null-terminated string) |
|
||||
| DELETE_DIR | 0x06 | Delete directory | Path (null-terminated string) |
|
||||
| START_UPLOAD | 0x10 | Start file upload | Path (null-terminated) + File size (8 bytes) |
|
||||
| UPLOAD_CHUNK | 0x11 | Upload file chunk | Chunk data (raw bytes) |
|
||||
| END_UPLOAD | 0x12 | Finish upload | None |
|
||||
| SHUTDOWN | 0xFF | Shutdown server | None |
|
||||
|
||||
### Server → Client
|
||||
|
||||
| Response | Value | Description | Data Format |
|
||||
|----------|-------|-------------|-------------|
|
||||
| OK | 0x01 | Success | Optional message |
|
||||
| ERROR | 0x02 | Error occurred | Error message (null-terminated) |
|
||||
| DATA | 0x03 | Response data | Variable format |
|
||||
| READY | 0x04 | Ready for next chunk | None |
|
||||
| PROGRESS | 0x05 | Upload progress | Bytes written (8 bytes) |
|
||||
|
||||
## Data Formats
|
||||
|
||||
### LIST_STORAGE Response
|
||||
```
|
||||
[Drive count (4 bytes)]
|
||||
For each drive:
|
||||
[Path length (2 bytes)][Path][Total space (8 bytes)][Free space (8 bytes)]
|
||||
```
|
||||
|
||||
### LIST_DIR Response
|
||||
```
|
||||
[Entry count (4 bytes)]
|
||||
For each entry:
|
||||
[Type (1 byte: 0=file, 1=dir)][Name length (2 bytes)][Name][Size (8 bytes)][Timestamp (8 bytes)]
|
||||
```
|
||||
|
||||
## Flow Examples
|
||||
|
||||
### Upload File
|
||||
```
|
||||
Client: START_UPLOAD "/data/test.bin" size=1048576
|
||||
Server: READY
|
||||
Client: UPLOAD_CHUNK [4MB data]
|
||||
Server: PROGRESS bytes=4194304
|
||||
Client: UPLOAD_CHUNK [remaining data]
|
||||
Server: PROGRESS bytes=1048576
|
||||
Client: END_UPLOAD
|
||||
Server: OK
|
||||
```
|
||||
|
||||
### List Directory
|
||||
```
|
||||
Client: LIST_DIR "/data"
|
||||
Server: DATA [directory listing]
|
||||
```
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
- 4MB socket buffers
|
||||
- SO_NOSIGPIPE enabled
|
||||
- TCP_NODELAY for low latency
|
||||
- Direct disk writes (no temp files)
|
||||
- Streaming chunks (no full file buffering)
|
||||
@@ -2,37 +2,45 @@
|
||||
|
||||
**By Manos**
|
||||
|
||||
Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s** transfer speeds using a custom binary protocol.
|
||||
**Version 3.2.0 - Mobile Improvements & Bug Fixes**
|
||||
|
||||

|
||||

|
||||
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 v3.2.0:** Accurate storage display (848 GB), mobile multi-select, folder browser, bug fixes!
|
||||
|
||||
📱 **Android Mobile Client available!**
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📦 What's Included
|
||||
## 📦 Downloads
|
||||
|
||||
Download the latest release from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page:
|
||||
|
||||
### 1. PS5 Server Payload
|
||||
- **File:** `payload/ps5_upload_server.elf`
|
||||
- **File:** `ps5_upload_server.elf`
|
||||
- **Port:** 9113
|
||||
- **Protocol:** Custom binary (optimized for speed)
|
||||
- **Features:**
|
||||
- 4MB socket buffers
|
||||
- SO_NOSIGPIPE enabled
|
||||
- TCP_NODELAY for low latency
|
||||
- Direct disk writes (no temp files)
|
||||
- Multi-threaded client handling
|
||||
- High-speed custom binary protocol
|
||||
- 16MB socket buffers for maximum throughput
|
||||
- Multi-threaded client handling
|
||||
|
||||
### 2. Windows GUI Client
|
||||
- **File:** `client/bin/Release/net6.0-windows/win-x64/publish/PS5Upload.exe`
|
||||
- **Framework:** .NET 6.0 WPF
|
||||
- **Features:**
|
||||
- Modern dark theme UI
|
||||
- Drag & drop file/folder upload
|
||||
- Browse PS5 filesystem
|
||||
- Real-time upload progress
|
||||
- Speed indicator (MB/s)
|
||||
- Storage space display
|
||||
- Recursive folder upload
|
||||
- **File:** `PS5Upload.exe`
|
||||
- Self-contained executable (no .NET installation required)
|
||||
- Modern dark theme UI
|
||||
- Drag & drop file/folder upload
|
||||
- Real-time upload progress with speed tracking
|
||||
- Transfer History with success/failed tracking
|
||||
- Storage space display
|
||||
|
||||
### 3. 📱 Android Mobile Client
|
||||
- **File:** `PS5UploadMobile.apk`
|
||||
- Upload/download files from your phone
|
||||
- Multi-PS5 Profiles support
|
||||
- Browse PS5 filesystem
|
||||
- Favorites for quick navigation
|
||||
|
||||
---
|
||||
|
||||
@@ -40,17 +48,13 @@ Custom high-speed file transfer system for PS5 with etaHEN. Achieves **100+ MB/s
|
||||
|
||||
### Step 1: Load PS5 Payload
|
||||
|
||||
1. Copy `payload/ps5_upload_server.elf` to your PS5:
|
||||
```
|
||||
/data/etaHEN/payloads/ps5_upload_server.elf
|
||||
```
|
||||
1. Download `ps5_upload_server.elf` from the [Releases](https://github.com/manos555555/PS5-Upload-Suite/releases) page
|
||||
|
||||
2. Load the payload with elfldr
|
||||
2. Copy it to your PS5: `/data/etaHEN/payloads/ps5_upload_server.elf`
|
||||
|
||||
3. You should see notification:
|
||||
```
|
||||
PS5 Upload Server: 192.168.0.XXX:9113 - By Manos
|
||||
```
|
||||
3. Load the payload with elfldr
|
||||
|
||||
4. You should see notification: `PS5 Upload Server: 192.168.0.XXX:9113 - By Manos`
|
||||
|
||||
### Step 2: Run Windows Client
|
||||
|
||||
@@ -76,73 +80,45 @@ 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 (NEW v2.1)
|
||||
✅ **Transfer History** - Track all uploads/downloads with success/failed status (NEW v2.1)
|
||||
✅ **Auto-Clear History** - Optional auto-clear on startup (NEW v2.1)
|
||||
✅ **Folder Upload** - Recursive directory upload
|
||||
✅ **Storage Info** - Free space display
|
||||
✅ **Download Files** - Download from PS5 to PC (v2.0)
|
||||
✅ **File Search** - Real-time filtering of PS5 files (v2.0)
|
||||
✅ **Favorites/Bookmarks** - Quick navigation to saved paths (v2.0)
|
||||
✅ **Multi-PS5 Support** - Save and switch between multiple PS5 profiles (v2.0)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
| Network | Expected Speed |
|
||||
|---------|----------------|
|
||||
| **Gigabit Ethernet** | 100-120 MB/s |
|
||||
| **WiFi 6 (5GHz)** | 60-80 MB/s |
|
||||
| **WiFi 5 (5GHz)** | 40-60 MB/s |
|
||||
### v3.1.0 Optimized Results:
|
||||
- ✅ **104+ MB/s** upload speed for large files (parallel chunked uploads)
|
||||
- ✅ **4 parallel chunk connections** for maximum throughput
|
||||
- ✅ **500MB chunks** - optimal size for PS5 disk performance
|
||||
- ✅ **Real-time progress** - Speed/ETA updates during upload
|
||||
- ✅ **Zero chunk failures** - Fixed race conditions
|
||||
- ✅ **Fully responsive UI** throughout upload
|
||||
- ✅ **No memory leaks** - stable operation
|
||||
|
||||
**Note:** Speeds depend on network quality and PS5 disk performance.
|
||||
### Previous Results (v1.3.0):
|
||||
- ✅ **42,801 files** uploaded successfully
|
||||
- ✅ **60-150 MB/s** sustained throughput
|
||||
|
||||
---
|
||||
| Network | Expected Upload Speed | Expected Download Speed |
|
||||
|---------|----------------------|------------------------|
|
||||
| **Gigabit Ethernet** | 88-110 MB/s (aggregate) | 100-120 MB/s |
|
||||
| **WiFi 6 (5GHz)** | 32-70 MB/s (aggregate) | 60-80 MB/s |
|
||||
| **WiFi 5 (5GHz)** | 20-50 MB/s (aggregate) | 40-60 MB/s |
|
||||
|
||||
## 🔧 Building from Source
|
||||
**Note:** Upload speeds depend on network quality and PS5 disk write performance. WiFi has ~40-60% overhead compared to Ethernet.
|
||||
|
||||
### PS5 Payload
|
||||
|
||||
Requirements:
|
||||
- PS5 SDK (prospero-clang)
|
||||
- WSL or Linux
|
||||
|
||||
```bash
|
||||
cd payload
|
||||
bash compile.sh
|
||||
```
|
||||
|
||||
### Windows Client
|
||||
|
||||
Requirements:
|
||||
- .NET 6.0 SDK or later
|
||||
- Windows 10/11
|
||||
|
||||
```cmd
|
||||
cd client
|
||||
build.bat
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```cmd
|
||||
dotnet publish -c Release -r win-x64 --self-contained true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Protocol Specification
|
||||
|
||||
See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
|
||||
|
||||
### Commands
|
||||
- `PING` - Test connection
|
||||
- `LIST_STORAGE` - Get storage info
|
||||
- `LIST_DIR` - List directory contents
|
||||
- `CREATE_DIR` - Create directory
|
||||
- `DELETE_FILE` - Delete file
|
||||
- `DELETE_DIR` - Delete directory
|
||||
- `START_UPLOAD` - Begin file upload
|
||||
- `UPLOAD_CHUNK` - Upload file chunk
|
||||
- `END_UPLOAD` - Finish upload
|
||||
- `SHUTDOWN` - Shutdown server
|
||||
|
||||
---
|
||||
|
||||
@@ -174,9 +150,156 @@ See [PROTOCOL.md](PROTOCOL.md) for detailed protocol documentation.
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.2.0
|
||||
|
||||
### 🎯 Improvements:
|
||||
|
||||
#### 1. 💾 Accurate Storage Display
|
||||
- **Storage now matches PS5 UI** - Shows ~848 GB instead of raw 872 GB
|
||||
- **Accounts for reserved space** - Same calculation as PS5 system
|
||||
|
||||
#### 2. 📱 Mobile App Improvements
|
||||
- **Multi-select mode** - Toggle button to select multiple files/folders
|
||||
- **Folder browser** - Visual folder picker for Copy/Move destinations
|
||||
- **Sorted file list** - Folders always displayed at top
|
||||
- **Exit confirmation** - Prompt when pressing back button
|
||||
- **Delete folders** - Now supports folder deletion (not just files)
|
||||
- **Batch operations** - Delete/download multiple items at once
|
||||
|
||||
#### 3. 🐛 Bug Fixes
|
||||
- **Fixed empty folder deletion** - No more "Unexpected response" error
|
||||
- **Fixed Copy/Move crash** - No longer crashes when selecting destination
|
||||
- **Fixed multi-select count** - Correct item count in delete confirmation
|
||||
- **Fixed folder navigation** - Can enter folders after actions complete
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.1.0
|
||||
|
||||
### 🚀 Performance Improvements:
|
||||
|
||||
#### 1. ⚡ Parallel Chunked Uploads (104 MB/s!)
|
||||
- **Large files (>100MB)** are now split into 500MB chunks
|
||||
- **4 parallel connections** upload chunks simultaneously
|
||||
- **Result:** ~104 MB/s upload speed (2x faster than before!)
|
||||
- **Fixed race condition:** First chunk creates file before others start
|
||||
- **Fixed closure bug:** Correct offset/size capture for parallel tasks
|
||||
|
||||
#### 2. 📊 Real-time Progress Display
|
||||
- **Speed display** now updates in real-time during chunked uploads
|
||||
- **ETA display** shows accurate time remaining
|
||||
- **Progress bar** correctly shows 0-100% for chunked files
|
||||
- No more speed dropping to 0 during large file uploads
|
||||
|
||||
#### 3. 💾 Storage Display Improvements
|
||||
- **Changed label** to "Data Storage (/data)" for clarity
|
||||
- **Uses statfs with f_bavail** for more accurate available space
|
||||
- **Real-time updates** every 5 seconds when connected
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v3.0.0
|
||||
|
||||
### 🐛 Critical Bug Fixes:
|
||||
|
||||
#### 1. 🔧 Path Normalization Fix
|
||||
- **Fixed:** Double-slash paths (`//mnt/ext1/...`) that caused directory creation to fail
|
||||
- **Affected:** All file operations (upload, download, delete, rename, copy, move)
|
||||
- **Solution:** Added `NormalizePath()` function in both client and server
|
||||
- **Result:** 100% reliable directory creation and file uploads
|
||||
|
||||
#### 2. 📱 Android Mobile Client
|
||||
- **NEW:** Full-featured Android app for PS5 file management
|
||||
- **Multi-PS5 Profiles** - Save and switch between multiple PS5 consoles
|
||||
- Upload files from phone to PS5
|
||||
- Download files from PS5 with Share option
|
||||
- Browse PS5 filesystem
|
||||
- Favorites for quick navigation
|
||||
- Debug Log with Copy to clipboard
|
||||
- Transfer History tracking
|
||||
|
||||
#### 3. 🛠️ Server Improvements
|
||||
- Path normalization in all handler functions
|
||||
- More robust error handling
|
||||
- Improved stability for parallel uploads
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v2.1.0
|
||||
|
||||
### 🚀 Performance Optimizations:
|
||||
|
||||
#### 1. ⚡ Massive Upload Speed Boost (88-110 MB/s)
|
||||
- **Server-side:** Replaced `fwrite()` with direct `write()` syscalls
|
||||
- **Client-side:** 8 parallel large file uploads (optimal for PS5 disk)
|
||||
- **Result:** 80-110 MB/s aggregate upload speed on Gigabit Ethernet
|
||||
- **Peak bursts:** Up to 2.05 GB/s when hitting disk cache
|
||||
- **Per-file:** 11-14 MB/s sustained per connection
|
||||
|
||||
#### 2. 📊 Transfer History
|
||||
- Complete history of all uploads and downloads
|
||||
- Success/Failed status tracking with error messages
|
||||
- Speed statistics (average, min, max)
|
||||
- Export to CSV/JSON for analysis
|
||||
- Persistent storage across sessions
|
||||
|
||||
#### 3. 🔄 Auto-Clear History on Startup
|
||||
- Optional checkbox to clear history automatically
|
||||
- Useful for keeping UI clean between sessions
|
||||
- Setting saved in `ps5_upload_settings.json`
|
||||
|
||||
#### 4. 🖥️ Maximized Window UI
|
||||
- Application opens in full-screen mode by default
|
||||
- Better visibility for large file transfers
|
||||
- Can be resized/restored as needed
|
||||
|
||||
### Technical Improvements:
|
||||
- ✅ **16MB socket buffers** (up from 4MB) for maximum throughput
|
||||
- ✅ **Per-file mutex locking** - Parallel writes without race conditions
|
||||
- ✅ **File pre-allocation** - Reduces disk fragmentation for large files
|
||||
- ✅ **Direct syscalls** - Bypasses stdio buffering overhead
|
||||
|
||||
---
|
||||
|
||||
## 📝 What's New in v2.0.0
|
||||
|
||||
### 🎉 4 Major New Features:
|
||||
|
||||
#### 1. 📥 Download Files (PS5 → PC)
|
||||
- Right-click any file → "⬇️ Download to PC"
|
||||
- Save file dialog for destination selection
|
||||
- Real-time progress tracking with speed display
|
||||
- Optimized with sendfile for maximum speed
|
||||
|
||||
#### 2. 🔍 File Search
|
||||
- Search box in PS5 Files panel
|
||||
- Real-time filtering as you type
|
||||
- Case-insensitive search
|
||||
- Quick "Clear" button to reset
|
||||
|
||||
#### 3. ⭐ Favorites/Bookmarks
|
||||
- Save frequently used PS5 paths
|
||||
- Quick dropdown navigation
|
||||
- Add/Remove favorite paths
|
||||
- Persistent storage in JSON
|
||||
|
||||
#### 4. 🎮 Multi-PS5 Support
|
||||
- Save multiple PS5 profiles (IP + name)
|
||||
- Quick switch between different PS5 consoles
|
||||
- Dropdown profile selector
|
||||
- Persistent profile storage
|
||||
|
||||
### Previous Stability (v1.3.0):
|
||||
- ✅ **Zero connection drops** - 5 minute socket timeout + aggressive keepalive
|
||||
- ✅ **Fully responsive UI** - Async updates + throttled logging
|
||||
- ✅ **No memory leaks** - Proper resource disposal
|
||||
- ✅ **Tested:** 42,801 files uploaded with 100% success rate
|
||||
|
||||
---
|
||||
|
||||
## 📝 License
|
||||
|
||||
MIT License - Free to use and modify
|
||||
This software is provided as-is for personal use.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<Application x:Class="PS5Upload.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -1,8 +0,0 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace PS5Upload
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
<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"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="#1E1E1E"
|
||||
Icon="ps5_logo.png">
|
||||
|
||||
<Window.Resources>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="#007ACC"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="15,8"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="4"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#1C97EA"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Background" Value="#3E3E42"/>
|
||||
<Setter Property="Foreground" Value="#666666"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="#2D2D30"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#3E3E42"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="8"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListBox">
|
||||
<Setter Property="Background" Value="#2D2D30"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="BorderBrush" Value="#3E3E42"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</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"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Connection Panel -->
|
||||
<Border Grid.Row="1"
|
||||
Background="#252526"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Padding="15"
|
||||
Margin="0,0,0,15">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="PS5 IP Address:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
|
||||
<TextBox Grid.Column="1"
|
||||
x:Name="IpAddressTextBox"
|
||||
Text="192.168.0.160"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
x:Name="ConnectButton"
|
||||
Content="Connect"
|
||||
Click="ConnectButton_Click"
|
||||
Width="100"
|
||||
Margin="0,0,10,0"/>
|
||||
|
||||
<StackPanel Grid.Column="3"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse x:Name="StatusIndicator"
|
||||
Width="12"
|
||||
Height="12"
|
||||
Fill="#DC3545"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock x:Name="StatusText"
|
||||
Text="Disconnected"
|
||||
Foreground="#999999"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Main Content -->
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="15"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="15"/>
|
||||
<ColumnDefinition Width="300"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Local Files -->
|
||||
<Border Grid.Column="0"
|
||||
Background="#252526"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0"
|
||||
Background="#2D2D30"
|
||||
Padding="10"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="0,0,0,1">
|
||||
<TextBlock Text="📁 Local Files"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
x:Name="LocalFilesListBox"
|
||||
BorderThickness="0"
|
||||
AllowDrop="True"
|
||||
Drop="LocalFilesListBox_Drop"
|
||||
DragEnter="LocalFilesListBox_DragEnter"
|
||||
MouseRightButtonUp="LocalFilesListBox_RightClick">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="{Binding Icon}"
|
||||
FontSize="16"
|
||||
Margin="0,0,10,0"/>
|
||||
<TextBlock Text="{Binding Name}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="🗑️ Remove" Click="RemoveLocalFile_Click"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
Margin="10"
|
||||
HorizontalAlignment="Center">
|
||||
<Button Content="Browse Files"
|
||||
Click="BrowseFilesButton_Click"
|
||||
Margin="0,0,5,0"/>
|
||||
<Button Content="Browse Folder"
|
||||
Click="BrowseFolderButton_Click"
|
||||
Margin="0,0,5,0"/>
|
||||
<Button Content="Clear All"
|
||||
Click="ClearLocalFiles_Click"
|
||||
Background="#DC3545"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- PS5 Files -->
|
||||
<Border Grid.Column="2"
|
||||
Background="#252526"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0"
|
||||
Background="#2D2D30"
|
||||
Padding="10"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="0,0,0,1">
|
||||
<TextBlock Text="🎮 PS5 Files"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
Margin="10"
|
||||
Background="#2D2D30">
|
||||
<TextBlock Text="Current Path:"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<TextBox x:Name="CurrentPathTextBox"
|
||||
Text="/data"
|
||||
Width="200"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,10,0"/>
|
||||
<Button Content="Go"
|
||||
Click="GoButton_Click"
|
||||
Width="60"/>
|
||||
</StackPanel>
|
||||
|
||||
<ListBox Grid.Row="2"
|
||||
x:Name="PS5FilesListBox"
|
||||
BorderThickness="0"
|
||||
MouseDoubleClick="PS5FilesListBox_MouseDoubleClick">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="📝 Rename" Click="RenameMenuItem_Click"/>
|
||||
<MenuItem Header="📋 Copy" Click="CopyMenuItem_Click"/>
|
||||
<MenuItem Header="✂️ Move" Click="MoveMenuItem_Click"/>
|
||||
<Separator/>
|
||||
<MenuItem Header="🗑️ Delete" Click="DeleteMenuItem_Click"/>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="5">
|
||||
<TextBlock Text="{Binding Icon}"
|
||||
FontSize="16"
|
||||
Margin="0,0,10,0"/>
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding SizeText}"
|
||||
FontSize="11"
|
||||
Foreground="#999999"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="3"
|
||||
Margin="10">
|
||||
<TextBlock x:Name="StorageInfoText"
|
||||
Text="Storage: --"
|
||||
FontSize="12"
|
||||
Foreground="#999999"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Log Panel -->
|
||||
<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>
|
||||
|
||||
<Border Grid.Row="0"
|
||||
Background="#2D2D30"
|
||||
Padding="10"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="0,0,0,1">
|
||||
<TextBlock Text="📋 Debug Log"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
|
||||
<TextBox Grid.Row="1"
|
||||
x:Name="LogTextBox"
|
||||
Text=""
|
||||
FontFamily="Consolas"
|
||||
FontSize="11"
|
||||
Foreground="#CCCCCC"
|
||||
Background="#1E1E1E"
|
||||
BorderThickness="0"
|
||||
Padding="10"
|
||||
TextWrapping="Wrap"
|
||||
IsReadOnly="True"
|
||||
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>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<Border Grid.Row="3"
|
||||
Background="#252526"
|
||||
BorderBrush="#3E3E42"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Padding="15"
|
||||
Margin="0,15,0,0"
|
||||
x:Name="ProgressPanel"
|
||||
Visibility="Collapsed">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Files Remaining Counter -->
|
||||
<TextBlock Grid.Row="0"
|
||||
x:Name="FilesRemainingText"
|
||||
Text="Files: 0 / 0 (0 remaining)"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFC107"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<!-- Total Folder Progress -->
|
||||
<TextBlock Grid.Row="1"
|
||||
x:Name="TotalProgressText"
|
||||
Text="Total: 0 / 0 files"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#28A745"
|
||||
Margin="0,0,0,5"/>
|
||||
|
||||
<ProgressBar Grid.Row="2"
|
||||
x:Name="TotalProgressBar"
|
||||
Height="20"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="0"
|
||||
Foreground="#17A2B8"
|
||||
Background="#2D2D30"
|
||||
BorderBrush="#3E3E42"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
<!-- Current File -->
|
||||
<TextBlock Grid.Row="3"
|
||||
x:Name="UploadFileNameText"
|
||||
Text="Uploading: filename.bin"
|
||||
FontSize="13"
|
||||
Margin="0,0,0,5"/>
|
||||
|
||||
<ProgressBar Grid.Row="4"
|
||||
x:Name="UploadProgressBar"
|
||||
Height="20"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="0"
|
||||
Foreground="#28A745"
|
||||
Background="#2D2D30"
|
||||
BorderBrush="#3E3E42"/>
|
||||
|
||||
<!-- Speed and Progress -->
|
||||
<Grid Grid.Row="5" Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0"
|
||||
x:Name="UploadSpeedText"
|
||||
Text="Speed: 0 MB/s | Avg: 0 MB/s"
|
||||
FontSize="12"
|
||||
Foreground="#999999"/>
|
||||
|
||||
<TextBlock Grid.Column="1"
|
||||
x:Name="UploadProgressText"
|
||||
Text="0 / 0 MB (0%)"
|
||||
FontSize="12"
|
||||
Foreground="#999999"/>
|
||||
</Grid>
|
||||
|
||||
<!-- ETA -->
|
||||
<TextBlock Grid.Row="6"
|
||||
x:Name="UploadETAText"
|
||||
Text="ETA: Calculating..."
|
||||
FontSize="12"
|
||||
Foreground="#999999"
|
||||
Margin="0,5,0,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Upload Buttons -->
|
||||
<Grid Grid.Row="4" Margin="0,15,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="10"/>
|
||||
<ColumnDefinition Width="150"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button Grid.Column="0"
|
||||
x:Name="UploadButton"
|
||||
Content="⬆️ Upload to PS5"
|
||||
Click="UploadButton_Click"
|
||||
Height="45"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
IsEnabled="False"/>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
x:Name="CancelButton"
|
||||
Content="❌ Cancel"
|
||||
Click="CancelButton_Click"
|
||||
Height="45"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Background="#DC3545"
|
||||
IsEnabled="False"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,29 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AssemblyName>PS5Upload</AssemblyName>
|
||||
<RootNamespace>PS5Upload</RootNamespace>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Manos</Authors>
|
||||
<Description>High-Speed File Upload Tool for PS5</Description>
|
||||
<Copyright>Copyright © 2026 Manos</Copyright>
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<ApplicationIcon>ps5_logo_fixed.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="ps5_logo.png" />
|
||||
<None Include="ps5_logo.ico">
|
||||
<Link>ps5_logo.ico</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,453 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PS5Upload
|
||||
{
|
||||
// Protocol commands
|
||||
public enum Command : byte
|
||||
{
|
||||
Ping = 0x01,
|
||||
ListStorage = 0x02,
|
||||
ListDir = 0x03,
|
||||
CreateDir = 0x04,
|
||||
DeleteFile = 0x05,
|
||||
DeleteDir = 0x06,
|
||||
Rename = 0x07,
|
||||
CopyFile = 0x08,
|
||||
MoveFile = 0x09,
|
||||
StartUpload = 0x10,
|
||||
UploadChunk = 0x11,
|
||||
EndUpload = 0x12,
|
||||
Shutdown = 0xFF
|
||||
}
|
||||
|
||||
// Protocol responses
|
||||
public enum Response : byte
|
||||
{
|
||||
Ok = 0x01,
|
||||
Error = 0x02,
|
||||
Data = 0x03,
|
||||
Ready = 0x04,
|
||||
Progress = 0x05
|
||||
}
|
||||
|
||||
public class PS5Protocol : IDisposable
|
||||
{
|
||||
private TcpClient? _client;
|
||||
private NetworkStream? _stream;
|
||||
private const int BufferSize = 32 * 1024 * 1024; // 32MB for maximum throughput
|
||||
|
||||
public bool IsConnected => _client?.Connected ?? false;
|
||||
|
||||
public async Task<bool> ConnectAsync(string ipAddress, int port = 9113)
|
||||
{
|
||||
try
|
||||
{
|
||||
_client = new TcpClient();
|
||||
_client.ReceiveBufferSize = 64 * 1024 * 1024; // 64MB for maximum TCP throughput
|
||||
_client.SendBufferSize = 64 * 1024 * 1024; // 64MB for maximum TCP throughput
|
||||
_client.NoDelay = true;
|
||||
_client.LingerState = new System.Net.Sockets.LingerOption(false, 0);
|
||||
|
||||
await _client.ConnectAsync(ipAddress, port);
|
||||
_stream = _client.GetStream();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_stream?.Close();
|
||||
_client?.Close();
|
||||
_stream = null;
|
||||
_client = null;
|
||||
}
|
||||
|
||||
private async Task SendCommandAsync(Command cmd, byte[]? data = null)
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
|
||||
byte[] header = new byte[5];
|
||||
header[0] = (byte)cmd;
|
||||
|
||||
uint dataLen = data != null ? (uint)data.Length : 0;
|
||||
BitConverter.GetBytes(dataLen).CopyTo(header, 1);
|
||||
|
||||
await _stream.WriteAsync(header, 0, 5);
|
||||
if (data != null && data.Length > 0)
|
||||
{
|
||||
await _stream.WriteAsync(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(Response response, byte[] data)> ReceiveResponseAsync()
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
|
||||
byte[] header = new byte[5];
|
||||
await ReadExactAsync(header, 5);
|
||||
|
||||
Response response = (Response)header[0];
|
||||
uint dataLen = BitConverter.ToUInt32(header, 1);
|
||||
|
||||
byte[] data = new byte[dataLen];
|
||||
if (dataLen > 0)
|
||||
{
|
||||
await ReadExactAsync(data, (int)dataLen);
|
||||
}
|
||||
|
||||
return (response, data);
|
||||
}
|
||||
|
||||
private async Task ReadExactAsync(byte[] buffer, int count, int timeoutMs = 30000)
|
||||
{
|
||||
if (_stream == null) throw new InvalidOperationException("Not connected");
|
||||
|
||||
int offset = 0;
|
||||
using var cts = new CancellationTokenSource(timeoutMs);
|
||||
|
||||
try
|
||||
{
|
||||
while (offset < count)
|
||||
{
|
||||
int read = await _stream.ReadAsync(buffer, offset, count - offset, cts.Token);
|
||||
if (read == 0) throw new IOException("Connection closed");
|
||||
offset += read;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw new TimeoutException("Read timeout");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PingAsync()
|
||||
{
|
||||
await SendCommandAsync(Command.Ping);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<StorageInfo[]> ListStorageAsync()
|
||||
{
|
||||
await SendCommandAsync(Command.ListStorage);
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
|
||||
if (response != Response.Data) return Array.Empty<StorageInfo>();
|
||||
|
||||
using var ms = new MemoryStream(data);
|
||||
using var br = new BinaryReader(ms);
|
||||
|
||||
int count = br.ReadInt32();
|
||||
var result = new StorageInfo[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ushort pathLen = br.ReadUInt16();
|
||||
string path = Encoding.UTF8.GetString(br.ReadBytes(pathLen));
|
||||
long total = br.ReadInt64();
|
||||
long free = br.ReadInt64();
|
||||
|
||||
result[i] = new StorageInfo { Path = path, TotalBytes = total, FreeBytes = free };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<FileEntry[]> ListDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.ListDir, pathBytes);
|
||||
var (response, data) = await ReceiveResponseAsync();
|
||||
|
||||
if (response != Response.Data) return Array.Empty<FileEntry>();
|
||||
|
||||
using var ms = new MemoryStream(data);
|
||||
using var br = new BinaryReader(ms);
|
||||
|
||||
int count = br.ReadInt32();
|
||||
|
||||
// Handle empty directory
|
||||
if (count <= 0) return Array.Empty<FileEntry>();
|
||||
|
||||
var result = new FileEntry[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
byte type = br.ReadByte();
|
||||
ushort nameLen = br.ReadUInt16();
|
||||
string name = Encoding.UTF8.GetString(br.ReadBytes(nameLen));
|
||||
long size = br.ReadInt64();
|
||||
long timestamp = br.ReadInt64();
|
||||
|
||||
// Clamp timestamp to valid DateTimeOffset range
|
||||
DateTime dt;
|
||||
try
|
||||
{
|
||||
if (timestamp < -62135596800 || timestamp > 253402300799)
|
||||
{
|
||||
dt = DateTime.Now; // Use current time for invalid timestamps
|
||||
}
|
||||
else
|
||||
{
|
||||
dt = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
dt = DateTime.Now;
|
||||
}
|
||||
|
||||
result[i] = new FileEntry
|
||||
{
|
||||
Name = name,
|
||||
IsDirectory = type == 1,
|
||||
Size = size,
|
||||
Timestamp = dt
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> CreateDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.CreateDir, pathBytes);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteFileAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.DeleteFile, pathBytes);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteDirAsync(string path)
|
||||
{
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(path + "\0");
|
||||
await SendCommandAsync(Command.DeleteDir, pathBytes);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> RenameAsync(string oldPath, string newPath)
|
||||
{
|
||||
byte[] oldBytes = Encoding.UTF8.GetBytes(oldPath + "\0");
|
||||
byte[] newBytes = Encoding.UTF8.GetBytes(newPath + "\0");
|
||||
byte[] data = new byte[oldBytes.Length + newBytes.Length];
|
||||
Array.Copy(oldBytes, 0, data, 0, oldBytes.Length);
|
||||
Array.Copy(newBytes, 0, data, oldBytes.Length, newBytes.Length);
|
||||
|
||||
await SendCommandAsync(Command.Rename, data);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> CopyFileAsync(string srcPath, string dstPath)
|
||||
{
|
||||
byte[] srcBytes = Encoding.UTF8.GetBytes(srcPath + "\0");
|
||||
byte[] dstBytes = Encoding.UTF8.GetBytes(dstPath + "\0");
|
||||
byte[] data = new byte[srcBytes.Length + dstBytes.Length];
|
||||
Array.Copy(srcBytes, 0, data, 0, srcBytes.Length);
|
||||
Array.Copy(dstBytes, 0, data, srcBytes.Length, dstBytes.Length);
|
||||
|
||||
await SendCommandAsync(Command.CopyFile, data);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> MoveFileAsync(string srcPath, string dstPath)
|
||||
{
|
||||
byte[] srcBytes = Encoding.UTF8.GetBytes(srcPath + "\0");
|
||||
byte[] dstBytes = Encoding.UTF8.GetBytes(dstPath + "\0");
|
||||
byte[] data = new byte[srcBytes.Length + dstBytes.Length];
|
||||
Array.Copy(srcBytes, 0, data, 0, srcBytes.Length);
|
||||
Array.Copy(dstBytes, 0, data, srcBytes.Length, dstBytes.Length);
|
||||
|
||||
await SendCommandAsync(Command.MoveFile, data);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
return response == Response.Ok;
|
||||
}
|
||||
|
||||
public async Task<bool> UploadFileAsync(string localPath, string remotePath, IProgress<UploadProgress>? progress = null, CancellationToken cancellationToken = default, long chunkOffset = 0, long chunkSize = 0)
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(localPath);
|
||||
if (!fileInfo.Exists) return false;
|
||||
|
||||
// Determine actual upload size
|
||||
long uploadSize = chunkSize > 0 ? chunkSize : fileInfo.Length;
|
||||
|
||||
// Send START_UPLOAD with optional chunk offset
|
||||
byte[] pathBytes = Encoding.UTF8.GetBytes(remotePath);
|
||||
byte[] startData = new byte[pathBytes.Length + 1 + 8 + 8]; // path + null + size + offset
|
||||
Array.Copy(pathBytes, 0, startData, 0, pathBytes.Length);
|
||||
BitConverter.GetBytes(fileInfo.Length).CopyTo(startData, pathBytes.Length + 1);
|
||||
BitConverter.GetBytes(chunkOffset).CopyTo(startData, pathBytes.Length + 9);
|
||||
|
||||
await SendCommandAsync(Command.StartUpload, startData);
|
||||
var (response, _) = await ReceiveResponseAsync();
|
||||
|
||||
if (response != Response.Ready) return false;
|
||||
|
||||
// Upload chunks - simple async approach
|
||||
long totalSent = 0;
|
||||
var startTime = DateTime.Now;
|
||||
double avgSpeed = 0;
|
||||
|
||||
byte[] sendBuffer = new byte[5 + BufferSize];
|
||||
sendBuffer[0] = (byte)Command.UploadChunk;
|
||||
|
||||
using (FileStream fs = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, FileOptions.SequentialScan))
|
||||
{
|
||||
// Seek to chunk offset if specified
|
||||
if (chunkOffset > 0)
|
||||
{
|
||||
fs.Seek(chunkOffset, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
int chunksSent = 0;
|
||||
int bytesRead;
|
||||
long bytesRemaining = uploadSize;
|
||||
|
||||
while (bytesRemaining > 0 && (bytesRead = await fs.ReadAsync(sendBuffer, 5, (int)Math.Min(BufferSize, bytesRemaining), cancellationToken)) > 0)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested) return false;
|
||||
|
||||
BitConverter.GetBytes((uint)bytesRead).CopyTo(sendBuffer, 1);
|
||||
|
||||
if (_stream == null) return false;
|
||||
|
||||
await _stream.WriteAsync(sendBuffer, 0, 5 + bytesRead, cancellationToken);
|
||||
|
||||
totalSent += bytesRead;
|
||||
bytesRemaining -= bytesRead;
|
||||
chunksSent++;
|
||||
|
||||
if (chunksSent % 5 == 0 || bytesRemaining == 0)
|
||||
{
|
||||
var elapsed = DateTime.Now - startTime;
|
||||
double currentSpeed = elapsed.TotalSeconds > 0 ? totalSent / elapsed.TotalSeconds : 0;
|
||||
avgSpeed = currentSpeed;
|
||||
|
||||
TimeSpan eta = currentSpeed > 0
|
||||
? TimeSpan.FromSeconds(bytesRemaining / currentSpeed)
|
||||
: TimeSpan.Zero;
|
||||
|
||||
progress?.Report(new UploadProgress
|
||||
{
|
||||
BytesSent = chunkOffset + totalSent,
|
||||
TotalBytes = fileInfo.Length,
|
||||
SpeedBytesPerSecond = currentSpeed,
|
||||
AverageSpeedBytesPerSecond = avgSpeed,
|
||||
ElapsedTime = elapsed,
|
||||
EstimatedTimeRemaining = eta,
|
||||
CurrentFileName = fileInfo.Name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send END_UPLOAD - fire-and-forget for maximum speed
|
||||
// Don't wait for response to avoid PS5 payload bottleneck
|
||||
try
|
||||
{
|
||||
await SendCommandAsync(Command.EndUpload);
|
||||
// Immediately return success without waiting for response
|
||||
// This eliminates the PS5 response handling bottleneck
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // Assume success if connection issues
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
public class StorageInfo
|
||||
{
|
||||
public string Path { get; set; } = "";
|
||||
public long TotalBytes { get; set; }
|
||||
public long FreeBytes { get; set; }
|
||||
}
|
||||
|
||||
public class FileEntry
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public bool IsDirectory { get; set; }
|
||||
public long Size { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
|
||||
public class UploadProgress
|
||||
{
|
||||
public long BytesSent { get; set; }
|
||||
public long TotalBytes { get; set; }
|
||||
public double SpeedBytesPerSecond { get; set; }
|
||||
public double AverageSpeedBytesPerSecond { get; set; }
|
||||
public TimeSpan ElapsedTime { get; set; }
|
||||
public TimeSpan EstimatedTimeRemaining { get; set; }
|
||||
public string CurrentFileName { get; set; } = "";
|
||||
|
||||
// For folder progress
|
||||
public int CurrentFileIndex { get; set; }
|
||||
public int TotalFiles { get; set; }
|
||||
public long TotalFolderBytes { get; set; }
|
||||
public long TotalFolderBytesSent { get; set; }
|
||||
}
|
||||
|
||||
// High-speed parallel uploader using multiple connections
|
||||
public class ParallelUploader : IDisposable
|
||||
{
|
||||
private readonly string _ipAddress;
|
||||
private readonly int _port;
|
||||
private readonly int _connectionCount;
|
||||
private const int ChunkSize = 4 * 1024 * 1024; // 4MB chunks per connection
|
||||
|
||||
public ParallelUploader(string ipAddress, int port = 9113, int connectionCount = 4)
|
||||
{
|
||||
_ipAddress = ipAddress;
|
||||
_port = port;
|
||||
_connectionCount = connectionCount;
|
||||
}
|
||||
|
||||
public async Task<bool> UploadFileAsync(string localPath, string remotePath, IProgress<UploadProgress>? progress = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(localPath);
|
||||
if (!fileInfo.Exists) return false;
|
||||
|
||||
// For small files, use single connection
|
||||
if (fileInfo.Length < ChunkSize * 2)
|
||||
{
|
||||
using var protocol = new PS5Protocol();
|
||||
if (!await protocol.ConnectAsync(_ipAddress, _port)) return false;
|
||||
return await protocol.UploadFileAsync(localPath, remotePath, progress, cancellationToken);
|
||||
}
|
||||
|
||||
// For large files, use parallel connections
|
||||
// This requires server support for chunked/parallel uploads
|
||||
// For now, fall back to single connection
|
||||
using var proto = new PS5Protocol();
|
||||
if (!await proto.ConnectAsync(_ipAddress, _port)) return false;
|
||||
return await proto.UploadFileAsync(localPath, remotePath, progress, cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
@echo off
|
||||
echo ================================================
|
||||
echo PS5 Upload Client - Building
|
||||
echo By Manos
|
||||
echo ================================================
|
||||
echo.
|
||||
|
||||
echo [+] Building Windows GUI Client...
|
||||
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true
|
||||
|
||||
if exist "bin\Release\net6.0-windows\win-x64\publish\PS5Upload.exe" (
|
||||
echo.
|
||||
echo ================================================
|
||||
echo [+] SUCCESS! Built PS5Upload.exe
|
||||
echo ================================================
|
||||
echo.
|
||||
echo Location: bin\Release\net6.0-windows\win-x64\publish\PS5Upload.exe
|
||||
echo.
|
||||
echo Copy PS5Upload.exe to any location and run it!
|
||||
) else (
|
||||
echo.
|
||||
echo ================================================
|
||||
echo [-] BUILD FAILED
|
||||
echo ================================================
|
||||
echo.
|
||||
echo Make sure .NET 6.0 SDK is installed:
|
||||
echo https://dotnet.microsoft.com/download/dotnet/6.0
|
||||
exit /b 1
|
||||
)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "================================================"
|
||||
echo "PS5 Upload Server - Compilation"
|
||||
echo "By Manos"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
echo "[+] Compiling..."
|
||||
rm -f ps5_upload_server.elf
|
||||
/home/hackman/ps5sdk_copy/bin/prospero-clang -Wall -O3 -pthread -o ps5_upload_server.elf main.c
|
||||
|
||||
if [ -f "ps5_upload_server.elf" ]; then
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "[+] SUCCESS! Compiled ps5_upload_server.elf"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
ls -lh ps5_upload_server.elf
|
||||
file ps5_upload_server.elf
|
||||
echo ""
|
||||
echo "Upload to PS5 and run with elfldr"
|
||||
echo "Server will listen on port 9113"
|
||||
else
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "[-] COMPILATION FAILED"
|
||||
echo "================================================"
|
||||
exit 1
|
||||
fi
|
||||
-875
@@ -1,875 +0,0 @@
|
||||
/* PS5 Upload Server - Custom High-Speed Protocol
|
||||
* By Manos
|
||||
* Port: 9113
|
||||
* Protocol: Custom binary for maximum speed
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statvfs.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#define SERVER_PORT 9113
|
||||
#define BUFFER_SIZE (8 * 1024 * 1024)
|
||||
#define MAX_PATH 2048
|
||||
#define DISK_WORKER_COUNT 4
|
||||
#define QUEUE_MAX_SIZE 32
|
||||
|
||||
// Protocol commands
|
||||
#define CMD_PING 0x01
|
||||
#define CMD_LIST_STORAGE 0x02
|
||||
#define CMD_LIST_DIR 0x03
|
||||
#define CMD_CREATE_DIR 0x04
|
||||
#define CMD_DELETE_FILE 0x05
|
||||
#define CMD_DELETE_DIR 0x06
|
||||
#define CMD_RENAME 0x07
|
||||
#define CMD_COPY_FILE 0x08
|
||||
#define CMD_MOVE_FILE 0x09
|
||||
#define CMD_START_UPLOAD 0x10
|
||||
#define CMD_UPLOAD_CHUNK 0x11
|
||||
#define CMD_END_UPLOAD 0x12
|
||||
#define CMD_SHUTDOWN 0xFF
|
||||
|
||||
// Protocol responses
|
||||
#define RESP_OK 0x01
|
||||
#define RESP_ERROR 0x02
|
||||
#define RESP_DATA 0x03
|
||||
#define RESP_READY 0x04
|
||||
#define RESP_PROGRESS 0x05
|
||||
|
||||
typedef struct notify_request {
|
||||
char useless1[45];
|
||||
char message[3075];
|
||||
} notify_request_t;
|
||||
|
||||
int sceKernelSendNotificationRequest(int, notify_request_t*, size_t, int);
|
||||
|
||||
void send_notification(const char *msg) {
|
||||
notify_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
strncpy(req.message, msg, sizeof(req.message) - 1);
|
||||
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int sock;
|
||||
FILE *upload_fp;
|
||||
char upload_path[MAX_PATH];
|
||||
uint64_t upload_size;
|
||||
uint64_t upload_received;
|
||||
} client_session_t;
|
||||
|
||||
// Disk write job for queue
|
||||
typedef struct write_job {
|
||||
uint8_t *data;
|
||||
size_t len;
|
||||
FILE *fp;
|
||||
struct write_job *next;
|
||||
} write_job_t;
|
||||
|
||||
// Job queue (producer-consumer pattern)
|
||||
typedef struct {
|
||||
write_job_t *head;
|
||||
write_job_t *tail;
|
||||
size_t count;
|
||||
size_t max;
|
||||
int closed;
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t not_empty;
|
||||
pthread_cond_t not_full;
|
||||
} job_queue_t;
|
||||
|
||||
// Global queue and workers
|
||||
static job_queue_t g_queue;
|
||||
static pthread_t g_workers[DISK_WORKER_COUNT];
|
||||
static int g_workers_initialized = 0;
|
||||
|
||||
// Queue operations
|
||||
void queue_init(job_queue_t *q, size_t max) {
|
||||
memset(q, 0, sizeof(*q));
|
||||
q->max = max;
|
||||
pthread_mutex_init(&q->mutex, NULL);
|
||||
pthread_cond_init(&q->not_empty, NULL);
|
||||
pthread_cond_init(&q->not_full, NULL);
|
||||
}
|
||||
|
||||
int queue_push(job_queue_t *q, write_job_t *job) {
|
||||
pthread_mutex_lock(&q->mutex);
|
||||
while (!q->closed && q->count >= q->max) {
|
||||
pthread_cond_wait(&q->not_full, &q->mutex);
|
||||
}
|
||||
if (q->closed) {
|
||||
pthread_mutex_unlock(&q->mutex);
|
||||
return -1;
|
||||
}
|
||||
job->next = NULL;
|
||||
if (!q->tail) {
|
||||
q->head = job;
|
||||
q->tail = job;
|
||||
} else {
|
||||
q->tail->next = job;
|
||||
q->tail = job;
|
||||
}
|
||||
q->count++;
|
||||
pthread_cond_signal(&q->not_empty);
|
||||
pthread_mutex_unlock(&q->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
write_job_t *queue_pop(job_queue_t *q) {
|
||||
pthread_mutex_lock(&q->mutex);
|
||||
while (!q->closed && q->count == 0) {
|
||||
pthread_cond_wait(&q->not_empty, &q->mutex);
|
||||
}
|
||||
if (q->count == 0 && q->closed) {
|
||||
pthread_mutex_unlock(&q->mutex);
|
||||
return NULL;
|
||||
}
|
||||
write_job_t *job = q->head;
|
||||
q->head = job->next;
|
||||
if (!q->head) {
|
||||
q->tail = NULL;
|
||||
}
|
||||
q->count--;
|
||||
pthread_cond_signal(&q->not_full);
|
||||
pthread_mutex_unlock(&q->mutex);
|
||||
return job;
|
||||
}
|
||||
|
||||
// Disk worker thread
|
||||
void *disk_worker(void *arg) {
|
||||
(void)arg;
|
||||
while (1) {
|
||||
write_job_t *job = queue_pop(&g_queue);
|
||||
if (!job) break;
|
||||
|
||||
if (job->fp && job->data && job->len > 0) {
|
||||
fwrite(job->data, 1, job->len, job->fp);
|
||||
// No fflush here - let setvbuf handle buffering
|
||||
}
|
||||
|
||||
free(job->data);
|
||||
free(job);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void init_workers() {
|
||||
if (g_workers_initialized) return;
|
||||
|
||||
queue_init(&g_queue, QUEUE_MAX_SIZE);
|
||||
for (int i = 0; i < DISK_WORKER_COUNT; i++) {
|
||||
pthread_create(&g_workers[i], NULL, disk_worker, NULL);
|
||||
}
|
||||
g_workers_initialized = 1;
|
||||
}
|
||||
|
||||
// Send response - combined header+data in single send for speed
|
||||
void send_response(int sock, uint8_t response, const void *data, uint32_t data_len) {
|
||||
// Combine header and data into single buffer for single send()
|
||||
size_t total_len = 5 + data_len;
|
||||
uint8_t *combined = malloc(total_len);
|
||||
if (!combined) {
|
||||
// Fallback to separate sends
|
||||
uint8_t header[5];
|
||||
header[0] = response;
|
||||
memcpy(header + 1, &data_len, 4);
|
||||
send(sock, header, 5, 0);
|
||||
if (data && data_len > 0) {
|
||||
send(sock, data, data_len, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
combined[0] = response;
|
||||
memcpy(combined + 1, &data_len, 4);
|
||||
if (data && data_len > 0) {
|
||||
memcpy(combined + 5, data, data_len);
|
||||
}
|
||||
|
||||
send(sock, combined, total_len, 0);
|
||||
free(combined);
|
||||
}
|
||||
|
||||
// Send OK response
|
||||
void send_ok(int sock, const char *msg) {
|
||||
uint32_t len = msg ? strlen(msg) : 0;
|
||||
send_response(sock, RESP_OK, msg, len);
|
||||
}
|
||||
|
||||
// Send error response
|
||||
void send_error(int sock, const char *msg) {
|
||||
uint32_t len = msg ? strlen(msg) : 0;
|
||||
send_response(sock, RESP_ERROR, msg, len);
|
||||
}
|
||||
|
||||
// Recursive directory creation
|
||||
int mkdir_recursive(const char *path) {
|
||||
char tmp[MAX_PATH];
|
||||
char *p = NULL;
|
||||
size_t len;
|
||||
|
||||
snprintf(tmp, sizeof(tmp), "%s", path);
|
||||
len = strlen(tmp);
|
||||
if (tmp[len - 1] == '/') {
|
||||
tmp[len - 1] = 0;
|
||||
}
|
||||
|
||||
for (p = tmp + 1; *p; p++) {
|
||||
if (*p == '/') {
|
||||
*p = 0;
|
||||
if (mkdir(tmp, 0777) != 0 && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
chmod(tmp, 0777);
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
if (mkdir(tmp, 0777) != 0 && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
chmod(tmp, 0777);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Recursive directory deletion
|
||||
int rmdir_recursive(const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
if (!dir) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct dirent *entry;
|
||||
char child[MAX_PATH];
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
snprintf(child, sizeof(child), "%s/%s", path, entry->d_name);
|
||||
|
||||
struct stat st;
|
||||
if (stat(child, &st) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
rmdir_recursive(child);
|
||||
} else {
|
||||
unlink(child);
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
// Handle PING
|
||||
void handle_ping(client_session_t *session) {
|
||||
send_ok(session->sock, "PONG");
|
||||
}
|
||||
|
||||
// Handle LIST_STORAGE
|
||||
void handle_list_storage(client_session_t *session) {
|
||||
const char *mounts[] = {"/data", "/mnt/usb0", "/mnt/usb1", "/system", "/system_ex"};
|
||||
int mount_count = 0;
|
||||
|
||||
// Count available mounts
|
||||
for (int i = 0; i < 5; i++) {
|
||||
struct stat st;
|
||||
if (stat(mounts[i], &st) == 0) {
|
||||
mount_count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
size_t buf_size = 4 + (mount_count * (2 + MAX_PATH + 16));
|
||||
uint8_t *buffer = malloc(buf_size);
|
||||
if (!buffer) {
|
||||
send_error(session->sock, "Memory allocation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *ptr = buffer;
|
||||
memcpy(ptr, &mount_count, 4);
|
||||
ptr += 4;
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
struct statvfs vfs;
|
||||
if (statvfs(mounts[i], &vfs) == 0) {
|
||||
uint16_t path_len = strlen(mounts[i]);
|
||||
uint64_t total = (uint64_t)vfs.f_blocks * vfs.f_frsize;
|
||||
uint64_t free = (uint64_t)vfs.f_bfree * vfs.f_frsize;
|
||||
|
||||
memcpy(ptr, &path_len, 2);
|
||||
ptr += 2;
|
||||
memcpy(ptr, mounts[i], path_len);
|
||||
ptr += path_len;
|
||||
memcpy(ptr, &total, 8);
|
||||
ptr += 8;
|
||||
memcpy(ptr, &free, 8);
|
||||
ptr += 8;
|
||||
}
|
||||
}
|
||||
|
||||
send_response(session->sock, RESP_DATA, buffer, ptr - buffer);
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
// Handle LIST_DIR - Optimized version using d_type only (no stat for dirs)
|
||||
void handle_list_dir(client_session_t *session, const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
if (!dir) {
|
||||
int32_t count = 0;
|
||||
send_response(session->sock, RESP_DATA, &count, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t buf_size = 256 * 1024;
|
||||
uint8_t *buffer = malloc(buf_size);
|
||||
if (!buffer) {
|
||||
closedir(dir);
|
||||
int32_t count = 0;
|
||||
send_response(session->sock, RESP_DATA, &count, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *ptr = buffer + 4;
|
||||
int32_t entry_count = 0;
|
||||
|
||||
struct dirent *entry;
|
||||
char full_path[MAX_PATH];
|
||||
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
uint16_t name_len = (uint16_t)strlen(entry->d_name);
|
||||
size_t needed = 1 + 2 + name_len + 8 + 8;
|
||||
|
||||
if ((size_t)(ptr - buffer) + needed > buf_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Determine type - use d_type if available, otherwise fall back to stat()
|
||||
uint8_t type = 0;
|
||||
if (entry->d_type == DT_DIR) {
|
||||
type = 1;
|
||||
} else if (entry->d_type == DT_UNKNOWN) {
|
||||
// d_type not supported on this filesystem - use stat() as fallback
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
|
||||
struct stat st;
|
||||
if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||
type = 1;
|
||||
}
|
||||
}
|
||||
// else: DT_REG or other = file (type = 0)
|
||||
|
||||
uint64_t size = 0; // Don't need file size for browsing
|
||||
uint64_t timestamp = 0; // Don't need timestamp for browsing
|
||||
|
||||
*ptr++ = type;
|
||||
memcpy(ptr, &name_len, 2);
|
||||
ptr += 2;
|
||||
memcpy(ptr, entry->d_name, name_len);
|
||||
ptr += name_len;
|
||||
memcpy(ptr, &size, 8);
|
||||
ptr += 8;
|
||||
memcpy(ptr, ×tamp, 8);
|
||||
ptr += 8;
|
||||
|
||||
entry_count++;
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
memcpy(buffer, &entry_count, 4);
|
||||
send_response(session->sock, RESP_DATA, buffer, (uint32_t)(ptr - buffer));
|
||||
free(buffer);
|
||||
}
|
||||
|
||||
// Handle CREATE_DIR
|
||||
void handle_create_dir(client_session_t *session, const char *path) {
|
||||
if (mkdir_recursive(path) == 0) {
|
||||
send_ok(session->sock, "Directory created");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to create directory");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DELETE_FILE
|
||||
void handle_delete_file(client_session_t *session, const char *path) {
|
||||
if (unlink(path) == 0) {
|
||||
send_ok(session->sock, "File deleted");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to delete file");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DELETE_DIR
|
||||
void handle_delete_dir(client_session_t *session, const char *path) {
|
||||
if (rmdir_recursive(path) == 0) {
|
||||
send_ok(session->sock, "Directory deleted");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to delete directory");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle RENAME
|
||||
void handle_rename(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
const char *old_path = (const char *)data;
|
||||
uint32_t old_len = strlen(old_path);
|
||||
if (old_len + 2 > data_len) {
|
||||
send_error(session->sock, "Invalid rename request");
|
||||
return;
|
||||
}
|
||||
const char *new_path = (const char *)(data + old_len + 1);
|
||||
|
||||
if (rename(old_path, new_path) == 0) {
|
||||
send_ok(session->sock, "Renamed successfully");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to rename");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle COPY_FILE
|
||||
void handle_copy_file(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
const char *src = (const char *)data;
|
||||
uint32_t src_len = strlen(src);
|
||||
if (src_len + 2 > data_len) {
|
||||
send_error(session->sock, "Invalid copy request");
|
||||
return;
|
||||
}
|
||||
const char *dst = (const char *)(data + src_len + 1);
|
||||
|
||||
int src_fd = open(src, O_RDONLY);
|
||||
if (src_fd < 0) {
|
||||
send_error(session->sock, "Cannot open source file");
|
||||
return;
|
||||
}
|
||||
|
||||
int dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0777);
|
||||
if (dst_fd < 0) {
|
||||
close(src_fd);
|
||||
send_error(session->sock, "Cannot create destination file");
|
||||
return;
|
||||
}
|
||||
|
||||
char *buf = malloc(BUFFER_SIZE);
|
||||
if (!buf) {
|
||||
close(src_fd);
|
||||
close(dst_fd);
|
||||
send_error(session->sock, "Memory allocation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
ssize_t n;
|
||||
int success = 1;
|
||||
while ((n = read(src_fd, buf, BUFFER_SIZE)) > 0) {
|
||||
if (write(dst_fd, buf, n) != n) {
|
||||
success = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(buf);
|
||||
close(src_fd);
|
||||
close(dst_fd);
|
||||
chmod(dst, 0777);
|
||||
|
||||
if (success) {
|
||||
send_ok(session->sock, "File copied");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to copy file");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MOVE_FILE
|
||||
void handle_move_file(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
const char *src = (const char *)data;
|
||||
uint32_t src_len = strlen(src);
|
||||
if (src_len + 2 > data_len) {
|
||||
send_error(session->sock, "Invalid move request");
|
||||
return;
|
||||
}
|
||||
const char *dst = (const char *)(data + src_len + 1);
|
||||
|
||||
if (rename(src, dst) == 0) {
|
||||
send_ok(session->sock, "File moved");
|
||||
} else {
|
||||
send_error(session->sock, "Failed to move file");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle START_UPLOAD (with optional chunk offset for parallel upload)
|
||||
void handle_start_upload(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
}
|
||||
|
||||
// Parse path, size, and optional offset
|
||||
const char *path = (const char *)data;
|
||||
uint32_t path_len = strlen(path);
|
||||
if (path_len + 9 > data_len) {
|
||||
send_error(session->sock, "Invalid upload request");
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t file_size;
|
||||
memcpy(&file_size, data + path_len + 1, 8);
|
||||
|
||||
// Check for optional offset (for chunked parallel upload)
|
||||
uint64_t chunk_offset = 0;
|
||||
if (path_len + 17 <= data_len) {
|
||||
memcpy(&chunk_offset, data + path_len + 9, 8);
|
||||
}
|
||||
|
||||
// Create parent directories
|
||||
char parent[MAX_PATH];
|
||||
strncpy(parent, path, sizeof(parent) - 1);
|
||||
parent[sizeof(parent) - 1] = '\0';
|
||||
char *last_slash = strrchr(parent, '/');
|
||||
if (last_slash) {
|
||||
*last_slash = '\0';
|
||||
mkdir_recursive(parent);
|
||||
}
|
||||
|
||||
// Open file for writing (r+b for chunk mode, wb for normal)
|
||||
if (chunk_offset > 0) {
|
||||
// Chunk mode: open existing file or create if needed
|
||||
session->upload_fp = fopen(path, "r+b");
|
||||
if (!session->upload_fp) {
|
||||
// File doesn't exist, create it with full size
|
||||
session->upload_fp = fopen(path, "wb");
|
||||
if (session->upload_fp) {
|
||||
// CRITICAL: Allocate full file size before any chunk writes
|
||||
// This ensures all chunks can write to their offsets without corruption
|
||||
if (fseeko(session->upload_fp, file_size - 1, SEEK_SET) == 0) {
|
||||
// Write a single byte at the end to allocate the full size
|
||||
fputc(0, session->upload_fp);
|
||||
fflush(session->upload_fp);
|
||||
}
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = fopen(path, "r+b");
|
||||
}
|
||||
}
|
||||
if (session->upload_fp) {
|
||||
// Seek to chunk offset
|
||||
fseeko(session->upload_fp, chunk_offset, SEEK_SET);
|
||||
}
|
||||
} else {
|
||||
// Normal mode: create new file
|
||||
session->upload_fp = fopen(path, "wb");
|
||||
}
|
||||
|
||||
if (!session->upload_fp) {
|
||||
send_error(session->sock, "Cannot create file");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use HUGE buffer (8MB) for maximum write speed
|
||||
setvbuf(session->upload_fp, NULL, _IOFBF, BUFFER_SIZE);
|
||||
|
||||
strncpy(session->upload_path, path, sizeof(session->upload_path) - 1);
|
||||
session->upload_size = file_size;
|
||||
session->upload_received = chunk_offset;
|
||||
|
||||
// Increase socket receive buffer for this upload session
|
||||
int huge_buf = 8 * 1024 * 1024; // 8MB receive buffer
|
||||
setsockopt(session->sock, SOL_SOCKET, SO_RCVBUF, &huge_buf, sizeof(huge_buf));
|
||||
|
||||
send_response(session->sock, RESP_READY, NULL, 0);
|
||||
}
|
||||
|
||||
// Handle UPLOAD_CHUNK
|
||||
void handle_upload_chunk(client_session_t *session, const uint8_t *data, uint32_t data_len) {
|
||||
if (!session->upload_fp) {
|
||||
send_error(session->sock, "No upload in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
// Direct write with periodic flush for stability
|
||||
size_t written = fwrite(data, 1, data_len, session->upload_fp);
|
||||
if (written != data_len) {
|
||||
send_error(session->sock, "Write failed");
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
session->upload_received += written;
|
||||
|
||||
// Flush every 32MB to prevent buffer overflow with multiple connections
|
||||
if (session->upload_received % (32 * 1024 * 1024) < data_len) {
|
||||
fflush(session->upload_fp);
|
||||
}
|
||||
// No response - zero blocking for maximum speed
|
||||
}
|
||||
|
||||
// Handle END_UPLOAD
|
||||
void handle_end_upload(client_session_t *session) {
|
||||
if (!session->upload_fp) {
|
||||
send_error(session->sock, "No upload in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
fflush(session->upload_fp);
|
||||
fclose(session->upload_fp);
|
||||
session->upload_fp = NULL;
|
||||
chmod(session->upload_path, 0777);
|
||||
|
||||
send_ok(session->sock, "Upload complete");
|
||||
}
|
||||
|
||||
// Handle client
|
||||
void *client_thread(void *arg) {
|
||||
client_session_t *session = (client_session_t *)arg;
|
||||
uint8_t *buffer = malloc(BUFFER_SIZE);
|
||||
|
||||
if (!buffer) {
|
||||
close(session->sock);
|
||||
free(session);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Set socket timeout to prevent blocking forever (60 seconds)
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 60;
|
||||
tv.tv_usec = 0;
|
||||
setsockopt(session->sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
while (1) {
|
||||
// Read command header (5 bytes: 1 cmd + 4 data_len)
|
||||
uint8_t header[5];
|
||||
ssize_t n = recv(session->sock, header, 5, MSG_WAITALL);
|
||||
if (n != 5) {
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t cmd = header[0];
|
||||
uint32_t data_len;
|
||||
memcpy(&data_len, header + 1, 4);
|
||||
|
||||
// Read data if present
|
||||
uint8_t *data = NULL;
|
||||
if (data_len > 0) {
|
||||
if (data_len > BUFFER_SIZE) {
|
||||
send_error(session->sock, "Data too large");
|
||||
break;
|
||||
}
|
||||
data = buffer;
|
||||
ssize_t received = 0;
|
||||
while (received < data_len) {
|
||||
n = recv(session->sock, data + received, data_len - received, 0);
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
received += n;
|
||||
}
|
||||
if (received != data_len) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle command
|
||||
switch (cmd) {
|
||||
case CMD_PING:
|
||||
handle_ping(session);
|
||||
break;
|
||||
case CMD_LIST_STORAGE:
|
||||
handle_list_storage(session);
|
||||
break;
|
||||
case CMD_LIST_DIR:
|
||||
if (data) {
|
||||
handle_list_dir(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_CREATE_DIR:
|
||||
if (data) {
|
||||
handle_create_dir(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_DELETE_FILE:
|
||||
if (data) {
|
||||
handle_delete_file(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_DELETE_DIR:
|
||||
if (data) {
|
||||
handle_delete_dir(session, (const char *)data);
|
||||
}
|
||||
break;
|
||||
case CMD_RENAME:
|
||||
if (data) {
|
||||
handle_rename(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_COPY_FILE:
|
||||
if (data) {
|
||||
handle_copy_file(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_MOVE_FILE:
|
||||
if (data) {
|
||||
handle_move_file(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_START_UPLOAD:
|
||||
if (data) {
|
||||
handle_start_upload(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_UPLOAD_CHUNK:
|
||||
if (data) {
|
||||
handle_upload_chunk(session, data, data_len);
|
||||
}
|
||||
break;
|
||||
case CMD_END_UPLOAD:
|
||||
handle_end_upload(session);
|
||||
break;
|
||||
case CMD_SHUTDOWN:
|
||||
send_ok(session->sock, "Shutting down");
|
||||
free(buffer);
|
||||
close(session->sock);
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
}
|
||||
free(session);
|
||||
exit(0);
|
||||
default:
|
||||
send_error(session->sock, "Unknown command");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
close(session->sock);
|
||||
if (session->upload_fp) {
|
||||
fclose(session->upload_fp);
|
||||
}
|
||||
free(session);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Initialize worker threads for async disk I/O
|
||||
init_workers();
|
||||
|
||||
int server_sock;
|
||||
struct sockaddr_in server_addr;
|
||||
|
||||
server_sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (server_sock < 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int opt = 1;
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
// Prevent SIGPIPE
|
||||
int no_sigpipe = 1;
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe));
|
||||
|
||||
// 8MB buffers for sustained high throughput
|
||||
int buf_size = 8 * 1024 * 1024;
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));
|
||||
setsockopt(server_sock, SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));
|
||||
|
||||
memset(&server_addr, 0, sizeof(server_addr));
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
server_addr.sin_port = htons(SERVER_PORT);
|
||||
|
||||
if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
|
||||
close(server_sock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (listen(server_sock, 10) < 0) {
|
||||
close(server_sock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get IP address
|
||||
char ip_str[INET_ADDRSTRLEN] = "0.0.0.0";
|
||||
struct ifaddrs *ifaddr, *ifa;
|
||||
if (getifaddrs(&ifaddr) == 0) {
|
||||
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
|
||||
if (ifa->ifa_addr == NULL) continue;
|
||||
if (ifa->ifa_addr->sa_family == AF_INET) {
|
||||
struct sockaddr_in *addr = (struct sockaddr_in *)ifa->ifa_addr;
|
||||
inet_ntop(AF_INET, &addr->sin_addr, ip_str, INET_ADDRSTRLEN);
|
||||
if (strcmp(ip_str, "127.0.0.1") != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
freeifaddrs(ifaddr);
|
||||
}
|
||||
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "PS5 Upload Server v3.0: %s:%d - By Manos", ip_str, SERVER_PORT);
|
||||
send_notification(msg);
|
||||
|
||||
while (1) {
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
|
||||
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);
|
||||
if (client_sock < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Aggressive TCP socket options for sustained high speed
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe));
|
||||
|
||||
// Increase buffers to 8MB for maximum throughput
|
||||
int large_buf = 8 * 1024 * 1024;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_RCVBUF, &large_buf, sizeof(large_buf));
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_SNDBUF, &large_buf, sizeof(large_buf));
|
||||
|
||||
// TCP optimizations - TCP_NODELAY for immediate send
|
||||
int nodelay = 1;
|
||||
setsockopt(client_sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
|
||||
|
||||
// NOTE: Removed TCP_NOPUSH - it was causing buffering delays!
|
||||
|
||||
// Set keepalive to prevent connection drops
|
||||
int keepalive = 1;
|
||||
setsockopt(client_sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
|
||||
|
||||
client_session_t *session = malloc(sizeof(client_session_t));
|
||||
if (!session) {
|
||||
close(client_sock);
|
||||
continue;
|
||||
}
|
||||
|
||||
memset(session, 0, sizeof(client_session_t));
|
||||
session->sock = client_sock;
|
||||
|
||||
pthread_t thread;
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
|
||||
if (pthread_create(&thread, &attr, client_thread, session) != 0) {
|
||||
close(client_sock);
|
||||
free(session);
|
||||
}
|
||||
|
||||
pthread_attr_destroy(&attr);
|
||||
}
|
||||
|
||||
close(server_sock);
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Reference in new issue
Block a user