🎯 Overview: Implemented complete VPS-based storage system allowing the iOS app to download and store manga chapters on the VPS for ad-free offline reading. 📦 Backend Changes: - Added storage.js service for managing chapter downloads (270 lines) - Updated server.js with 6 new storage endpoints: - POST /api/download - Download chapters to VPS - GET /api/storage/chapters/:mangaSlug - List downloaded chapters - GET /api/storage/chapter/:mangaSlug/:chapterNumber - Check download status - GET /api/storage/image/:mangaSlug/:chapterNumber/:pageIndex - Serve images - DELETE /api/storage/chapter/:mangaSlug/:chapterNumber - Delete chapters - GET /api/storage/stats - Get storage statistics - Fixed scraper.js Puppeteer compatibility issues (waitForTimeout, networkidle0) - Added comprehensive test suite: - test-vps-flow.js (13 tests - 100% pass rate) - test-concurrent-downloads.js (10 tests for parallel operations) - run-tests.sh automation script 📱 iOS App Changes: - Created APIConfig.swift with VPS connection settings - Created VPSAPIClient.swift service (727 lines) for backend communication - Updated MangaDetailView.swift with VPS download integration: - Cloud icon for VPS-available chapters - Upload button to download chapters to VPS - Progress indicators for active downloads - Bulk download options (last 10 or all chapters) - Updated ReaderView.swift to load images from VPS first - Progressive enhancement: app works without VPS, enhances when available ✅ Tests: - All 13 VPS flow tests passing (100%) - Tests verify: scraping, downloading, storage, serving, deletion, stats - Chapter 789 download test: 21 images, 4.68 MB - Concurrent download tests verify no race conditions 🔧 Configuration: - VPS URL: https://gitea.cbcren.online:3001 - Storage location: /home/ren/ios/MangaReader/storage/ - Static file serving: /storage path 📚 Documentation: - Added VPS_INTEGRATION_SUMMARY.md - Complete feature overview - Added CHANGES.md - Detailed code changes reference - Added TEST_README.md, TEST_QUICK_START.md, TEST_SUMMARY.md - Added APIConfig README with usage examples 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
API Configuration for MangaReader iOS App
Overview
This directory contains the API configuration for connecting the iOS app to the VPS backend. The configuration is centralized in APIConfig.swift and includes all necessary settings for API communication.
Files
- APIConfig.swift: Main configuration file with all API settings, endpoints, and helper methods
- APIConfigExample.swift: Comprehensive usage examples and demonstrations
- README.md (this file): Documentation and usage guide
Current Configuration
Server Connection
- Server URL:
https://gitea.cbcren.online - Port:
3001 - Full Base URL:
https://gitea.cbcren.online:3001 - API Version:
v1 - API Base Path:
https://gitea.cbcren.online:3001/api/v1
Timeouts
- Default Request Timeout:
30.0seconds (for regular API calls) - Resource Download Timeout:
300.0seconds (5 minutes, for large downloads)
Retry Policy
- Max Retries:
3attempts - Base Retry Delay:
1.0second (with exponential backoff)
Cache Configuration
- Max Memory Usage:
100cached responses - Cache Expiry:
300.0seconds (5 minutes)
Usage
Basic URL Construction
// Method 1: Use the helper function
let url = APIConfig.url(for: "manga/popular")
// Result: "https://gitea.cbcren.online:3001/manga/popular"
// Method 2: Get a URL object
if let urlObj = APIConfig.urlObject(for: "manga/popular") {
var request = URLRequest(url: urlObj)
// Make request...
}
// Method 3: Use predefined endpoints
let endpoint = APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1089)
// Result: "https://gitea.cbcren.online:3001/api/v1/download/one-piece/1089"
URLSession Configuration
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = APIConfig.defaultTimeout
configuration.timeoutIntervalForResource = APIConfig.downloadTimeout
let session = URLSession(configuration: configuration)
URLRequest with Headers
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.defaultTimeout
// Add common headers
for (key, value) in APIConfig.commonHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
// Add authentication if needed
if let token = authToken {
let authHeaders = APIConfig.authHeader(token: token)
for (key, value) in authHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
}
Available Endpoints
Download Endpoints
// Request chapter download
APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1089)
// Check if chapter is downloaded
APIConfig.Endpoints.checkDownloaded(mangaSlug: "one-piece", chapterNumber: 1089)
// List all downloaded chapters for a manga
APIConfig.Endpoints.listChapters(mangaSlug: "one-piece")
// Get specific image from chapter
APIConfig.Endpoints.getImage(mangaSlug: "one-piece", chapterNumber: 1089, pageIndex: 0)
// Delete a chapter
APIConfig.Endpoints.deleteChapter(mangaSlug: "one-piece", chapterNumber: 1089)
Server Endpoints
// Get storage statistics
APIConfig.Endpoints.storageStats()
// Health check
APIConfig.Endpoints.health()
Environment Configuration
The configuration includes presets for different environments:
Development
APIConfig.development
// - serverURL: "http://192.168.1.100"
// - port: 3001
// - timeout: 60.0s
// - logging: true
Staging
APIConfig.staging
// - serverURL: "https://staging.cbcren.online"
// - port: 3001
// - timeout: 30.0s
// - logging: true
Production (Current)
APIConfig.production
// - serverURL: "https://gitea.cbcren.online"
// - port: 3001
// - timeout: 30.0s
// - logging: false
Testing (Debug Only)
#if DEBUG
APIConfig.testing
// - serverURL: "http://localhost:3001"
// - port: 3001
// - timeout: 5.0s
// - logging: true
#endif
Changing the Server URL
To change the API server URL, modify the serverURL property in APIConfig.swift:
// In APIConfig.swift, line 37
static let serverURL = "https://gitea.cbcren.online" // Change this
For environment-specific URLs, use compile-time conditionals:
#if DEBUG
static let serverURL = "http://192.168.1.100" // Local development
#else
static let serverURL = "https://gitea.cbcren.online" // Production
#endif
Error Codes
The API defines specific error codes for different scenarios:
APIConfig.ErrorCodes.chapterNotFound // 40401
APIConfig.ErrorCodes.chapterAlreadyDownloaded // 40901
APIConfig.ErrorCodes.storageLimitExceeded // 50701
APIConfig.ErrorCodes.invalidImageFormat // 42201
APIConfig.ErrorCodes.downloadFailed // 50001
Validation
The configuration includes a validation method:
if APIConfig.isValid {
print("Configuration is valid")
} else {
print("Configuration is invalid")
}
This checks:
- Server URL is not empty
- Port is in valid range (1-65535)
- Timeout values are positive
- Retry count is non-negative
Debug Support
In debug builds, you can print the current configuration:
#if DEBUG
APIConfig.printConfiguration()
#endif
This outputs:
=== API Configuration ===
Server URL: https://gitea.cbcren.online
Port: 3001
Base URL: https://gitea.cbcren.online:3001
API Version: v1
Default Timeout: 30.0s
Download Timeout: 300.0s
Max Retries: 3
Logging Enabled: false
Cache Enabled: true
=========================
Best Practices
- Always use predefined endpoints when available instead of manually constructing URLs
- Use appropriate timeouts -
defaultTimeoutfor regular calls,downloadTimeoutfor large downloads - Validate configuration on app startup
- Use the helper methods (
url(),urlObject()) for URL construction - Include common headers in all requests
- Handle specific error codes defined in
APIConfig.ErrorCodes - Enable logging only in debug builds for security
Example: Making an API Call
func fetchPopularManga() async throws -> [Manga] {
// Construct URL
guard let url = APIConfig.urlObject(for: "manga/popular") else {
throw APIError.invalidURL
}
// Create request
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.defaultTimeout
// Add headers
for (key, value) in APIConfig.commonHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
// Make request
let (data, response) = try await URLSession.shared.data(for: request)
// Validate response
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw APIError.requestFailed
}
// Decode response
let mangas = try JSONDecoder().decode([Manga].self, from: data)
return mangas
}
Example: Downloading with Retry
func downloadChapterWithRetry(
mangaSlug: String,
chapterNumber: Int
) async throws -> Data {
let endpoint = APIConfig.Endpoints.download(
mangaSlug: mangaSlug,
chapterNumber: chapterNumber
)
return try await fetchWithRetry(endpoint: endpoint, retryCount: 0)
}
func fetchWithRetry(endpoint: String, retryCount: Int) async throws -> Data {
guard let url = URL(string: endpoint),
retryCount < APIConfig.maxRetries else {
throw APIError.retryLimitExceeded
}
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.downloadTimeout
do {
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
return data
} else {
throw APIError.requestFailed
}
} catch {
// Calculate exponential backoff delay
let delay = APIConfig.baseRetryDelay * pow(2.0, Double(retryCount))
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
return try await fetchWithRetry(endpoint: endpoint, retryCount: retryCount + 1)
}
}
Troubleshooting
Connection Issues
- Verify server URL: Check that
serverURLis correct and accessible - Check port: Ensure
portmatches the backend server configuration - Test connectivity: Use the health endpoint:
APIConfig.Endpoints.health() - Enable logging: Set
loggingEnabled = trueto see request details
Timeout Issues
- For regular API calls: Use
APIConfig.defaultTimeout(30 seconds) - For large downloads: Use
APIConfig.downloadTimeout(300 seconds) - Slow networks: Increase timeout values if needed
SSL Certificate Issues
If using HTTPS with a self-signed certificate:
- Add the certificate to the app's bundle
- Configure URLSession to trust the certificate
- Or use HTTP for development (not recommended for production)
Migration Notes
When migrating from the old configuration:
- Replace hardcoded URLs with
APIConfig.url(for:)or predefined endpoints - Use
APIConfig.commonHeadersinstead of manually setting headers - Replace hardcoded timeouts with
APIConfig.defaultTimeoutorAPIConfig.downloadTimeout - Add validation on app startup with
APIConfig.isValid - Use specific error codes from
APIConfig.ErrorCodes
Additional Resources
- See
APIConfigExample.swiftfor more comprehensive examples - Check the backend API documentation for available endpoints
- Review the iOS app's Services directory for integration examples