Files
Ascently/ios/OpenClimb/Utils/ImageNamingUtils.swift

133 lines
4.4 KiB
Swift

//
// ImageNamingUtils.swift
import CryptoKit
import Foundation
/// Utility for creating consistent image filenames across platforms
class ImageNamingUtils {
private static let imageExtension = ".jpg"
private static let hashLength = 12
/// Generates a deterministic filename for a problem image
static func generateImageFilename(problemId: String, timestamp: String, imageIndex: Int)
-> String
{
let input = "\(problemId)_\(timestamp)_\(imageIndex)"
let hash = createHash(from: input)
return "problem_\(hash)_\(imageIndex)\(imageExtension)"
}
/// Generates a deterministic filename using current timestamp
static func generateImageFilename(problemId: String, imageIndex: Int) -> String {
let timestamp = ISO8601DateFormatter().string(from: Date())
return generateImageFilename(
problemId: problemId, timestamp: timestamp, imageIndex: imageIndex)
}
/// Extracts problem ID from an image filename
static func extractProblemIdFromFilename(_ filename: String) -> String? {
guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else {
return nil
}
let nameWithoutExtension = String(filename.dropLast(imageExtension.count))
let parts = nameWithoutExtension.components(separatedBy: "_")
guard parts.count == 3 && parts[0] == "problem" else {
return nil
}
return parts[1]
}
/// Validates if a filename follows our naming convention
static func isValidImageFilename(_ filename: String) -> Bool {
guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else {
return false
}
let nameWithoutExtension = String(filename.dropLast(imageExtension.count))
let parts = nameWithoutExtension.components(separatedBy: "_")
return parts.count == 3 && parts[0] == "problem" && parts[1].count == hashLength
&& Int(parts[2]) != nil
}
/// Migrates an existing filename to our naming convention
static func migrateFilename(oldFilename: String, problemId: String, imageIndex: Int) -> String {
if isValidImageFilename(oldFilename) {
return oldFilename
}
let timestamp = ISO8601DateFormatter().string(from: Date())
return generateImageFilename(
problemId: problemId, timestamp: timestamp, imageIndex: imageIndex)
}
/// Creates a deterministic hash from input string
private static func createHash(from input: String) -> String {
let inputData = Data(input.utf8)
let hashed = SHA256.hash(data: inputData)
let hashString = hashed.compactMap { String(format: "%02x", $0) }.joined()
return String(hashString.prefix(hashLength))
}
/// Batch renames images for a problem to use our naming convention
static func batchRenameForProblem(problemId: String, existingFilenames: [String]) -> [String:
String]
{
var renameMap: [String: String] = [:]
for (index, oldFilename) in existingFilenames.enumerated() {
let newFilename = migrateFilename(
oldFilename: oldFilename, problemId: problemId, imageIndex: index)
if newFilename != oldFilename {
renameMap[oldFilename] = newFilename
}
}
return renameMap
}
/// Validates that a collection of filenames follow our naming convention
static func validateFilenames(_ filenames: [String]) -> ImageValidationResult {
var validImages: [String] = []
var invalidImages: [String] = []
for filename in filenames {
if isValidImageFilename(filename) {
validImages.append(filename)
} else {
invalidImages.append(filename)
}
}
return ImageValidationResult(
totalImages: filenames.count,
validImages: validImages,
invalidImages: invalidImages
)
}
}
// Result of image filename validation
struct ImageValidationResult {
let totalImages: Int
let validImages: [String]
let invalidImages: [String]
var isAllValid: Bool {
return invalidImages.isEmpty
}
var validPercentage: Double {
guard totalImages > 0 else { return 100.0 }
return (Double(validImages.count) / Double(totalImages)) * 100.0
}
}