Removing structure of empty folders - swift

I am trying to delete any empty folders in a directory (sometimes folders within folders).
Here's how I'm trying to do it (see below), however it still leaves the last folder.
If there is a better way to approach this?
func removeEmptyFoldersAt(url: URL) {
let folderContents = try? fm.contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsPackageDescendants, .skipsHiddenFiles]).filter({u in
let attr = try? u.resourceValues(forKeys: [.isDirectoryKey])
return attr!.isDirectory!
})
for folder in folderContents! {
let attr = try? folder.resourceValues(forKeys: [.isDirectoryKey])
let contents = try? fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: [], options: [.skipsHiddenFiles,.skipsPackageDescendants])
if attr!.isDirectory! && contents!.count > 0 {
removeEmptyFoldersAt(url: folder)
}
if attr!.isDirectory! && contents?.count == 0 {
try? fm.removeItem(at: folder)
}
}
}

I'd follow a slightly different workflow, I'd have the removeEmptyFoldersAt be more generic in its workflow.
So you can pass it URL and if:
it's a directory, list the contents and recursively call itself with each item
it's a file, just remove it
For example...
extension URL {
var isDirectory: Bool {
return (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory ?? false
}
}
func removeItem(at url: URL) throws {
let fm = FileManager.default
if url.isDirectory {
for item in try fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []) {
try removeItem(at: item)
}
}
try fm.removeItem(at: url)
}
nb: I've not tested this, but this is a common workflow I've used in the past

Related

Swift: FileManager().fileExists(atPath: (fileURL.path)) without knowing extension

)
today I have a problem and I can't find an easy solution.
With:
FileManager().fileExists(atPath:(fileURL.path))
it's simple to find out if a file exist. Actually I have the file name but don't know the extension. How can I use FileManager() to find a file without the extension. Something like .deletingPathExtension() for FileManger().fileExists?
Something like
ls filename.*
You could create a FileManager extension that retrieves the contents of the directory and filters for files as well as the expected filename.
It might look something like this:
extension FileManager {
func urls(of filename: String, in directory: URL) -> [URL]? {
guard let urls = try? contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [])
else { return nil }
return urls.filter { url in
!url.hasDirectoryPath && url.deletingPathExtension().lastPathComponent == filename
}
}
}
Finally, one would call it something like this:
let directory = URL(string: "file:///Users/stephan/tmp")!
if let urls = FileManager.default.urls(of: "test", in: directory) {
for url in urls {
print("do something with url: \(url)")
}
}

How can I enumerate through a hard coded directory?

I cannot work out why hard coding a directory doesn’t work when trying to enumerate through a directory.
I have written a simple function to open a dialog and return a selected folder. The function includes a starting directory (directoryURL below):
func selectFolder(title: String, directoryURL: String = ".") -> String? {
let openPanel=NSOpenPanel();
openPanel.title = title
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canChooseFiles = false
openPanel.canCreateDirectories = true
openPanel.directoryURL = URL(fileURLWithPath: directoryURL)
if(openPanel.runModal() == NSApplication.ModalResponse.OK) {
return directoryURL; // This won’t work
return openPanel.url!.path // This is OK
}
else {
return nil
}
}
In the above function I have prematurely returned with the original directory which is a string, so the whole process is ignored. If I comment out the first return statement, then it will return the selected directory, which is also a string.
Here is a SwiftUI button to test the function:
Button(action: {
let fileManager = FileManager.default
let sourceFolder = selectFolder(title: "test", directoryURL: "/path/to/folder")
if let enumerator = fileManager.enumerator(
at: URL(fileURLWithPath: sourceFolder!),
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles,.skipsPackageDescendants]
) {
for case let fileURL as URL in enumerator {
print("fileURL: \(fileURL)")
}
}
}) {
Text("Test")
}
The purpose is to iterate through the contents of the directory, including subdirectories.
If I return the hard coded string from the function, the for case let fileURL as URL in enumerator statement has nothing, and there are no results. There are no errors either.
If I return the openPanel.url!.path, the for case … statement prints the directory contents as expected.
I can’t see what the function returns which is different from the original string.
What can I do to get a hard coded string to work?

Finder algorithm for "sort by name" options

Im use NSFileManager class to create collection of URL
let resourceKeys = Set<URLResourceKey>([.nameKey, .isDirectoryKey, .typeIdentifierKey])
let directoryContents = try FileManager.default.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)
let fileURLs = directoryContents.filter { (url) -> Bool in
do {
let resourceValues = try url.resourceValues(forKeys: resourceKeys)
return !resourceValues.isDirectory! && resourceValues.typeIdentifier! == "public.jpeg"
} catch { return false }
}
the next step Im sorted fileURLs collection by file name
let sortedFileURLs = fileURLs.sorted(by: { (URL1: URL, URL2: URL) -> Bool in
return URL1.pathComponents.last! < URL2.pathComponents.last!
})
it works, but it's not the way as used Finder for "sort by name" options (another sorted result)
Please help! What algorithm used Finder for "sort by name"
The Finder-like sort order can be realized with localizedStandardCompare
The documentation says:
This method should be used whenever file names or other strings are presented in lists and tables where Finder-like sorting is appropriate.
let sortedFileURLs = fileURLs.sorted{ $0.lastPathComponent.localizedStandardCompare($1.lastPathComponent) == .orderedAscending }
Note: lastPathComponent is preferable over pathComponents.last!

