Initial scaffold — NASBackup iOS app
Full project scaffold: XcodeGen project.yml, Core models/protocols/errors, SMBService (kishikawakatsumi/SMBClient), SFTPService (Citadel 0.9.2), PhotoLibraryService, LANMonitor, BackupEngine, BackgroundTaskManager, all feature UIs (Login, Connect, Browse, Backup, History, Settings), Shared components and theme. Builds clean with zero errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
90
Core/Models/BackupJob.swift
Normal file
90
Core/Models/BackupJob.swift
Normal file
@@ -0,0 +1,90 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
enum BackupStatus: Equatable {
|
||||
case idle
|
||||
case preparing
|
||||
case running
|
||||
case paused
|
||||
case completed
|
||||
case failed(String)
|
||||
case cancelled
|
||||
|
||||
var isActive: Bool {
|
||||
switch self {
|
||||
case .running, .paused, .preparing: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class BackupJob: ObservableObject {
|
||||
@Published private(set) var status: BackupStatus = .idle
|
||||
@Published private(set) var totalFiles: Int = 0
|
||||
@Published private(set) var uploadedFiles: Int = 0
|
||||
@Published private(set) var skippedFiles: Int = 0
|
||||
@Published private(set) var failedFiles: Int = 0
|
||||
@Published private(set) var currentFileName: String = ""
|
||||
@Published private(set) var currentFileSizeBytes: Int64 = 0
|
||||
@Published private(set) var bytesTransferred: Int64 = 0
|
||||
@Published private(set) var totalBytes: Int64 = 0
|
||||
@Published private(set) var speedBytesPerSecond: Double = 0
|
||||
@Published private(set) var startDate: Date?
|
||||
@Published private(set) var endDate: Date?
|
||||
|
||||
var progress: Double {
|
||||
guard totalFiles > 0 else { return 0 }
|
||||
return Double(uploadedFiles + skippedFiles + failedFiles) / Double(totalFiles)
|
||||
}
|
||||
|
||||
var eta: TimeInterval? {
|
||||
guard speedBytesPerSecond > 0, totalBytes > bytesTransferred else { return nil }
|
||||
return Double(totalBytes - bytesTransferred) / speedBytesPerSecond
|
||||
}
|
||||
|
||||
func prepare() { status = .preparing }
|
||||
|
||||
func start(totalFiles: Int, totalBytes: Int64) {
|
||||
self.totalFiles = totalFiles
|
||||
self.totalBytes = totalBytes
|
||||
self.uploadedFiles = 0
|
||||
self.skippedFiles = 0
|
||||
self.failedFiles = 0
|
||||
self.bytesTransferred = 0
|
||||
self.startDate = Date()
|
||||
self.endDate = nil
|
||||
self.status = .running
|
||||
}
|
||||
|
||||
func updateProgress(fileName: String, fileSize: Int64, bytesTransferred: Int64, speed: Double) {
|
||||
self.currentFileName = fileName
|
||||
self.currentFileSizeBytes = fileSize
|
||||
self.bytesTransferred = bytesTransferred
|
||||
self.speedBytesPerSecond = speed
|
||||
}
|
||||
|
||||
func fileCompleted(skipped: Bool = false) {
|
||||
if skipped { skippedFiles += 1 } else { uploadedFiles += 1 }
|
||||
}
|
||||
|
||||
func fileFailed() { failedFiles += 1 }
|
||||
|
||||
func finish(result: BackupResult) {
|
||||
endDate = Date()
|
||||
status = result.failedCount == 0 ? .completed : .completed
|
||||
}
|
||||
|
||||
func fail(error: BackupError) {
|
||||
endDate = Date()
|
||||
status = .failed(error.errorDescription ?? error.localizedDescription)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
endDate = Date()
|
||||
status = .cancelled
|
||||
}
|
||||
|
||||
func pause() { if case .running = status { status = .paused } }
|
||||
func resume() { if case .paused = status { status = .running } }
|
||||
}
|
||||
41
Core/Models/BackupResult.swift
Normal file
41
Core/Models/BackupResult.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
struct BackupResult {
|
||||
let uploadedCount: Int
|
||||
let skippedCount: Int
|
||||
let failedCount: Int
|
||||
let duration: TimeInterval
|
||||
let totalBytes: Int64
|
||||
let date: Date
|
||||
|
||||
var hasErrors: Bool { failedCount > 0 }
|
||||
|
||||
static func empty(date: Date = Date()) -> BackupResult {
|
||||
BackupResult(uploadedCount: 0, skippedCount: 0, failedCount: 0,
|
||||
duration: 0, totalBytes: 0, date: date)
|
||||
}
|
||||
}
|
||||
|
||||
struct BackupHistoryEntry: Codable, Identifiable {
|
||||
let id: UUID
|
||||
let date: Date
|
||||
let uploadedCount: Int
|
||||
let skippedCount: Int
|
||||
let failedCount: Int
|
||||
let durationSeconds: Double
|
||||
let totalBytes: Int64
|
||||
let nasHost: String
|
||||
let triggeredByLAN: Bool
|
||||
|
||||
init(result: BackupResult, nasHost: String, triggeredByLAN: Bool) {
|
||||
self.id = UUID()
|
||||
self.date = result.date
|
||||
self.uploadedCount = result.uploadedCount
|
||||
self.skippedCount = result.skippedCount
|
||||
self.failedCount = result.failedCount
|
||||
self.durationSeconds = result.duration
|
||||
self.totalBytes = result.totalBytes
|
||||
self.nasHost = nasHost
|
||||
self.triggeredByLAN = triggeredByLAN
|
||||
}
|
||||
}
|
||||
42
Core/Models/NASConnection.swift
Normal file
42
Core/Models/NASConnection.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
|
||||
enum NASProtocol: String, Codable, CaseIterable {
|
||||
case smb = "SMB"
|
||||
case sftp = "SFTP"
|
||||
|
||||
var defaultPort: Int {
|
||||
switch self {
|
||||
case .smb: return 445
|
||||
case .sftp: return 22
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NASConnection: Codable, Identifiable, Equatable {
|
||||
var id: UUID = UUID()
|
||||
var host: String
|
||||
var port: Int
|
||||
var nasProtocol: NASProtocol
|
||||
var username: String
|
||||
var password: String
|
||||
var remotePath: String
|
||||
var displayName: String
|
||||
|
||||
init(
|
||||
host: String,
|
||||
port: Int? = nil,
|
||||
nasProtocol: NASProtocol = .smb,
|
||||
username: String,
|
||||
password: String,
|
||||
remotePath: String = "/",
|
||||
displayName: String = ""
|
||||
) {
|
||||
self.host = host
|
||||
self.port = port ?? nasProtocol.defaultPort
|
||||
self.nasProtocol = nasProtocol
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.remotePath = remotePath
|
||||
self.displayName = displayName.isEmpty ? host : displayName
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user