🎯 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>
353 lines
9.4 KiB
Markdown
353 lines
9.4 KiB
Markdown
# 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
|