diff --git a/Resources/paths.conf b/Resources/paths.conf new file mode 100644 index 0000000..bd95f79 --- /dev/null +++ b/Resources/paths.conf @@ -0,0 +1,3 @@ +/data/logs/capserver/server.log +/data/public/capserver + diff --git a/Sources/App/CapServer.swift b/Sources/App/CapServer.swift new file mode 100644 index 0000000..2ed8eff --- /dev/null +++ b/Sources/App/CapServer.swift @@ -0,0 +1,147 @@ +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)") + } +} diff --git a/Sources/App/Error.swift b/Sources/App/Error.swift index 79a89f2..2c7f221 100644 --- a/Sources/App/Error.swift +++ b/Sources/App/Error.swift @@ -14,6 +14,7 @@ enum CapError: Error { case invalidBody case dataInconsistency case invalidFile + case invalidConfiguration var response: HTTPResponseStatus { switch self { @@ -25,6 +26,8 @@ enum CapError: Error { case .dataInconsistency: return .conflict /// 412 case .invalidFile: return .preconditionFailed + /// 500 + case .invalidConfiguration: return .internalServerError } } } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index ceea56a..ab16715 100755 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -1,5 +1,8 @@ import Vapor + +private(set) var server: CapServer! + // configures your application public func configure(_ app: Application) throws { // uncomment to serve files from /Public folder @@ -8,6 +11,23 @@ public func configure(_ app: Application) throws { app.http.server.configuration.port = 6001 app.routes.defaultMaxBodySize = "2mb" + let configFile = URL(fileURLWithPath: app.directory.resourcesDirectory) + .appendingPathComponent("paths.conf") + let configData = try String(contentsOf: configFile) + .components(separatedBy: "\n") + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + guard configData.count == 2 else { + throw CapError.invalidConfiguration + } + let logFile = URL(fileURLWithPath: configData[0]) + let publicDirectory = URL(fileURLWithPath: configData[1]) + + try Log.set(logFile: logFile.path) + + server = try CapServer(in: publicDirectory) + try server.loadCapNames() + // Register routes to the router try routes(app) // Configure the rest of your application here diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 132bf93..ae8fe3d 100755 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -1,77 +1,17 @@ import Vapor -// MARK: Paths - -private let baseFolder = URL(fileURLWithPath: "/caps") - -private let logFile = baseFolder.appendingPathComponent("logs/server.log").path - -private let publicFolder = baseFolder.appendingPathComponent("Public") - -private let imageFolder = publicFolder.appendingPathComponent("images") - -private let nameFile = publicFolder.appendingPathComponent("names.txt") - -private let tempImageFile = publicFolder.appendingPathComponent("temp.jpg") - -// MARK: Variables - -private let fm = FileManager.default - -private var caps = [String]() - -// 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: Helper - -private func folder(of cap: Int) -> URL { - imageFolder.appendingPathComponent(String(format: "%04d", cap)) -} - -private func file(of cap: Int, version: Int) -> URL { - folder(of: cap).appendingPathComponent(String(format: "%04d-%02d.jpg", cap, version)) -} - -private func countImages(in folder: URL) throws -> Int { - try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: nil).filter({ $0.pathExtension == "jpg" }).count -} - -private 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: Routes - /// Register your application's routes here. /// /// [Learn More →](https://docs.vapor.codes/3.0/getting-started/structure/#routesswift) func routes(_ app: Application) throws { - try Log.set(logFile: logFile) - try loadCapNames() - // 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) } - 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)! + return try server.name(for: cap) } // Set the name of a cap @@ -80,30 +20,11 @@ func routes(_ app: Application) throws { log("Invalid parameter for cap") throw Abort(.badRequest) } - let index = cap - 1 guard let buffer = request.body.data, let name = String(data: Data(buffer: buffer), encoding: .utf8) else { log("Invalid body data") throw CapError.invalidBody } - 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 = 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: nameFile) - + try server.set(name: name, for: cap) } // Upload an image @@ -117,14 +38,8 @@ func routes(_ app: Application) throws { throw CapError.invalidBody } let data = Data(buffer: buffer) - 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)".data(using: .utf8)! + let newCount = try server.save(image: data, for: cap) + return "\(newCount)".data(using: .utf8)! } // Get count of a cap @@ -133,13 +48,13 @@ func routes(_ app: Application) throws { log("Invalid parameter for cap") throw Abort(.badRequest) } - let c = try count(of: cap) + let c = try server.count(of: cap) return "\(c)".data(using: .utf8)! } // Get the count of all caps app.getCatching("counts") { request -> Data in - Data(try (1...caps.count).map({ UInt8(try count(of: $0)) })) + try server.getCountData() } // Set a different version as the main image @@ -152,23 +67,6 @@ func routes(_ app: Application) throws { log("Invalid parameter for cap version") throw Abort(.badRequest) } - 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)") + try server.switchMainImage(to: version, for: cap) } }