Use json database

This commit is contained in:
Christoph Hagen 2022-05-24 14:47:50 +02:00
parent a6b61ef796
commit 3f4202d9ad
4 changed files with 105 additions and 95 deletions

30
Sources/App/Cap.swift Normal file
View File

@ -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
}
}

View File

@ -6,40 +6,56 @@ final class CapServer {
// MARK: Paths // MARK: Paths
private let imageFolder: URL 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 private let fm = FileManager.default
// MARK: Caps // 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 { init(in folder: URL) throws {
self.imageFolder = folder.appendingPathComponent("images") self.imageFolder = folder.appendingPathComponent("images")
self.nameFile = folder.appendingPathComponent("names.txt") self.dbFile = folder.appendingPathComponent("caps.json")
self.tempImageFile = folder.appendingPathComponent("temp.jpg")
var isDirectory: ObjCBool = false var isDirectory: ObjCBool = false
guard fm.fileExists(atPath: folder.path, isDirectory: &isDirectory), 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") log("Public directory \(folder.path) is not a folder, or doesn't exist")
throw CapError.invalidConfiguration throw CapError.invalidConfiguration
} }
}
// MARK: SQLite
func loadCapNames() throws { try loadCaps()
let s = try String(contentsOf: nameFile) try updateCounts()
caps = s }
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "\n") 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") log("\(caps.count) caps loaded")
} }
private func saveCaps() throws {
let data = try JSONEncoder().encode(caps.values.sorted())
try data.write(to: dbFile)
}
// MARK: Paths // MARK: Paths
func folder(of cap: Int) -> URL { func folder(of cap: Int) -> URL {
@ -52,10 +68,25 @@ final class CapServer {
// MARK: Counts // MARK: Counts
func countImages(in folder: URL) throws -> Int { private func updateCounts() throws {
try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil).filter({ $0.pathExtension == "jpg" }).count 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 { func count(of cap: Int) throws -> Int {
let f = folder(of: cap) let f = folder(of: cap)
guard fm.fileExists(atPath: f.path) else { guard fm.fileExists(atPath: f.path) else {
@ -65,43 +96,22 @@ final class CapServer {
} }
// MARK: Names // MARK: Names
func name(for cap: Int) throws -> Data { func set(name: String, for capId: Int) throws {
let index = cap - 1 guard var cap = caps[capId] else {
guard index >= 0, index < caps.count else { let url = server.folder(of: capId)
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) { if !fm.fileExists(atPath: url.path) {
try fm.createDirectory(at: url, withIntermediateDirectories: false) try fm.createDirectory(at: url, withIntermediateDirectories: false)
} }
log("Added cap \(cap)") caps[capId] = Cap(id: capId, name: name, count: 0, mainImage: 0, classifierVersion: nextClassifierVersion)
} else { log("Added cap \(capId)")
caps[index] = name return
log("Set name for cap \(cap)")
} }
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 // MARK: Images
/** /**
@ -111,37 +121,32 @@ final class CapServer {
- Parameter data: The image data - Parameter data: The image data
- Parameter cap: The id of the cap. - Parameter cap: The id of the cap.
- Returns: The new image count for 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 { func save(image data: Data, for cap: Int) throws -> Int {
let c = try count(of: cap) guard var count = caps[cap]?.count else {
let f = file(of: cap, version: c) throw CapError.unknownId
guard !fm.fileExists(atPath: f.path) else { }
log("Image \(c) for cap \(cap) already exists on disk") var id = 0
throw CapError.dataInconsistency 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) 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 { 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) let file2 = file(of: cap, version: version)
guard fm.fileExists(atPath: file2.path) else { guard fm.fileExists(atPath: file2.path) else {
log("No image \(version) for cap \(cap)") log("No image \(version) for cap \(cap)")
throw CapError.invalidFile throw CapError.invalidFile
} }
try fm.moveItem(at: file1, to: tempImageFile) caps[cap]?.mainImage = version
try fm.moveItem(at: file2, to: file1)
try fm.moveItem(at: tempImageFile, to: file2)
log("Switched cap \(cap) to version \(version)") log("Switched cap \(cap) to version \(version)")
} }
} }

View File

@ -31,7 +31,6 @@ public func configure(_ app: Application) throws {
} }
server = try CapServer(in: URL(fileURLWithPath: publicDirectory)) server = try CapServer(in: URL(fileURLWithPath: publicDirectory))
try server.loadCapNames()
// Register routes to the router // Register routes to the router
try routes(app) try routes(app)

View File

@ -5,15 +5,6 @@ import Vapor
/// [Learn More ](https://docs.vapor.codes/3.0/getting-started/structure/#routesswift) /// [Learn More ](https://docs.vapor.codes/3.0/getting-started/structure/#routesswift)
func routes(_ app: Application) throws { 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 // Set the name of a cap
app.postCatching("name", ":n") { request in app.postCatching("name", ":n") { request in
guard let cap = request.parameters.get("n", as: Int.self) else { 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)! 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 // Set a different version as the main image
app.getCatching("switch", ":n", ":v") { request in 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") log("Invalid parameter for cap")
throw Abort(.badRequest) 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") log("Invalid parameter for cap version")
throw Abort(.badRequest) throw Abort(.badRequest)
} }