import Foundation import Logging /// Derives a version token from the contents of the static files directory. /// /// The token folds every file under the directory — its relative path and its bytes, in a stable order — into one FNV-1a digest, so it changes whenever any /// asset changes and agrees across the instances of a deployment. The pages append it to their asset URLs (`?v=`), which lets the assets be /// served with a long-lived, immutable cache policy: a deploy that changes an asset changes the URLs pointing at it, so no client ever revalidates or holds a /// stale copy. public struct FingerprintAssets: Sendable { // MARK: Properties /// The logger unreadable files are reported to, or `nil` to skip them silently. private let logger: Logger? // MARK: Initializers /// Creates an asset fingerprinting method. /// - Parameter logger: the logger unreadable files are reported to, or `nil` (the default) to skip them silently. public init( logger: Logger? = nil ) { self.logger = logger } // MARK: Functions /// Fingerprints the static files under the given directory. /// /// A file that cannot be read is reported to the ``logger`` and left out of the token, so its /// later changes would not bust caches — a warning there usually points at a permissions /// problem in the deployment. /// - Parameter path: the directory the static files are served from. /// - Returns: the version token, or `nil` when the directory holds no readable files (asset URLs are then left unversioned). public func callAsFunction( _ path: String ) -> String? { let manager = FileManager.default guard let enumerated = manager.enumerator(atPath: path) else { return nil } // The path-based enumerator yields paths relative to the directory, so the token depends // only on the directory's contents — never on where the directory itself lives (the // URL-based enumerator standardizes symlinked bases, e.g. `/var/…` to `/private/var/…`, // which would leak the absolute path into the hash). var files: [String] = [] while let relativePath = enumerated.nextObject() as? String { if enumerated.fileAttributes?[.type] as? FileAttributeType == .typeRegular { files.append(relativePath) } } var hash = FNV1aHash() var hashed = false for relativePath in files.sorted() { guard let contents = manager.contents( atPath: "\(path)/\(relativePath)" ) else { logger?.warning( "Static file could not be read while fingerprinting; the version token will not reflect it.", metadata: ["path": "\(relativePath)"] ) continue } hash.combine(Array(relativePath.utf8)) hash.combine(contents) hashed = true } guard hashed else { return nil } return hash.digest } }