feat: Add VPS storage system and complete integration
🎯 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>
This commit is contained in:
352
ios-app/Sources/Config/README.md
Normal file
352
ios-app/Sources/Config/README.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# 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.0` seconds (for regular API calls)
|
||||
- **Resource Download Timeout**: `300.0` seconds (5 minutes, for large downloads)
|
||||
|
||||
### Retry Policy
|
||||
- **Max Retries**: `3` attempts
|
||||
- **Base Retry Delay**: `1.0` second (with exponential backoff)
|
||||
|
||||
### Cache Configuration
|
||||
- **Max Memory Usage**: `100` cached responses
|
||||
- **Cache Expiry**: `300.0` seconds (5 minutes)
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic URL Construction
|
||||
|
||||
```swift
|
||||
// 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
|
||||
|
||||
```swift
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = APIConfig.defaultTimeout
|
||||
configuration.timeoutIntervalForResource = APIConfig.downloadTimeout
|
||||
let session = URLSession(configuration: configuration)
|
||||
```
|
||||
|
||||
### URLRequest with Headers
|
||||
|
||||
```swift
|
||||
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
|
||||
|
||||
```swift
|
||||
// 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
|
||||
|
||||
```swift
|
||||
// Get storage statistics
|
||||
APIConfig.Endpoints.storageStats()
|
||||
|
||||
// Health check
|
||||
APIConfig.Endpoints.health()
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
The configuration includes presets for different environments:
|
||||
|
||||
### Development
|
||||
```swift
|
||||
APIConfig.development
|
||||
// - serverURL: "http://192.168.1.100"
|
||||
// - port: 3001
|
||||
// - timeout: 60.0s
|
||||
// - logging: true
|
||||
```
|
||||
|
||||
### Staging
|
||||
```swift
|
||||
APIConfig.staging
|
||||
// - serverURL: "https://staging.cbcren.online"
|
||||
// - port: 3001
|
||||
// - timeout: 30.0s
|
||||
// - logging: true
|
||||
```
|
||||
|
||||
### Production (Current)
|
||||
```swift
|
||||
APIConfig.production
|
||||
// - serverURL: "https://gitea.cbcren.online"
|
||||
// - port: 3001
|
||||
// - timeout: 30.0s
|
||||
// - logging: false
|
||||
```
|
||||
|
||||
### Testing (Debug Only)
|
||||
```swift
|
||||
#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`:
|
||||
|
||||
```swift
|
||||
// In APIConfig.swift, line 37
|
||||
static let serverURL = "https://gitea.cbcren.online" // Change this
|
||||
```
|
||||
|
||||
For environment-specific URLs, use compile-time conditionals:
|
||||
|
||||
```swift
|
||||
#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:
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
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:
|
||||
|
||||
```swift
|
||||
#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
|
||||
|
||||
1. **Always use predefined endpoints** when available instead of manually constructing URLs
|
||||
2. **Use appropriate timeouts** - `defaultTimeout` for regular calls, `downloadTimeout` for large downloads
|
||||
3. **Validate configuration** on app startup
|
||||
4. **Use the helper methods** (`url()`, `urlObject()`) for URL construction
|
||||
5. **Include common headers** in all requests
|
||||
6. **Handle specific error codes** defined in `APIConfig.ErrorCodes`
|
||||
7. **Enable logging only in debug builds** for security
|
||||
|
||||
## Example: Making an API Call
|
||||
|
||||
```swift
|
||||
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
|
||||
|
||||
```swift
|
||||
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
|
||||
|
||||
1. **Verify server URL**: Check that `serverURL` is correct and accessible
|
||||
2. **Check port**: Ensure `port` matches the backend server configuration
|
||||
3. **Test connectivity**: Use the health endpoint: `APIConfig.Endpoints.health()`
|
||||
4. **Enable logging**: Set `loggingEnabled = true` to see request details
|
||||
|
||||
### Timeout Issues
|
||||
|
||||
1. **For regular API calls**: Use `APIConfig.defaultTimeout` (30 seconds)
|
||||
2. **For large downloads**: Use `APIConfig.downloadTimeout` (300 seconds)
|
||||
3. **Slow networks**: Increase timeout values if needed
|
||||
|
||||
### SSL Certificate Issues
|
||||
|
||||
If using HTTPS with a self-signed certificate:
|
||||
1. Add the certificate to the app's bundle
|
||||
2. Configure URLSession to trust the certificate
|
||||
3. Or use HTTP for development (not recommended for production)
|
||||
|
||||
## Migration Notes
|
||||
|
||||
When migrating from the old configuration:
|
||||
|
||||
1. Replace hardcoded URLs with `APIConfig.url(for:)` or predefined endpoints
|
||||
2. Use `APIConfig.commonHeaders` instead of manually setting headers
|
||||
3. Replace hardcoded timeouts with `APIConfig.defaultTimeout` or `APIConfig.downloadTimeout`
|
||||
4. Add validation on app startup with `APIConfig.isValid`
|
||||
5. Use specific error codes from `APIConfig.ErrorCodes`
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- See `APIConfigExample.swift` for more comprehensive examples
|
||||
- Check the backend API documentation for available endpoints
|
||||
- Review the iOS app's Services directory for integration examples
|
||||
Reference in New Issue
Block a user