diff --git a/Sources/App/Cap.swift b/Sources/App/Cap.swift new file mode 100644 index 0000000..d126cb2 --- /dev/null +++ b/Sources/App/Cap.swift @@ -0,0 +1,30 @@ +import Foundation + +struct Cap: Codable { + + let id: Int + + var name: String + + var count: Int + + var mainImage: Int + + /// The version of the first classifier trained on this cap + var classifierVersion: Int + + enum CodingKeys: String, CodingKey { + case id = "i" + case name = "n" + case count = "c" + case mainImage = "m" + case classifierVersion = "v" + } +} + +extension Cap: Comparable { + + static func < (lhs: Cap, rhs: Cap) -> Bool { + lhs.id < rhs.id + } +} diff --git a/Sources/App/CapServer.swift b/Sources/App/CapServer.swift index 2ed8eff..aa6a784 100644 --- a/Sources/App/CapServer.swift +++ b/Sources/App/CapServer.swift @@ -6,40 +6,56 @@ final class CapServer { // MARK: Paths private let imageFolder: URL - - private let nameFile: URL - private let tempImageFile: URL + /// The file where the database of caps is stored + private let dbFile: URL private let fm = FileManager.default // MARK: Caps - private var caps = [String]() + private var saveImmediatelly = true + + private var caps = [Int: Cap]() { + didSet { + guard saveImmediatelly else { + return + } + try? saveCaps() + } + } + + var nextClassifierVersion: Int { + caps.values.map { $0.classifierVersion }.max() ?? 1 + } init(in folder: URL) throws { self.imageFolder = folder.appendingPathComponent("images") - self.nameFile = folder.appendingPathComponent("names.txt") - self.tempImageFile = folder.appendingPathComponent("temp.jpg") + self.dbFile = folder.appendingPathComponent("caps.json") var isDirectory: ObjCBool = false guard fm.fileExists(atPath: folder.path, isDirectory: &isDirectory), - isDirectory.boolValue else { + 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") + try loadCaps() + try updateCounts() + } + + private func loadCaps() throws { + let data = try Data(contentsOf: dbFile) + caps = try JSONDecoder().decode([Cap].self, from: data) + .reduce(into: [:]) { $0[$1.id] = $1 } log("\(caps.count) caps loaded") } + private func saveCaps() throws { + let data = try JSONEncoder().encode(caps.values.sorted()) + try data.write(to: dbFile) + } + // MARK: Paths func folder(of cap: Int) -> URL { @@ -52,10 +68,25 @@ final class CapServer { // MARK: Counts - func countImages(in folder: URL) throws -> Int { - try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil).filter({ $0.pathExtension == "jpg" }).count + private func updateCounts() throws { + saveImmediatelly = false + caps = try caps.mapValues { + var cap = $0 + cap.count = try count(of: $0.id) + return cap + } + saveImmediatelly = true + try? saveCaps() } + func countImages(in folder: URL) throws -> Int { + try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil) + .filter({ $0.pathExtension == "jpg" }).count + } + + /** + Get the image count of a cap. + */ func count(of cap: Int) throws -> Int { let f = folder(of: cap) guard fm.fileExists(atPath: f.path) else { @@ -65,43 +96,22 @@ final class CapServer { } // 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) + + func set(name: String, for capId: Int) throws { + guard var cap = caps[capId] else { + let url = server.folder(of: capId) 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)") + caps[capId] = Cap(id: capId, name: name, count: 0, mainImage: 0, classifierVersion: nextClassifierVersion) + log("Added cap \(capId)") + return } - try caps.joined(separator: "\n").data(using: .utf8)!.write(to: server.nameFile) + cap.name = name + caps[capId] = cap + log("Set name for cap \(capId)") } - - 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 /** @@ -111,37 +121,32 @@ final class CapServer { - 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. + - Throws: `CapError.unknownId`, if the cap doesn't exist. `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 + guard var count = caps[cap]?.count else { + throw CapError.unknownId + } + var id = 0 + var f = file(of: cap, version: id) + while fm.fileExists(atPath: f.path) { + id += 1 + f = file(of: cap, version: id) } try data.write(to: f) - return c + count += 1 + caps[cap]!.count = count + log("Added image \(id) for cap \(cap)") + return count } 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) + caps[cap]?.mainImage = version log("Switched cap \(cap) to version \(version)") } } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index d48f6b0..c37b4f0 100755 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -31,7 +31,6 @@ public func configure(_ app: Application) throws { } server = try CapServer(in: URL(fileURLWithPath: publicDirectory)) - try server.loadCapNames() // Register routes to the router try routes(app) diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index ae8fe3d..161df1f 100755 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -5,15 +5,6 @@ import Vapor /// [Learn More →](https://docs.vapor.codes/3.0/getting-started/structure/#routesswift) func routes(_ app: Application) throws { - // Get the name of a cap - app.getCatching("name", ":n") { request -> Data in - guard let cap = request.parameters.get("n", as: Int.self) else { - log("Invalid body data") - throw Abort(.badRequest) - } - return try server.name(for: cap) - } - // Set the name of a cap app.postCatching("name", ":n") { request in guard let cap = request.parameters.get("n", as: Int.self) else { @@ -42,28 +33,13 @@ func routes(_ app: Application) throws { return "\(newCount)".data(using: .utf8)! } - // Get count of a cap - app.getCatching("count", ":c") { request -> Data in - guard let cap = request.parameters.get("c", as: Int.self) else { - log("Invalid parameter for cap") - throw Abort(.badRequest) - } - let c = try server.count(of: cap) - return "\(c)".data(using: .utf8)! - } - - // Get the count of all caps - app.getCatching("counts") { request -> Data in - try server.getCountData() - } - // Set a different version as the main image app.getCatching("switch", ":n", ":v") { request in - guard let cap = request.parameters.get("n", as: Int.self) else { + guard let cap = request.parameters.get("n", as: Int.self), cap >= 0 else { log("Invalid parameter for cap") throw Abort(.badRequest) } - guard let version = request.parameters.get("v", as: Int.self) else { + guard let version = request.parameters.get("v", as: Int.self), version >= 0 else { log("Invalid parameter for cap version") throw Abort(.badRequest) }