Files
renato97 83e25e3bd6 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>
2026-02-04 16:20:28 +01:00

9.4 KiB

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

// 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

  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

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

  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