2021-11-08 21:58:55 +01:00
|
|
|
import Foundation
|
|
|
|
import Vapor
|
|
|
|
|
|
|
|
final class CapServer {
|
|
|
|
|
|
|
|
// MARK: Paths
|
|
|
|
|
|
|
|
private let imageFolder: URL
|
|
|
|
|
2022-05-24 14:47:50 +02:00
|
|
|
/// The file where the database of caps is stored
|
|
|
|
private let dbFile: URL
|
2021-11-08 21:58:55 +01:00
|
|
|
|
|
|
|
private let fm = FileManager.default
|
|
|
|
|
|
|
|
// MARK: Caps
|
|
|
|
|
2022-05-24 14:47:50 +02:00
|
|
|
private var saveImmediatelly = true
|
|
|
|
|
2022-05-27 09:25:41 +02:00
|
|
|
private var writers: Set<String>
|
|
|
|
|
2022-05-24 14:47:50 +02:00
|
|
|
private var caps = [Int: Cap]() {
|
|
|
|
didSet {
|
|
|
|
guard saveImmediatelly else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
try? saveCaps()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var nextClassifierVersion: Int {
|
2022-05-27 09:25:41 +02:00
|
|
|
caps.values.compactMap { $0.classifierVersion }.max() ?? 1
|
2022-05-24 14:47:50 +02:00
|
|
|
}
|
2021-11-08 21:58:55 +01:00
|
|
|
|
2022-05-27 09:25:41 +02:00
|
|
|
init(in folder: URL, writers: [String]) throws {
|
2021-11-08 21:58:55 +01:00
|
|
|
self.imageFolder = folder.appendingPathComponent("images")
|
2022-05-24 14:47:50 +02:00
|
|
|
self.dbFile = folder.appendingPathComponent("caps.json")
|
2022-05-27 09:25:41 +02:00
|
|
|
self.writers = Set(writers)
|
2021-11-08 21:58:55 +01:00
|
|
|
|
|
|
|
var isDirectory: ObjCBool = false
|
|
|
|
guard fm.fileExists(atPath: folder.path, isDirectory: &isDirectory),
|
2022-05-24 14:47:50 +02:00
|
|
|
isDirectory.boolValue else {
|
2021-11-08 21:58:55 +01:00
|
|
|
log("Public directory \(folder.path) is not a folder, or doesn't exist")
|
|
|
|
throw CapError.invalidConfiguration
|
|
|
|
}
|
2022-05-24 14:47:50 +02:00
|
|
|
|
|
|
|
try loadCaps()
|
|
|
|
try updateCounts()
|
2021-11-08 21:58:55 +01:00
|
|
|
}
|
|
|
|
|
2022-05-24 14:47:50 +02:00
|
|
|
private func loadCaps() throws {
|
|
|
|
let data = try Data(contentsOf: dbFile)
|
|
|
|
caps = try JSONDecoder().decode([Cap].self, from: data)
|
|
|
|
.reduce(into: [:]) { $0[$1.id] = $1 }
|
2021-11-08 21:58:55 +01:00
|
|
|
log("\(caps.count) caps loaded")
|
|
|
|
}
|
|
|
|
|
2022-05-24 14:47:50 +02:00
|
|
|
private func saveCaps() throws {
|
|
|
|
let data = try JSONEncoder().encode(caps.values.sorted())
|
|
|
|
try data.write(to: dbFile)
|
|
|
|
}
|
|
|
|
|
2021-11-08 21:58:55 +01:00
|
|
|
// 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))
|
|
|
|
}
|
2022-05-27 09:25:41 +02:00
|
|
|
|
|
|
|
// MARK: Authentication
|
|
|
|
|
|
|
|
func hasAuthorization(for key: String) -> Bool {
|
|
|
|
// Note: This is not a constant-time compare, so there may be an opportunity
|
|
|
|
// for timing attack here. Sets perform hashed lookups, so this may be less of an issue,
|
|
|
|
// and we're not doing anything critical in this application.
|
|
|
|
// Worst case, an unauthorized person with a lot of free time and energy to hack this system
|
|
|
|
// is able to change contents of the database, which are backed up in any case.
|
|
|
|
writers.contains(key)
|
|
|
|
}
|
2021-11-08 21:58:55 +01:00
|
|
|
|
|
|
|
// MARK: Counts
|
|
|
|
|
2022-05-24 14:47:50 +02:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2021-11-08 21:58:55 +01:00
|
|
|
func countImages(in folder: URL) throws -> Int {
|
2022-05-24 14:47:50 +02:00
|
|
|
try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil)
|
|
|
|
.filter({ $0.pathExtension == "jpg" }).count
|
2021-11-08 21:58:55 +01:00
|
|
|
}
|
|
|
|
|
2022-05-24 14:47:50 +02:00
|
|
|
/**
|
|
|
|
Get the image count of a cap.
|
|
|
|
*/
|
2021-11-08 21:58:55 +01:00
|
|
|
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
|
2022-05-24 14:47:50 +02:00
|
|
|
|
|
|
|
func set(name: String, for capId: Int) throws {
|
|
|
|
guard var cap = caps[capId] else {
|
|
|
|
let url = server.folder(of: capId)
|
2021-11-08 21:58:55 +01:00
|
|
|
if !fm.fileExists(atPath: url.path) {
|
|
|
|
try fm.createDirectory(at: url, withIntermediateDirectories: false)
|
|
|
|
}
|
2022-05-24 14:47:50 +02:00
|
|
|
caps[capId] = Cap(id: capId, name: name, count: 0, mainImage: 0, classifierVersion: nextClassifierVersion)
|
|
|
|
log("Added cap \(capId)")
|
|
|
|
return
|
2021-11-08 21:58:55 +01:00
|
|
|
}
|
2022-05-24 14:47:50 +02:00
|
|
|
cap.name = name
|
|
|
|
caps[capId] = cap
|
|
|
|
log("Set name for cap \(capId)")
|
2021-11-08 21:58:55 +01:00
|
|
|
}
|
2022-05-24 14:47:50 +02:00
|
|
|
|
2021-11-08 21:58:55 +01:00
|
|
|
// 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
|
2022-05-24 14:47:50 +02:00
|
|
|
- Throws: `CapError.unknownId`, if the cap doesn't exist. `CapError.dataInconsistency` if an image already exists for the current count.
|
2021-11-08 21:58:55 +01:00
|
|
|
*/
|
|
|
|
func save(image data: Data, for cap: Int) throws -> Int {
|
2022-05-24 14:47:50 +02:00
|
|
|
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)
|
2021-11-08 21:58:55 +01:00
|
|
|
}
|
|
|
|
try data.write(to: f)
|
2022-05-24 14:47:50 +02:00
|
|
|
count += 1
|
|
|
|
caps[cap]!.count = count
|
|
|
|
log("Added image \(id) for cap \(cap)")
|
|
|
|
return count
|
2021-11-08 21:58:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func switchMainImage(to version: Int, for cap: Int) throws {
|
|
|
|
let file2 = file(of: cap, version: version)
|
|
|
|
guard fm.fileExists(atPath: file2.path) else {
|
|
|
|
log("No image \(version) for cap \(cap)")
|
|
|
|
throw CapError.invalidFile
|
|
|
|
}
|
2022-05-24 14:47:50 +02:00
|
|
|
caps[cap]?.mainImage = version
|
2021-11-08 21:58:55 +01:00
|
|
|
log("Switched cap \(cap) to version \(version)")
|
|
|
|
}
|
|
|
|
}
|