How to handle symlinks when reading data from a file path in swift - swift

I have a file path as a string. I want to:
Test if there's a file there
Read the contents of the file as a string
the problem I'm having is that sometimes that file path involves a symbolic link (symlink). Maybe to the file itself. Maybe to one of the directories above the file.
[EDIT] closing this because the following code (that I started with), actually works just fine, there were just multiple levels of user error involved. Thanks for the input folks.
func getUserResource(relativeFilePath: String) -> String? {
let fileManager = NSFileManager.defaultManager()
let userFilePath = NSHomeDirectory() + relativeFilePath
if(fileManager.fileExistsAtPath(userFilePath))
{
do {
return try String(contentsOfFile: userFilePath, encoding: NSUTF8StringEncoding);
} catch {
return nil;
}
}
return nil;
}

If you're not sure if the symlink leads to a file or directory, you should be using fileExistsAtPath(path:, isDirectory:). fileExistsAtPath will always return true for a symlink, because technically there is a file at that path. By passing a boolean pointer to isDirectory, you can follow the symlink to a file or to a directory:
Assume symlinkToSomeFile is a symbolic link to a file and symlinkToSomeDir is a symbolic link to a directory.
let symlinkFilePath = NSHomeDirectory() + "/temp/symlinkToSomeFile"
let symlinkDirPath = NSHomeDirectory() + "/temp/symlinkToSomeDir"
var fileCheck: ObjCBool = false
var dirCheck: ObjCBool = false
print(fileManager.fileExistsAtPath(symlinkFilePath, isDirectory: &fileCheck)) // true
print(fileCheck) // false
print(fileManager.fileExistsAtPath(symlinkDirPath, isDirectory: &dirCheck)) // true
print(dirCheck) // true

Related

How to check if multiple files exist in documents directory? (Swift)

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)

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;
}

FileManager.contentsEqual returns false when comparing copied files

I need to preload SQLite files from my bundle's resources into the application support directory. I want to make sure the correct files are there vs. the empty files that Core Data puts there by default. To do this, I'm using FileManager.default.contentsEqual; however, this always returns false.
I tried testing with a playground, but the copy there is creating alias files, still resulting in a false comparison.
In the app, the files do copy over with the same name and size. The dates are different: the copies have the current date/time rather than the original's timestamps. Using contentsEqual, though, I wouldn't think that matters.
Update: diff at the command line shows the files are the same...
What am I missing?
Here's the code from the playground, which is virtually the same as my app code:
// get the URL for the application support directory
let appSupportDir: URL = try!
FileManager.default.url(for: FileManager.SearchPathDirectory.applicationSupportDirectory,
in: FileManager.SearchPathDomainMask.userDomainMask,
appropriateFor: nil, create: true)
// get the source URLs for the preload files
let sqliteFileBundleURL: URL = Bundle.main.url(forResource: "My_DB", withExtension: "sqlite")!
let sqliteShmFileBundleURL: URL = Bundle.main.url(forResource: "My_DB", withExtension: "sqlite-shm")!
let sqliteWalFileBundleURL: URL = Bundle.main.url(forResource: "My_DB", withExtension: "sqlite-wal")!
// create target URLs for copy to application support directory
let sqliteFileAppSptURL: URL = appSupportDir.appendingPathComponent("My_DB.sqlite")
let sqliteShmFileAppSptURL: URL = appSupportDir.appendingPathComponent("My_DB.sqlite-shm")
let sqliteWalFileAppSptURL: URL = appSupportDir.appendingPathComponent("My_DB.sqlite-wal")
// remove the files if they already exist at the target (for test - app doesn't do this)
do {
let filesFound: [URL] = try FileManager.default.contentsOfDirectory(at: appSupportDir,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
if !filesFound.isEmpty {
for fileURL in filesFound {
try FileManager.default.removeItem(at: fileURL)
}
print("Removed \(filesFound.count) files without error.")
}
}
catch {
print("Error:\n\(error)")
}
// copy the files to the application support directory
do {
try FileManager.default.copyItem(at: sqliteFileBundleURL, to: sqliteFileAppSptURL)
try FileManager.default.copyItem(at: sqliteShmFileBundleURL, to: sqliteShmFileAppSptURL)
try FileManager.default.copyItem(at: sqliteWalFileBundleURL, to: sqliteWalFileAppSptURL)
}
catch {
print("Error: \(error)")
}
// compare the copied target files to their source using contentsEqual
let sqliteFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteFileBundleURL.absoluteString, andPath: sqliteFileAppSptURL.absoluteString)
let sqliteShmFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteShmFileBundleURL.absoluteString, andPath: sqliteShmFileAppSptURL.absoluteString)
let sqliteWalFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteWalFileBundleURL.absoluteString, andPath: sqliteWalFileAppSptURL.absoluteString)
Aha! When using FileManager, one should be using path rather than absoluteString to convert a URL to a String:
// compare the copied target files to their source using contentsEqual
let sqliteFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteFileBundleURL.path, andPath: sqliteFileAppSptURL.path)
let sqliteShmFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteShmFileBundleURL.path, andPath: sqliteShmFileAppSptURL.path)
let sqliteWalFileCopied: Bool =
FileManager.default.contentsEqual(atPath: sqliteWalFileBundleURL.path, andPath: sqliteWalFileAppSptURL.path)
The difference between the two is that path generates a file system-type path:
/var/folders/kb/y2d_vrl133d1b04_5kc3kw880000gn/T/com.apple.dt.Xcode.pg/resources/238FF955-236A-42FC-B6EA-9A74FC52F235/My_DB.sqlite
whereas absoluteString generates a browser-friendly path:
file:///var/folders/kb/y2d_vrl133d1b04_5kc3kw880000gn/T/com.apple.dt.Xcode.pg/resources/238FF955-236A-42FC-B6EA-9A74FC52F235/My_DB.sqlite
Note: path also works in the playground with the alias files.

