This commit is contained in:
2025-10-05 12:42:02 -06:00
parent 4bbd422c09
commit b8f874a433
18 changed files with 147 additions and 432 deletions

View File

@@ -4,54 +4,36 @@
import CryptoKit
import Foundation
/// Utility for creating consistent image filenames across iOS and Android platforms.
/// Uses deterministic naming based on problem ID and timestamp to ensure sync compatibility.
/// Utility for creating consistent image filenames across platforms
class ImageNamingUtils {
private static let imageExtension = ".jpg"
private static let hashLength = 12 // First 12 chars of SHA-256
private static let hashLength = 12
/// Generates a deterministic filename for a problem image.
/// Format: "problem_{hash}_{index}.jpg"
///
/// - Parameters:
/// - problemId: The ID of the problem this image belongs to
/// - timestamp: ISO8601 timestamp when the image was created
/// - imageIndex: The index of this image for the problem (0, 1, 2, etc.)
/// - Returns: A consistent filename that will be the same across platforms
/// Generates a deterministic filename for a problem image
static func generateImageFilename(problemId: String, timestamp: String, imageIndex: Int)
-> String
{
// Create a deterministic hash from problemId + timestamp + index
let input = "\(problemId)_\(timestamp)_\(imageIndex)"
let hash = createHash(from: input)
return "problem_\(hash)_\(imageIndex)\(imageExtension)"
}
/// Generates a deterministic filename for a problem image using current timestamp.
///
/// - Parameters:
/// - problemId: The ID of the problem this image belongs to
/// - imageIndex: The index of this image for the problem (0, 1, 2, etc.)
/// - Returns: A consistent filename
/// 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 created by this utility.
/// Returns nil if the filename doesn't match our naming convention.
///
/// - Parameter filename: The image filename
/// - Returns: The hash identifier or nil if not a valid filename
/// Extracts problem ID from an image filename
static func extractProblemIdFromFilename(_ filename: String) -> String? {
guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else {
return nil
}
// Format: problem_{hash}_{index}.jpg
let nameWithoutExtension = String(filename.dropLast(imageExtension.count))
let parts = nameWithoutExtension.components(separatedBy: "_")
@@ -59,14 +41,10 @@ class ImageNamingUtils {
return nil
}
// Return the hash as identifier
return parts[1]
}
/// Validates if a filename follows our naming convention.
///
/// - Parameter filename: The filename to validate
/// - Returns: true if it matches our convention, false otherwise
/// Validates if a filename follows our naming convention
static func isValidImageFilename(_ filename: String) -> Bool {
guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else {
return false
@@ -79,32 +57,19 @@ class ImageNamingUtils {
&& Int(parts[2]) != nil
}
/// Migrates an existing UUID-based filename to our naming convention.
/// This is used during sync to rename downloaded images.
///
/// - Parameters:
/// - oldFilename: The existing filename (UUID-based)
/// - problemId: The problem ID this image belongs to
/// - imageIndex: The index of this image
/// - Returns: The new filename following our convention
/// Migrates an existing filename to our naming convention
static func migrateFilename(oldFilename: String, problemId: String, imageIndex: Int) -> String {
// If it's already using our convention, keep it
if isValidImageFilename(oldFilename) {
return oldFilename
}
// Generate new deterministic name
// Use current timestamp to maintain some consistency
let timestamp = ISO8601DateFormatter().string(from: Date())
return generateImageFilename(
problemId: problemId, timestamp: timestamp, imageIndex: imageIndex)
}
/// Creates a deterministic hash from input string.
/// Uses SHA-256 and takes first 12 characters for filename safety.
///
/// - Parameter input: The input string to hash
/// - Returns: First 12 characters of SHA-256 hash in lowercase
/// 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)
@@ -112,13 +77,7 @@ class ImageNamingUtils {
return String(hashString.prefix(hashLength))
}
/// Batch renames images for a problem to use our naming convention.
/// Returns a mapping of old filename -> new filename.
///
/// - Parameters:
/// - problemId: The problem ID
/// - existingFilenames: List of current image filenames for this problem
/// - Returns: Dictionary mapping old filename to new filename
/// Batch renames images for a problem to use our naming convention
static func batchRenameForProblem(problemId: String, existingFilenames: [String]) -> [String:
String]
{
@@ -135,10 +94,7 @@ class ImageNamingUtils {
return renameMap
}
/// Validates that a collection of filenames follow our naming convention.
///
/// - Parameter filenames: Array of filenames to validate
/// - Returns: Dictionary with validation results
/// Validates that a collection of filenames follow our naming convention
static func validateFilenames(_ filenames: [String]) -> ImageValidationResult {
var validImages: [String] = []
var invalidImages: [String] = []
@@ -159,7 +115,7 @@ class ImageNamingUtils {
}
}
/// Result of image filename validation
// Result of image filename validation
struct ImageValidationResult {
let totalImages: Int
let validImages: [String]