diff --git a/Sources/Photochk/Core/PhotoScanner.swift b/Sources/Photochk/Core/PhotoScanner.swift new file mode 100644 index 0000000..f7becb8 --- /dev/null +++ b/Sources/Photochk/Core/PhotoScanner.swift @@ -0,0 +1,74 @@ +import Foundation +import UniformTypeIdentifiers + +/// 走査で見つかった画像ファイル1件。中身はまだ読んでいない。 +struct ScannedFile: Hashable, Sendable { + let url: URL + let fileSize: Int64 + let modificationDate: Date + let creationDate: Date? +} + +enum PhotoScanner { + /// フォルダ配下の画像ファイルを列挙する。 + /// + /// UTType が `.image` に conform するものだけを拾うので、JPEG / PNG / HEIC / TIFF に加えて + /// 各社の RAW も自動的に対象になる。写真ライブラリなどのパッケージは中に入らない。 + static func scan(root: URL, includesSubfolders: Bool) throws -> [ScannedFile] { + let keys: [URLResourceKey] = [ + .isRegularFileKey, + .contentTypeKey, + .fileSizeKey, + .contentModificationDateKey, + .creationDateKey, + ] + + var options: FileManager.DirectoryEnumerationOptions = [ + .skipsHiddenFiles, + .skipsPackageDescendants, + ] + if !includesSubfolders { + options.insert(.skipsSubdirectoryDescendants) + } + + guard let enumerator = FileManager.default.enumerator( + at: root, + includingPropertiesForKeys: keys, + options: options + ) else { + throw ScanError.cannotEnumerate(root) + } + + var results: [ScannedFile] = [] + for case let url as URL in enumerator { + try Task.checkCancellation() + guard let values = try? url.resourceValues(forKeys: Set(keys)), + values.isRegularFile == true, + let type = values.contentType, + type.conforms(to: .image), + let size = values.fileSize, size > 0 + else { continue } + + results.append( + ScannedFile( + url: url, + fileSize: Int64(size), + modificationDate: values.contentModificationDate ?? .distantPast, + creationDate: values.creationDate + ) + ) + } + return results + } +} + +enum ScanError: LocalizedError { + case cannotEnumerate(URL) + + var errorDescription: String? { + switch self { + case .cannotEnumerate(let url): + return "「\(url.lastPathComponent)」の中身を読み取れませんでした。" + } + } +}