From fb8fa41dd08713c5fd78fcc4d63c827825af42e5 Mon Sep 17 00:00:00 2001 From: relikd Date: Wed, 5 Nov 2025 03:44:49 +0100 Subject: [PATCH] ref: URL utils class --- QLAppBundle.xcodeproj/project.pbxproj | 6 ++++ QLPreview/PreviewViewController.swift | 9 ++--- src/Preview+FileInfo.swift | 48 +++++++++++++++++---------- src/URL+File.swift | 12 +++++++ 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 src/URL+File.swift diff --git a/QLAppBundle.xcodeproj/project.pbxproj b/QLAppBundle.xcodeproj/project.pbxproj index 6d4142d..d0190bb 100644 --- a/QLAppBundle.xcodeproj/project.pbxproj +++ b/QLAppBundle.xcodeproj/project.pbxproj @@ -37,6 +37,8 @@ 547F52F72EB2CAC7002B6D5F /* Preview+Footer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F52F62EB2CAC7002B6D5F /* Preview+Footer.swift */; }; 547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547F52F82EB2CBAB002B6D5F /* Date+Format.swift */; }; 549E3B9F2EBA9D2500ADFF56 /* QLAppBundle Preview Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 54442C202E378BAF008A870E /* QLAppBundle Preview Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */; }; + 549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */; }; 54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AE5BFD2EB3DB1000B4CFC7 /* ThumbnailProvider.swift */; }; 54B6FFEE2EB6A847007397C0 /* AssetCarReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B6FFEC2EB6A847007397C0 /* AssetCarReader.swift */; }; 54B6FFEF2EB6A8E0007397C0 /* CoreUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54D3A6F52EA4610B001EF4F6 /* CoreUI.framework */; }; @@ -152,6 +154,7 @@ 547F52FB2EB37F10002B6D5F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 547F52FC2EB37F3A002B6D5F /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 549E3B9E2EBA8FDA00ADFF56 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+File.swift"; sourceTree = ""; }; 54AE5BFB2EB3DB1000B4CFC7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54AE5BFC2EB3DB1000B4CFC7 /* QLThumbnail.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QLThumbnail.entitlements; sourceTree = ""; }; 54AE5BFD2EB3DB1000B4CFC7 /* ThumbnailProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailProvider.swift; sourceTree = ""; }; @@ -222,6 +225,7 @@ 547F52E92EB2C672002B6D5F /* Preview+FileInfo.swift */, 547F52F62EB2CAC7002B6D5F /* Preview+Footer.swift */, 5405CF642EA1376B00613856 /* Zip.swift */, + 549E3BA02EBAE7D300ADFF56 /* URL+File.swift */, 54D3A6EF2EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift */, 547F52F82EB2CBAB002B6D5F /* Date+Format.swift */, ); @@ -540,6 +544,7 @@ 547F52E82EB2C41C002B6D5F /* PreviewGenerator.swift in Sources */, 547F52EB2EB2C672002B6D5F /* Preview+FileInfo.swift in Sources */, 547F52ED2EB2C822002B6D5F /* Preview+AppInfo.swift in Sources */, + 549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */, 547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */, 5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */, 547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */, @@ -556,6 +561,7 @@ 547899722EB38F3D00F96B80 /* MetaInfo.swift in Sources */, 547899732EB38F3D00F96B80 /* NSBezierPath+RoundedRect.swift in Sources */, 54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */, + 549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */, 547899752EB38F3D00F96B80 /* AppIcon.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/QLPreview/PreviewViewController.swift b/QLPreview/PreviewViewController.swift index cba0361..6264e0c 100644 --- a/QLPreview/PreviewViewController.swift +++ b/QLPreview/PreviewViewController.swift @@ -13,12 +13,9 @@ class PreviewViewController: NSViewController, QLPreviewingController { /// Load resource file either from user documents dir (if exists) or app bundle (default). func bundleFile(filename: String, ext: String) throws -> String { - if let appSupport = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - let override = appSupport.appendingPathComponent(filename + "." + ext) - if FileManager.default.fileExists(atPath: override.path) { - return try String(contentsOfFile: override.path, encoding: .utf8) - } - // else: do NOT copy! Breaks on future updates + if let userFile = URL.UserModDir?.appendingPathComponent(filename + "." + ext, isDirectory: false), userFile.exists() { + return try String(contentsOf: userFile, encoding: .utf8) + // else: do NOT copy! Breaks on future updates } // else, load bundle file let path = Bundle.main.url(forResource: filename, withExtension: ext) diff --git a/src/Preview+FileInfo.swift b/src/Preview+FileInfo.swift index d378091..336e50d 100644 --- a/src/Preview+FileInfo.swift +++ b/src/Preview+FileInfo.swift @@ -1,27 +1,12 @@ import Foundation extension PreviewGenerator { - /// Calculate file / folder size. - private func getFileSize(_ path: String) -> Int64 { - var isDir: ObjCBool = false - FileManager.default.fileExists(atPath: path, isDirectory: &isDir) - if !isDir.boolValue { - return try! FileManager.default.attributesOfItem(atPath: path)[.size] as! Int64 - } - var fileSize: Int64 = 0 - for child in try! FileManager.default.subpathsOfDirectory(atPath: path) { - fileSize += try! FileManager.default.attributesOfItem(atPath: path + "/" + child)[.size] as! Int64 - } - return fileSize - } - /// Process meta information about the file itself. Like file size and last modification. mutating func procFileInfo(_ url: URL) { - let attrs = try? FileManager.default.attributesOfItem(atPath: url.path) self.apply([ "FileName": escapeXML(url.lastPathComponent), - "FileSize": ByteCountFormatter.string(fromByteCount: getFileSize(url.path), countStyle: .file), - "FileModified": (attrs?[.modificationDate] as? Date)?.mediumFormat() ?? "", + "FileSize": url.fileSizeHuman(), + "FileModified": url.modificationDate()?.mediumFormat() ?? "", ]) } } @@ -36,3 +21,32 @@ private func escapeXML(_ stringToEscape: String) -> String { .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") } + + +extension URL { + /// Last modification date of file (or folder) + @inlinable func modificationDate() -> Date? { + (try? FileManager.default.attributesOfItem(atPath: self.path))?[.modificationDate] as? Date + } + + /// Calls `fileSize()`. Will convert `Int` to human readable `String`. + func fileSizeHuman() -> String { + ByteCountFormatter.string(fromByteCount: self.fileSize(), countStyle: .file) + } + + // MARK: - private methods + + /// Calculate file or folder size. + private func fileSize() -> Int64 { + var isDir: ObjCBool = false + FileManager.default.fileExists(atPath: self.path, isDirectory: &isDir) + if !isDir.boolValue { + return try! FileManager.default.attributesOfItem(atPath: self.path)[.size] as! Int64 + } + var fileSize: Int64 = 0 + for child in try! FileManager.default.subpathsOfDirectory(atPath: self.path) { + fileSize += try! FileManager.default.attributesOfItem(atPath: self.path + "/" + child)[.size] as! Int64 + } + return fileSize + } +} diff --git a/src/URL+File.swift b/src/URL+File.swift new file mode 100644 index 0000000..0750ca7 --- /dev/null +++ b/src/URL+File.swift @@ -0,0 +1,12 @@ +import Foundation + +extension URL { + /// Folder where user can mofifications to html template + static let UserModDir: URL? = + FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + + /// Returns `true` if file or folder exists. + @inlinable func exists() -> Bool { + FileManager.default.fileExists(atPath: self.path) + } +}