FileHandle not accepting my URLs for write access

I'd like to open a uniquely named output file for writing either plist or data, but not having any luck in getting a handle using either URL routine of init(fileURLWithPath:) or init(string:)
func NewFileHandleForWritingFile(path: String, name: String, type: String, outFile: inout String?) -> FileHandle? {
let fm = FileManager.default
var file: String? = nil
var uniqueNum = 0
while true {
let tag = (uniqueNum > 0 ? String(format: "-%d", uniqueNum) : "")
let unique = String(format: "%#%#.%#", name, tag, type)
file = String(format: "%#/%#", path, unique)
if false == fm.fileExists(atPath: file!) { break }
// Try another tag.
uniqueNum += 1;
}
outFile = file!
do {
let fileURL = URL.init(fileURLWithPath: file!)
let fileHandle = try FileHandle.init(forWritingTo: fileURL)
print("\(file!) was opened for writing")
//set the file extension hidden attribute to YES
try fm.setAttributes([FileAttributeKey.extensionHidden: true], ofItemAtPath: file!)
return fileHandle
} catch let error {
NSApp.presentError(error)
return nil;
}
}
debugger shows
which for this URL init routine adds the scheme (file://) but otherwise the same as the other, and I'd like to prefer the newer methods which throw reutrning (-1) when just using paths. The error thrown (2) is an ENOENT (no such entity!?) as I need a handle to write to I'm confused how else to get one? The sample path is a new folder created at desktop to triage.
Unlike the previous answer, I recommend using Data's write(to:options:) API instead of FileManager's createFile(atPath:contents:attributes:), because it is a URL-based API, which is generally to be preferred over path-based ones. The Data method also throws an error instead of just returning false if it fails, so if something goes wrong, you can tell the user why.
try Data().write(to: fileURL, options: [])
I would also suggesting replacing the path-based FileManager.fileExists(atPath:) with the URL-based checkResourceIsReachable():
if false == ((try? fileURL.checkResourceIsReachable()) ?? false)
You can't create a file handle to a non-existent file. That is what is causing the ENOENT error.
Use FileManager createFile(atPath:contents:attributes:) to create the file just before creating the file handle.
do {
fm.createFile(atPath: file!, contents: nil, attributes: [FileAttributeKey.extensionHidden: true])
let fileURL = URL(fileURLWithPath: file!)
let fileHandle = try FileHandle(forWritingTo: fileURL)
print("\(file!) was opened for writing")
return fileHandle
} catch let error {
NSApp.presentError(error)
return nil;
}

Find whether a directory contains a URL, following symlinks

I have two directories as follows:
Directory A contains file X.
Directory B contains an alias to directory A named C.
So there are two possible absolute URLs for file X: /A/X and /B/C/X. (A and B can be anywhere in my filesystem.)
What I need to do is, given the file URL for directory B (file:///B/) and either file URL for file X, determine whether or not file X is within directory B.
Here's what I came up with:
extension URL {
func isIdenticalFile(to other: URL) -> Bool {
return resolvingSymlinksInPath() == other.resolvingSymlinksInPath()
}
func contains(_ other: URL) -> Bool {
guard isFileURL, other.isFileURL, let enumerator = FileManager.default.enumerator(atPath: path) else {
return false
}
for subURL in enumerator.map({ appendingPathComponent($0 as! String) }) {
if subURL.isIdenticalFile(to: other) || subURL.contains(other) {
return true
}
}
return false
}
}
let b = URL(string: "file:///B/")!
let ax = URL(string: "file:///A/X")!
let bcx = URL(string: "file:///B/C/X")!
// Both b.contains(ax) and b.contains(bcx) are true
Is there a simpler/more efficient way to do this?
A better method to determine if two URLs refer to the same
file is to compare their fileResourceIdentifier. From the documentation:
An identifier which can be used to compare two file system objects for equality using isEqual.
Two object identifiers are equal if they have the same file system path or if the paths are linked to same inode on the same file system. This identifier is not persistent across system restarts.
Determining the resource identifier should be faster than fully
resolving the file path. In addition this detects also hard links to
the same file.
More remarks:
The recursion in your code is not necessary because the enumerator
already does a "deep" enumeration.
With enumerator(at: self, ...) you get an enumerator for URLs
instead of paths, so that you don't have to build the subURL.
The code then could look like this:
extension URL {
// Helper property get the resource identifier:
private var identifier: NSObjectProtocol? {
return (try? resourceValues(forKeys: [.fileResourceIdentifierKey]))?.fileResourceIdentifier
}
func contains(_ other: URL) -> Bool {
guard isFileURL, other.isFileURL else {
return false
}
guard let otherId = other.identifier else {
return false
}
guard let enumerator = FileManager.default.enumerator(at: self, includingPropertiesForKeys: [.fileResourceIdentifierKey]) else {
return false
}
for case let subURL as URL in enumerator {
if let fileId = subURL.identifier, fileId.isEqual(otherId) {
return true
}
}
return false
}
}