FileHandle not accepting my URLs for write access - swift

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

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?

Issues updating/appending a string to the end of a file in Swift

I am working on an app where I am trying to write data to a file. Basically the user can set the save directory and then I want to be able to check to see if the file exists. If it doesn't then create it and write the data. If it does exist I want to update/append a string to the end of the file. I have tried following many examples and guides online and it seems everyone is doing this differently. I apologize ahead of time if I am missing something super simple or if this has been asked a hundred times.
I have mostly been working off of this example with no success
Append text or data to text file in Swift
#IBAction func addVariance(_ sender: Any) {
let csvString = "08-06-2019,10:00 AM,10:23 AM,23,Offline,Test"
let directoryURL = URL(string: "/Users/username/Desktop/CSVtest")
let fileURL = directoryURL!.appendingPathComponent("/variances.csv")
let data = NSData(data: csvString.data(using: String.Encoding.utf8, allowLossyConversion: false)!)
if FileManager.default.fileExists(atPath: fileURL.path) {
var err:NSError?
if let fileHandle = try? FileHandle(forUpdating: fileURL) {
fileHandle.seekToEndOfFile()
fileHandle.write(data as Data)
fileHandle.closeFile()
}
else {
print("Can't open fileHandle \(String(describing: err))")
}
}
else {
var err:NSError?
do {
try data.write(to: URL(fileURLWithPath: fileURL.path), options: .atomic)
} catch {
print(error)
}
}
}
When trying to run this function when there is a file in the folder named "variances.csv" I get "Can't open fileHandle nil".
I have tried break points and print() lines. It doesn't seem to be getting past "if let fileHandle = try? FileHandle(forUpdating: fileURL)" and I can't figure out why. fileURL is not nil.
When I try running this function outputting to an empty directory I get.
"Error Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “variances.csv” in the folder “CSVtest”." UserInfo={NSFilePath=/Users/username/Desktop/CSVtest/variances.csv, NSUnderlyingError=0x600000c9eaf0 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}"

Sandbox and Finder alias

I'm trying to create a security scoped URL for a user provide file which happens to be an alias using this methods which uses a resolvedFinderAlias() method:
func storeBookmark(url: URL) -> Bool
{
// Resolve alias before storing bookmark
let origURL = (url as NSURL).resolvedFinderAlias()
// Peek to see if we've seen this key before
if let data = bookmarks[url] {
if self.fetchBookmark(key: url, value: data) {
Swift.print ("= \(url.absoluteString)")
return true
}
}
do
{
let options:URL.BookmarkCreationOptions = [.withSecurityScope,.securityScopeAllowOnlyReadAccess]
let data = try url.bookmarkData(options: options, includingResourceValuesForKeys: nil, relativeTo: origURL)
bookmarks[url] = data
return self.fetchBookmark(key: url, value: data)
}
catch let error
{
NSApp.presentError(error)
Swift.print ("Error storing bookmark: \(url)")
return false
}
}
which throws an error in attempt to use the resolved URL to as the relative URL; I had originally just swapped the passed in URL to the origURL which ddin't work either.
The only solution is to not do this, or to previously be passed in the original URL. It's almost as if you cannot swap URLs you must be supplied that from either an open dialog or a pasteboard drop.
Are URLs which are aliases not suitable for sandbox work ?

Writing to log file seems to overwrite previous save

As an exercise, I'm trying to write a logging class that logs strings to a text file. I've got my application to write and read from the file. However, if I try to log multiple times it seems to only pick up the most recent log.
Attempt
writing
private let file = "logfile.txt"
func write(text: String) {
let path = getDocumentsDirectory()
do {
try text.writeToFile(path, atomically: false, encoding: NSUTF8StringEncoding)
}
catch let error {
print("error: \n \(error)")
}
}
reading
func read() {
let path = getDocumentsDirectory()
do {
let text2 = try String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
print("From Log: \n \(text2)")
}
catch let error {
print("error: \n \(error)")
}
}
func getDocumentsDirectory() -> String {
guard let dir : NSString = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first else {
return ""
}
let documentsDirectory : String = dir.stringByAppendingPathComponent(file)
return documentsDirectory
}
Result
When I try to read my file I only get the last line saved.
Question
If my goal is to endlessly append new logs to a file, and to read the log file in bulk. What changes to my code do I need to make?
more details:
I'm writing to the file on application load:
Logger.shared.write("instance one writes")
Logger.shared.write("instance one writes again")
Logger.shared.write("instance one writes yet again")
and then attempting to read:
Logger.shared.read()
output:
From Log:
instance one writes yet again
The writeToFile(_:atomically:encoding:) method provided by Foundation replaces the contents of the given file. There are several ways of appending to files:
Plain ol’ fopen (with mode "a") and fwrite.
NSOutputStream, such as NSOutputStream(toFileAtPath: mypath, append: true), using stream.write(bytes, len) to write data.
Perhaps the easiest, NSFileHandle, such as NSFileHandle(forWritingAtPath: mypath), using seekToEndOfFile() and writeData().