import Foundation import Vapor final class CapServer { // MARK: Paths private let imageFolder: URL private let nameFile: URL private let tempImageFile: URL private let fm = FileManager.default // MARK: Caps private var caps = [String]() init(in folder: URL) throws { self.imageFolder = folder.appendingPathComponent("images") self.nameFile = folder.appendingPathComponent("names.txt") self.tempImageFile = folder.appendingPathComponent("temp.jpg") var isDirectory: ObjCBool = false guard fm.fileExists(atPath: folder.path, isDirectory: &isDirectory), isDirectory.boolValue else { log("Public directory \(folder.path) is not a folder, or doesn't exist") throw CapError.invalidConfiguration } } // MARK: SQLite func loadCapNames() throws { let s = try String(contentsOf: nameFile) caps = s .trimmingCharacters(in: .whitespacesAndNewlines) .components(separatedBy: "\n") log("\(caps.count) caps loaded") } // MARK: Paths func folder(of cap: Int) -> URL { imageFolder.appendingPathComponent(String(format: "%04d", cap)) } func file(of cap: Int, version: Int) -> URL { folder(of: cap).appendingPathComponent(String(format: "%04d-%02d.jpg", cap, version)) } // MARK: Counts func countImages(in folder: URL) throws -> Int { try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil).filter({ $0.pathExtension == "jpg" }).count } func count(of cap: Int) throws -> Int { let f = folder(of: cap) guard fm.fileExists(atPath: f.path) else { return 0 } return try countImages(in: f) } // MARK: Names func name(for cap: Int) throws -> Data { let index = cap - 1 guard index >= 0, index < caps.count else { log("Trying to get name for invalid cap \(cap) (\(caps.count) caps loaded)") throw CapError.unknownId } return caps[index].data(using: .utf8)! } func set(name: String, for cap: Int) throws { let index = cap - 1 guard index <= caps.count else { log("Trying to set name for cap \(cap), but only \(caps.count) caps exist") throw CapError.unknownId } if index == caps.count { caps.append(name) // Create image folder let url = server.folder(of: cap) if !fm.fileExists(atPath: url.path) { try fm.createDirectory(at: url, withIntermediateDirectories: false) } log("Added cap \(cap)") } else { caps[index] = name log("Set name for cap \(cap)") } try caps.joined(separator: "\n").data(using: .utf8)!.write(to: server.nameFile) } func getCountData() throws -> Data { #warning("Counts are encoded as UInt8, works only while count < 256") return Data(try (1...caps.count).map({ UInt8(try server.count(of: $0)) })) } // MARK: Images /** Save a cap image to disk. Automatically creates the image name with the current image count. - Parameter data: The image data - Parameter cap: The id of the cap. - Returns: The new image count for the cap - Throws: `CapError.dataInconsistency` if an image already exists for the current count. */ func save(image data: Data, for cap: Int) throws -> Int { let c = try count(of: cap) let f = file(of: cap, version: c) guard !fm.fileExists(atPath: f.path) else { log("Image \(c) for cap \(cap) already exists on disk") throw CapError.dataInconsistency } try data.write(to: f) return c } func switchMainImage(to version: Int, for cap: Int) throws { guard version > 0 else { log("Not switching cap \(cap) to image \(version)") return } let file1 = file(of: cap, version: 0) guard fm.fileExists(atPath: file1.path) else { log("No image 0 for cap \(cap)") throw CapError.invalidFile } let file2 = file(of: cap, version: version) guard fm.fileExists(atPath: file2.path) else { log("No image \(version) for cap \(cap)") throw CapError.invalidFile } try fm.moveItem(at: file1, to: tempImageFile) try fm.moveItem(at: file2, to: file1) try fm.moveItem(at: tempImageFile, to: file2) log("Switched cap \(cap) to version \(version)") } }