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
}
}
Related
)
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)")
}
}
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?
I can check if one file exists with this method:
let fileNameOne = "savedpicture1"
let fileURLOne = documentsDirectoryURL.appendingPathComponent(fileNameOne)
if !FileManager.default.fileExists(atPath: fileURLOne.path) {
removeImage(itemName: "savedpicture1", fileExtension: "jpg")
} else {
print("There was no image to remove")
}
My problem is having to repeat the same lines of code for multiple files. For instance, I would like to check if the files exist in an array of paths, but I would have to repeat the code from above for each file, and it seems too redundant. I'm wondering if there's a way to check multiple files instead of repeating the code for each single path. ".fileExists" only enables me to check one path:
let filePaths = [fileURLOne.path, fileURLTwo.path, fileURLThree.path,
fileURLFour.path]
Write a method for example
func checkFiles(with fileNames: [String] {
for fileName in fileNames {
let fileURL = documentsDirectoryURL.appendingPathComponent(fileName)
if !FileManager.default.fileExists(atPath: fileURL.path) {
removeImage(itemName: fileName, fileExtension: "jpg")
} else {
print("There was no image to remove at", fileURL)
}
}
}
and call it
let fileNames = ["savedpicture1", "savedpicture2", "savedpicture3", "savedpicture4"]
checkFiles(with: fileNames)
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;
}
I used to read the text files from my application's bundle by using the following code. However, no matter what my application can't find them anymore. I am 100% sure that all my files are in the Assets.xcassets, I can see them, edit them, transform them from a directory to another. But my application doesn't want to read them, please tell me what I missed!!
this is the procedure I am using...
func readBundle(file:String) -> String
{
var res: String = ""
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
{
let fm = NSFileManager()
let exists = fm.fileExistsAtPath(path)
if(exists)
{
let c = fm.contentsAtPath(path)
res = NSString(data: c!, encoding: NSUTF8StringEncoding) as! String
}
}
return res
}
I am using it like this:
let res = readBundle("test")
print(res)
when storing non image files in XCAssets, you should use NSDataAsset to acccess their content
https://developer.apple.com/library/ios/documentation/UIKit/Reference/NSDataAsset_Class/
func readBundle(file:String) -> String
{
var res = ""
if let asset = NSDataAsset(name: file) ,
string = String(data:asset.data, encoding: NSUTF8StringEncoding){
res = string
}
return res
}
In the another option then 'XCAssets' you can create a separate folder/group of your resources other than images in the project structure, check if they exist in the Copy Bundle Resource in the Build phases section of your project's main target
If you add resource like this your current code should work as it is
func readBundle(file:String) -> String
{
var res: String = ""
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
{
//you should be able to get the path
//other code as you has written in the question
}
return res
}