fileExistsAtPath check for filename?

How to check whether there is a file in a directory with only the name without extension? Now the files are written in my directory, their name will be generated from the id file. Accordingly, when I'm looking for a file, let file = "\ (fileId) .pdf", in the directory it is, but if no extension, it will not be found. Either return as easier extension from the server?
public var isDownloaded: Bool {
let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
let filePath = url.URLByAppendingPathComponent("\(fileMessageModel.attachment.id)")!.path!
let fileManager = NSFileManager.defaultManager()
return fileManager.fileExistsAtPath(filePath)
}
enumeratorAtPath creates a deep enumerator -- i.e. it will scan contents of subfolders and their subfolders too. For a shallow search, user contentOfDirectortAtPath:
func file(fileName: String, existsAt path: String) -> Bool {
var isFound = false
if let pathContents = try? NSFileManager.defaultManager().contentsOfDirectoryAtPath(path) {
pathContents.forEach { file in
if (file as NSString).stringByDeletingPathExtension == fileName {
isFound = true
return
}
}
}
return isFound
}
if let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first {
if file("something", existsAt: path) {
// The file exists, do something about it
}
}
What about iterating over the files in the directory and testing the name with extension excluded?
let filemanager:FileManager = FileManager()
let files = filemanager.enumeratorAtPath(/* your directory path */)
while let file = files?.nextObject() {
// Remove file name extension
// Do file name comparison here
}
In terms of time complexity is will be O(n), however, as long as there are not too many files, you are good to go. On the other hand, if there are many files, you will need to consider a more efficient way to traverse, may be a trie data structure consisted of all file names in that directory.

Substitute user path with tilde in URL

Assume a file URL containing /Users/me/a/b. Is there a better method than a naive string replace using NSHomeDirectory() to get the short form ~/a/b?
This is what I'm currently using
.replacingOccurrences(of: NSHomeDirectory(), with: "~", options: .anchored, range: nil)
PS: No NSString casting!
If you are in Sandboxed app then usage of getpwuid function is needed.
extension FileManager {
/// Returns path to real home directory in Sandboxed application
public var realHomeDirectory: String? {
if let home = getpwuid(getuid()).pointee.pw_dir {
return string(withFileSystemRepresentation: home, length: Int(strlen(home)))
}
return nil
}
}
Example usage:
func configure(url: URL) {
...
var directory = url.path.deletingLastPathComponent
toolTip = url.path
if let homeDir = FileManager.default.realHomeDirectory {
// FYI: `url.path.abbreviatingWithTildeInPath` not working for sandboxed apps.
let abbreviatedPath = url.path.replacingFirstOccurrence(of: homeDir, with: "~")
directory = abbreviatedPath.deletingLastPathComponent
toolTip = abbreviatedPath
}
directoryLabel.text = directory
}
More about getpwuid function: How do I get the users home directory in a sandboxed app?
Result of usage in NSView:
How about converting to NSString and then abbreviating it with:
var path = url.path as NSString!
var abbrevPath=path?.abbreviatingWithTildeInPath
NSPathUtilities
- (NSString *)stringByAbbreviatingWithTildeInPath;
- (NSString *)stringByExpandingTildeInPath;
[blueprintsDict writeToFile: [[NSString stringWithFormat:#"~/MbPatchBlueprints.plist"] stringByExpandingTildeInPath] atomically:YES];
I noticed this question doesn't have an accepted answer yet, and I ran into the same issue. I tried the NSString methods, but those didn't work in a sandboxed context anyway, and the getpwuid method felt… wrong. So here's a pure Swift solution that's only really hardcoded insofar as it assumes the sandbox root to share the first 3 path components (/, Users, your username) with your actual home directory:
extension URL {
var pathAbbreviatingWithTilde: String {
// find home directory path (more difficulty because we're sandboxed, so it's somewhere deep in our actual home dir)
let sandboxedHomeDir = FileManager.default.homeDirectoryForCurrentUser
let components = sandboxedHomeDir.pathComponents
guard components.first == "/" else { return path }
let homeDir = "/" + components.dropFirst().prefix(2).joined(separator: "/")
// replace home dir in path with tilde for brevity and aesthetics
guard path.hasPrefix(homeDir) else { return path }
return "~" + path.dropFirst(homeDir.count)
}
}