Update authentication and metrics logging
This commit is contained in:
parent
f3ee7a4fb4
commit
c5ce5414a9
@ -8,7 +8,7 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/vapor/vapor", from: "4.0.0"),
|
.package(url: "https://github.com/vapor/vapor", from: "4.0.0"),
|
||||||
.package(url: "https://github.com/christophhagen/Clairvoyant", from: "0.2.0"),
|
.package(url: "https://github.com/christophhagen/Clairvoyant", branch: "main"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(name: "App",
|
.target(name: "App",
|
||||||
|
39
Sources/App/Authenticator.swift
Normal file
39
Sources/App/Authenticator.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Foundation
|
||||||
|
import Clairvoyant
|
||||||
|
import Vapor
|
||||||
|
|
||||||
|
final class Authenticator: MetricAccessAuthenticator {
|
||||||
|
|
||||||
|
private var writers: Set<String>
|
||||||
|
|
||||||
|
init(writers: [String]) {
|
||||||
|
self.writers = Set(writers)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func metricAccess(isAllowedForToken accessToken: Data) -> Bool {
|
||||||
|
guard let key = String(data: accessToken, encoding: .utf8) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasAuthorization(for: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func authorize(_ request: Request) throws {
|
||||||
|
guard let key = request.headers.first(name: "key") else {
|
||||||
|
throw Abort(.badRequest) // 400
|
||||||
|
}
|
||||||
|
guard hasAuthorization(for: key) else {
|
||||||
|
throw Abort(.forbidden) // 403
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
Sources/App/CapServer+Routes.swift
Executable file
141
Sources/App/CapServer+Routes.swift
Executable file
@ -0,0 +1,141 @@
|
|||||||
|
import Vapor
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// The decoder to extract caps from JSON payloads given to the `cap` route.
|
||||||
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
/// The date formatter to decode dates in requests
|
||||||
|
private let dateFormatter: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateFormat = "yy-MM-dd-HH-mm-ss"
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
extension CapServer {
|
||||||
|
|
||||||
|
private func ensureOperability() throws {
|
||||||
|
guard isOperational else {
|
||||||
|
throw Abort(.noContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerRoutes(with app: Application, authenticator: Authenticator) {
|
||||||
|
app.get("version") { _ in
|
||||||
|
try self.ensureOperability()
|
||||||
|
return "\(self.classifierVersion)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add or change a cap
|
||||||
|
app.postCatching("cap") { request in
|
||||||
|
try self.ensureOperability()
|
||||||
|
try authenticator.authorize(request)
|
||||||
|
let data = try request.getBodyData()
|
||||||
|
let cap = try decoder.decode(Cap.self, from: data)
|
||||||
|
try self.addOrUpdate(cap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload an image
|
||||||
|
app.postCatching("images", ":n") { request in
|
||||||
|
try self.ensureOperability()
|
||||||
|
try authenticator.authorize(request)
|
||||||
|
guard let cap = request.parameters.get("n", as: Int.self) else {
|
||||||
|
log("Invalid parameter for cap")
|
||||||
|
throw Abort(.badRequest)
|
||||||
|
}
|
||||||
|
let data = try request.getBodyData()
|
||||||
|
try self.save(image: data, for: cap)
|
||||||
|
}
|
||||||
|
// Update the classifier
|
||||||
|
app.on(.POST, "classifier", ":version", body: .collect(maxSize: "50mb")) { request -> HTTPStatus in
|
||||||
|
guard let version = request.parameters.get("version", as: Int.self) else {
|
||||||
|
log("Invalid parameter for version")
|
||||||
|
throw Abort(.badRequest)
|
||||||
|
}
|
||||||
|
guard version > self.classifierVersion else {
|
||||||
|
throw Abort(.alreadyReported) // 208
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.ensureOperability()
|
||||||
|
try authenticator.authorize(request)
|
||||||
|
let data = try request.getBodyData()
|
||||||
|
try self.save(classifier: data, version: version)
|
||||||
|
return .ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the trained classes
|
||||||
|
app.postCatching("classes", ":date") { request in
|
||||||
|
guard let dateString = request.parameters.get("date") else {
|
||||||
|
log("Invalid parameter for date")
|
||||||
|
throw Abort(.badRequest)
|
||||||
|
}
|
||||||
|
guard let date = dateFormatter.date(from: dateString) else {
|
||||||
|
log("Invalid date specification")
|
||||||
|
throw Abort(.badRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.ensureOperability()
|
||||||
|
try authenticator.authorize(request)
|
||||||
|
let body = try request.getStringBody()
|
||||||
|
|
||||||
|
self.updateTrainedClasses(content: body)
|
||||||
|
self.removeAllEntriesInImageChangeList(before: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the list of missing thumbnails
|
||||||
|
app.get("thumbnails", "missing") { request in
|
||||||
|
try self.ensureOperability()
|
||||||
|
let missingThumbnails = self.getListOfMissingThumbnails()
|
||||||
|
return missingThumbnails.map(String.init).joined(separator: ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload the thumbnail of a cap
|
||||||
|
app.postCatching("thumbnails", ":cap") { request in
|
||||||
|
guard let cap = request.parameters.get("cap", as: Int.self) else {
|
||||||
|
log("Invalid cap parameter for thumbnail upload")
|
||||||
|
throw Abort(.badRequest)
|
||||||
|
}
|
||||||
|
try self.ensureOperability()
|
||||||
|
try authenticator.authorize(request)
|
||||||
|
let data = try request.getBodyData()
|
||||||
|
self.saveThumbnail(data, for: cap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the image of a cap
|
||||||
|
app.postCatching("delete", ":cap", ":version") { request in
|
||||||
|
guard let cap = request.parameters.get("cap", as: Int.self) else {
|
||||||
|
log("Invalid cap parameter for image deletion")
|
||||||
|
throw Abort(.badRequest)
|
||||||
|
}
|
||||||
|
guard let version = request.parameters.get("version", as: Int.self) else {
|
||||||
|
log("Invalid version parameter for image deletion")
|
||||||
|
throw Abort(.badRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.ensureOperability()
|
||||||
|
try authenticator.authorize(request)
|
||||||
|
guard self.deleteImage(version: version, for: cap) else {
|
||||||
|
throw Abort(.gone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Request {
|
||||||
|
|
||||||
|
func getBodyData() throws -> Data {
|
||||||
|
guard let buffer = body.data else {
|
||||||
|
log("Missing body data")
|
||||||
|
throw CapError.invalidBody
|
||||||
|
}
|
||||||
|
return Data(buffer: buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringBody() throws -> String {
|
||||||
|
let data = try getBodyData()
|
||||||
|
guard let content = String(data: data, encoding: .utf8) else {
|
||||||
|
log("Invalid string body")
|
||||||
|
throw CapError.invalidBody
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import Foundation
|
|||||||
import Vapor
|
import Vapor
|
||||||
import Clairvoyant
|
import Clairvoyant
|
||||||
|
|
||||||
final class CapServer: ServerOwner {
|
final class CapServer {
|
||||||
|
|
||||||
// MARK: Paths
|
// MARK: Paths
|
||||||
|
|
||||||
@ -28,10 +28,12 @@ final class CapServer: ServerOwner {
|
|||||||
private let fm = FileManager.default
|
private let fm = FileManager.default
|
||||||
|
|
||||||
private let changedImageEntryDateFormatter: DateFormatter
|
private let changedImageEntryDateFormatter: DateFormatter
|
||||||
|
|
||||||
|
/// Indicates that the data is loaded
|
||||||
|
private(set) var isOperational = false
|
||||||
|
|
||||||
// MARK: Caps
|
// MARK: Caps
|
||||||
|
|
||||||
private var writers: Set<String>
|
|
||||||
|
|
||||||
/// The changed images not yet written to disk
|
/// The changed images not yet written to disk
|
||||||
private var unwrittenImageChanges: [(cap: Int, image: Int)] = []
|
private var unwrittenImageChanges: [(cap: Int, image: Int)] = []
|
||||||
@ -39,7 +41,7 @@ final class CapServer: ServerOwner {
|
|||||||
var classifierVersion: Int = 0 {
|
var classifierVersion: Int = 0 {
|
||||||
didSet {
|
didSet {
|
||||||
writeClassifierVersion()
|
writeClassifierVersion()
|
||||||
updateMonitoredClassifierVersionProperty()
|
classifierMetric.update(classifierVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +62,8 @@ final class CapServer: ServerOwner {
|
|||||||
private var caps = [Int: Cap]() {
|
private var caps = [Int: Cap]() {
|
||||||
didSet {
|
didSet {
|
||||||
scheduleSave()
|
scheduleSave()
|
||||||
updateMonitoredPropertiesOnCapChange()
|
capCountMetric.update(caps.count)
|
||||||
|
imageCountMetric.update(imageCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +79,7 @@ final class CapServer: ServerOwner {
|
|||||||
caps.reduce(0) { $0 + $1.value.count }
|
caps.reduce(0) { $0 + $1.value.count }
|
||||||
}
|
}
|
||||||
|
|
||||||
init(in folder: URL, writers: [String]) {
|
init(in folder: URL) {
|
||||||
self.imageFolder = folder.appendingPathComponent("images")
|
self.imageFolder = folder.appendingPathComponent("images")
|
||||||
self.thumbnailFolder = folder.appendingPathComponent("thumbnails")
|
self.thumbnailFolder = folder.appendingPathComponent("thumbnails")
|
||||||
self.gridCountFile = folder.appendingPathComponent("count.js")
|
self.gridCountFile = folder.appendingPathComponent("count.js")
|
||||||
@ -85,7 +88,6 @@ final class CapServer: ServerOwner {
|
|||||||
self.classifierVersionFile = folder.appendingPathComponent("classifier.version")
|
self.classifierVersionFile = folder.appendingPathComponent("classifier.version")
|
||||||
self.classifierFile = folder.appendingPathComponent("classifier.mlmodel")
|
self.classifierFile = folder.appendingPathComponent("classifier.mlmodel")
|
||||||
self.changedImagesFile = folder.appendingPathComponent("changes.txt")
|
self.changedImagesFile = folder.appendingPathComponent("changes.txt")
|
||||||
self.writers = Set(writers)
|
|
||||||
self.changedImageEntryDateFormatter = DateFormatter()
|
self.changedImageEntryDateFormatter = DateFormatter()
|
||||||
changedImageEntryDateFormatter.dateFormat = "yy-MM-dd-HH-mm-ss"
|
changedImageEntryDateFormatter.dateFormat = "yy-MM-dd-HH-mm-ss"
|
||||||
}
|
}
|
||||||
@ -97,6 +99,7 @@ final class CapServer: ServerOwner {
|
|||||||
updateGridCapCount()
|
updateGridCapCount()
|
||||||
try ensureExistenceOfChangedImagesFile()
|
try ensureExistenceOfChangedImagesFile()
|
||||||
organizeImages()
|
organizeImages()
|
||||||
|
isOperational = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadClassifierVersion(at url: URL) {
|
private func loadClassifierVersion(at url: URL) {
|
||||||
@ -234,17 +237,6 @@ final class CapServer: ServerOwner {
|
|||||||
folder(of: cap).appendingPathComponent(String(format: "%04d-%02d.jpg", cap, version))
|
folder(of: cap).appendingPathComponent(String(format: "%04d-%02d.jpg", cap, version))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Counts
|
// MARK: Counts
|
||||||
|
|
||||||
private func images(in folder: URL) throws -> [URL] {
|
private func images(in folder: URL) throws -> [URL] {
|
||||||
@ -466,82 +458,11 @@ final class CapServer: ServerOwner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: ServerOwner
|
|
||||||
|
|
||||||
let authenticationMethod: PropertyAuthenticationMethod = .accessToken
|
|
||||||
|
|
||||||
func hasReadPermission(for property: UInt32, accessData: Data) -> Bool {
|
|
||||||
guard let key = String(data: accessData, encoding: .utf8) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return writers.contains(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasWritePermission(for property: UInt32, accessData: Data) -> Bool {
|
|
||||||
guard let key = String(data: accessData, encoding: .utf8) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return writers.contains(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasListAccessPermission(_ accessData: Data) -> Bool {
|
|
||||||
guard let key = String(data: accessData, encoding: .utf8) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return writers.contains(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Monitoring
|
// MARK: Monitoring
|
||||||
|
|
||||||
public let name = "caps"
|
private let capCountMetric = Metric<Int>("caps.count")
|
||||||
|
|
||||||
private let capCountPropertyId = PropertyId(owner: "caps", uniqueId: 2)
|
private let imageCountMetric = Metric<Int>("caps.images")
|
||||||
|
|
||||||
private let imageCountPropertyId = PropertyId(owner: "caps", uniqueId: 3)
|
private let classifierMetric = Metric<Int>("caps.classifier")
|
||||||
|
|
||||||
private let classifierVersionPropertyId = PropertyId(owner: "caps", uniqueId: 4)
|
|
||||||
|
|
||||||
func registerProperties(with monitor: PropertyManager) {
|
|
||||||
let capCountProperty = PropertyRegistration(
|
|
||||||
uniqueId: capCountPropertyId.uniqueId,
|
|
||||||
name: "caps",
|
|
||||||
updates: .continuous,
|
|
||||||
isLogged: true,
|
|
||||||
allowsManualUpdate: false,
|
|
||||||
read: { [weak self] in
|
|
||||||
return (self?.capCount ?? 0).timestamped()
|
|
||||||
})
|
|
||||||
monitor.register(capCountProperty, for: self)
|
|
||||||
|
|
||||||
let imageCountProperty = PropertyRegistration(
|
|
||||||
uniqueId: imageCountPropertyId.uniqueId,
|
|
||||||
name: "images",
|
|
||||||
updates: .continuous,
|
|
||||||
isLogged: true,
|
|
||||||
allowsManualUpdate: false,
|
|
||||||
read: { [weak self] in
|
|
||||||
return (self?.imageCount ?? 0).timestamped()
|
|
||||||
})
|
|
||||||
monitor.register(imageCountProperty, for: self)
|
|
||||||
|
|
||||||
let classifierVersionProperty = PropertyRegistration(
|
|
||||||
uniqueId: classifierVersionPropertyId.uniqueId,
|
|
||||||
name: "classifier",
|
|
||||||
updates: .continuous,
|
|
||||||
isLogged: true,
|
|
||||||
allowsManualUpdate: false,
|
|
||||||
read: { [weak self] in
|
|
||||||
return (self?.classifierVersion ?? 0).timestamped()
|
|
||||||
})
|
|
||||||
monitor.register(classifierVersionProperty, for: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateMonitoredPropertiesOnCapChange() {
|
|
||||||
try? monitor.logChanged(property: capCountPropertyId, value: capCount.timestamped())
|
|
||||||
try? monitor.logChanged(property: imageCountPropertyId, value: imageCount.timestamped())
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateMonitoredClassifierVersionProperty() {
|
|
||||||
try? monitor.logChanged(property: classifierVersionPropertyId, value: classifierVersion.timestamped())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,13 @@ enum CapError: Error {
|
|||||||
HTTP Code: 406
|
HTTP Code: 406
|
||||||
*/
|
*/
|
||||||
case invalidData
|
case invalidData
|
||||||
|
|
||||||
|
/**
|
||||||
|
The server failed to initialize the data and is not operational
|
||||||
|
|
||||||
|
HTTP Code: 204
|
||||||
|
*/
|
||||||
|
case serviceUnavailable
|
||||||
|
|
||||||
var response: HTTPResponseStatus {
|
var response: HTTPResponseStatus {
|
||||||
switch self {
|
switch self {
|
||||||
@ -60,6 +67,7 @@ enum CapError: Error {
|
|||||||
case .invalidFile: return .preconditionFailed
|
case .invalidFile: return .preconditionFailed
|
||||||
/// 500
|
/// 500
|
||||||
case .invalidConfiguration: return .internalServerError
|
case .invalidConfiguration: return .internalServerError
|
||||||
|
case .serviceUnavailable: return .noContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,24 +2,27 @@ import Vapor
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Clairvoyant
|
import Clairvoyant
|
||||||
|
|
||||||
|
|
||||||
private(set) var server: CapServer!
|
|
||||||
private(set) var monitor: PropertyManager!
|
|
||||||
|
|
||||||
public func configure(_ app: Application) throws {
|
public func configure(_ app: Application) throws {
|
||||||
|
|
||||||
let resourceDirectory = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
let resourceDirectory = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
||||||
let publicDirectory = app.directory.publicDirectory
|
let publicDirectory = app.directory.publicDirectory
|
||||||
|
|
||||||
let config = Config(loadFrom: resourceDirectory)
|
let config = Config(loadFrom: resourceDirectory)
|
||||||
|
let authenticator = Authenticator(writers: config.writers)
|
||||||
|
|
||||||
server = CapServer(in: URL(fileURLWithPath: publicDirectory),
|
let monitor = MetricObserver(
|
||||||
writers: config.writers)
|
logFolder: config.logURL,
|
||||||
|
authenticator: authenticator,
|
||||||
|
logMetricId: "caps.log")
|
||||||
|
|
||||||
monitor = .init(logFolder: config.logURL, serverOwner: server)
|
// All new metrics are automatically registered with the standard observer
|
||||||
monitor.update(status: .initializing)
|
MetricObserver.standard = monitor
|
||||||
|
|
||||||
|
let status = Metric<ServerStatus>("caps.status")
|
||||||
|
status.update(.initializing)
|
||||||
|
|
||||||
|
let server = CapServer(in: URL(fileURLWithPath: publicDirectory))
|
||||||
|
|
||||||
server.registerProperties(with: monitor)
|
|
||||||
monitor.registerRoutes(app)
|
monitor.registerRoutes(app)
|
||||||
|
|
||||||
app.http.server.configuration.port = config.port
|
app.http.server.configuration.port = config.port
|
||||||
@ -30,20 +33,19 @@ public func configure(_ app: Application) throws {
|
|||||||
app.middleware.use(middleware)
|
app.middleware.use(middleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register routes to the router
|
||||||
|
server.registerRoutes(with: app, authenticator: authenticator)
|
||||||
|
|
||||||
|
// Initialize the server data
|
||||||
do {
|
do {
|
||||||
try server.loadData()
|
try server.loadData()
|
||||||
|
status.update(.nominal)
|
||||||
} catch {
|
} catch {
|
||||||
monitor.update(status: .initializationFailure)
|
status.update(.initializationFailure)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register routes to the router
|
|
||||||
routes(app)
|
|
||||||
|
|
||||||
monitor.update(status: .nominal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func log(_ message: String) {
|
func log(_ message: String) {
|
||||||
monitor.log(message)
|
MetricObserver.standard?.log(message)
|
||||||
print(message)
|
print(message)
|
||||||
}
|
}
|
||||||
|
@ -1,133 +0,0 @@
|
|||||||
import Vapor
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// The decoder to extract caps from JSON payloads given to the `cap` route.
|
|
||||||
private let decoder = JSONDecoder()
|
|
||||||
|
|
||||||
/// The date formatter to decode dates in requests
|
|
||||||
private let dateFormatter: DateFormatter = {
|
|
||||||
let df = DateFormatter()
|
|
||||||
df.dateFormat = "yy-MM-dd-HH-mm-ss"
|
|
||||||
return df
|
|
||||||
}()
|
|
||||||
|
|
||||||
private func authorize(_ request: Request) throws {
|
|
||||||
guard let key = request.headers.first(name: "key") else {
|
|
||||||
throw Abort(.badRequest) // 400
|
|
||||||
}
|
|
||||||
guard server.hasAuthorization(for: key) else {
|
|
||||||
throw Abort(.forbidden) // 403
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func routes(_ app: Application) {
|
|
||||||
|
|
||||||
app.get("version") { _ in
|
|
||||||
"\(server.classifierVersion)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add or change a cap
|
|
||||||
app.postCatching("cap") { request in
|
|
||||||
try authorize(request)
|
|
||||||
let data = try request.getBodyData()
|
|
||||||
let cap = try decoder.decode(Cap.self, from: data)
|
|
||||||
try server.addOrUpdate(cap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload an image
|
|
||||||
app.postCatching("images", ":n") { request in
|
|
||||||
try authorize(request)
|
|
||||||
guard let cap = request.parameters.get("n", as: Int.self) else {
|
|
||||||
log("Invalid parameter for cap")
|
|
||||||
throw Abort(.badRequest)
|
|
||||||
}
|
|
||||||
let data = try request.getBodyData()
|
|
||||||
try server.save(image: data, for: cap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the classifier
|
|
||||||
app.on(.POST, "classifier", ":version", body: .collect(maxSize: "50mb")) { request -> HTTPStatus in
|
|
||||||
try authorize(request)
|
|
||||||
guard let version = request.parameters.get("version", as: Int.self) else {
|
|
||||||
log("Invalid parameter for version")
|
|
||||||
throw Abort(.badRequest)
|
|
||||||
}
|
|
||||||
guard version > server.classifierVersion else {
|
|
||||||
throw Abort(.alreadyReported) // 208
|
|
||||||
}
|
|
||||||
let data = try request.getBodyData()
|
|
||||||
try server.save(classifier: data, version: version)
|
|
||||||
return .ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the trained classes
|
|
||||||
app.postCatching("classes", ":date") { request in
|
|
||||||
guard let dateString = request.parameters.get("date") else {
|
|
||||||
log("Invalid parameter for date")
|
|
||||||
throw Abort(.badRequest)
|
|
||||||
}
|
|
||||||
guard let date = dateFormatter.date(from: dateString) else {
|
|
||||||
log("Invalid date specification")
|
|
||||||
throw Abort(.badRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
try authorize(request)
|
|
||||||
let body = try request.getStringBody()
|
|
||||||
|
|
||||||
server.updateTrainedClasses(content: body)
|
|
||||||
server.removeAllEntriesInImageChangeList(before: date)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the list of missing thumbnails
|
|
||||||
app.get("thumbnails", "missing") { request in
|
|
||||||
let missingThumbnails = server.getListOfMissingThumbnails()
|
|
||||||
return missingThumbnails.map(String.init).joined(separator: ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload the thumbnail of a cap
|
|
||||||
app.postCatching("thumbnails", ":cap") { request in
|
|
||||||
guard let cap = request.parameters.get("cap", as: Int.self) else {
|
|
||||||
log("Invalid cap parameter for thumbnail upload")
|
|
||||||
throw Abort(.badRequest)
|
|
||||||
}
|
|
||||||
try authorize(request)
|
|
||||||
let data = try request.getBodyData()
|
|
||||||
server.saveThumbnail(data, for: cap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the image of a cap
|
|
||||||
app.postCatching("delete", ":cap", ":version") { request in
|
|
||||||
guard let cap = request.parameters.get("cap", as: Int.self) else {
|
|
||||||
log("Invalid cap parameter for image deletion")
|
|
||||||
throw Abort(.badRequest)
|
|
||||||
}
|
|
||||||
try authorize(request)
|
|
||||||
guard let version = request.parameters.get("version", as: Int.self) else {
|
|
||||||
log("Invalid version parameter for image deletion")
|
|
||||||
throw Abort(.badRequest)
|
|
||||||
}
|
|
||||||
guard server.deleteImage(version: version, for: cap) else {
|
|
||||||
throw Abort(.gone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Request {
|
|
||||||
|
|
||||||
func getBodyData() throws -> Data {
|
|
||||||
guard let buffer = body.data else {
|
|
||||||
log("Missing body data")
|
|
||||||
throw CapError.invalidBody
|
|
||||||
}
|
|
||||||
return Data(buffer: buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStringBody() throws -> String {
|
|
||||||
let data = try getBodyData()
|
|
||||||
guard let content = String(data: data, encoding: .utf8) else {
|
|
||||||
log("Invalid string body")
|
|
||||||
throw CapError.invalidBody
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user