Writing to log file seems to overwrite previous save - swift

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().

Related

Swift 5: How to save results of NSFetchRequest to File

I am new to programming in general and have started with Swift. I have a feeling what I'm attempting to do is a bit outside of my scope, but I've come so far so here's the ask:
I am adding a tracker to a program for macOS X I've already created. The end user inputs a number and hits "Add to tracker" which then takes that number, the timestamp from the button click and writes that to the appropriate entity in Core Data. Everything works perfectly, my NSTable displays the data and I my batch delete works, but I cannot for the life of me work out the best way to take the results from the NSFetchRequest and print them to a text file.
Here is the code for my fetch request that occurs when the "print" button is hit:
#IBAction func printTracker(_ sender: Any) {
fetchRequest.propertiesToFetch = ["caseDate","caseNumber"]
fetchRequest.returnsDistinctResults = true
fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType
do {
let results = try context.fetch(fetchRequest)
let resultsDict = results as! [[String:String]]
} catch let err as NSError {
print(err.debugDescription)
}
}
After the "resultsDict" declaration is where I just can't seem to come to a workable solution for getting it to string, then to txt file.
If I add a print command to the console as is, I can see that resultsDict pulls correctly with the following format:
[["caseNumber": "12345", "caseDate": "3/22/21, 5:48:18 PM"]]
Ideally I need it in plaintext more like
"3/22/21, 5:48:18 PM : 12345"
Any advice or help on the conversion would be greatly appreciated.
A simple way if there is not a huge amount of data returned is to create a string from the fetched data and then write that string to disk
First create the string by getting the values from the dictionary and adding them in the right order into a string and joining the strings with a new line character
let output = results.reduce(into: []) { $0.append("\($1["caseDate", default: ""]) : \($1["caseNumber", default: ""])") }
.joined(separator: "\n")
Then we can write them to file, here I use the Document directory as the folder to save the file in
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let path = paths[0].appendingPathComponent("results.txt")
do {
try String(output).write(to: path, atomically: true, encoding: .utf8)
} catch {
print("Failed to write to file, error: \(error)")
}

How can I write to a file, line by line in Swift 4

I need help figuring out how to write repeatedly to the same, open, output file.
I am using Swift 4.2. My searches and tests have turned up only code that writes a single text string to a file and then closes the file. The next opening overwrites the last one. An example is shown below.
The problem is that I need to be able to write large numbers of records (say, 1.5 million) and perform calculations on each record just before it is written to a file. That’s not feasible when the code will only write once before closing. I'm calling this "writing line by line", much like the opposite, to "read line by line."
I tried to find an option in various Swift write statements and SO posts, but everything seems to be geared toward writing once then closing the file. I tried an open for append, but that did not work and anyway it seems inefficient to open, close, reopen-append each time I want to write to a file. I tried some C code in Swift, using open(… and freopen(… but could not get something that the compiler wouldn't complain about. Hopefully, there is a way to do this all in Swift. The following code works nicely for one write.
let file0 = “test_file.txt”
let s0 = ("This is a test line of text")
do {
try s0.write(to: NSURL(fileURLWithPath: file0) as URL, atomically: false, encoding: String.Encoding.utf8)
} catch {
print("Problem writing to file0")
}
How can I adapt this code snippet to write a string, and then another and another etc, and before closing the file when it’s all done? If not with this, is there Swift code that will do the job?
Following are the essential code components needed to write to a file, line-by-line in Swift. First is some file management code to create a file if it does not exist, then there is code to print a series of example statements, followed by code to print to the file in a loop, and finally close the file. This code worked correctly in Swift 4.2. The difference between this and the method in the question is that the write statements in this code use a method of fileHandle! and the question shows a method of a Swift string.
print("Swift_Write_to_File_Test_1")
var outFilename: NSString = "test_file.txt"
// Begin file manager segment
// Check for file presence and create it if it does not exist
let filemgr = FileManager.default
let path = filemgr.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).last?.appendingPathComponent(outFilename as String)
if !filemgr.fileExists(atPath: (path?.absoluteString)!) {
filemgr.createFile(atPath: String(outFilename), contents:Data(" ".utf8), attributes: nil)
}
// End file manager Segment
// Open outFilename for writing – this does not create a file
let fileHandle = FileHandle(forWritingAtPath: outFilename as String)
if(fileHandle == nil)
{
print("Open of outFilename forWritingAtPath: failed. \nCheck whether the file already exists. \nIt should already exist.\n");
exit(0)
}
var str: NSString = "2. Test string from NSString.\n";
var str0: String = "3. Test string from a Swift String.\n"
var str1: NSString = "4. Test string from NSString.\n";
fileHandle!.write("1. Text String in-line with code statement.\n".data(using: .utf8)!)
fileHandle!.write(String(str).data(using: .utf8)!)
fileHandle!.write(str0.data(using: .utf8)!)
fileHandle!.write(String(str1).data(using: .utf8)!)
fileHandle!.write("5. Text String in-line with code statement.\n".data(using: .utf8)!)
fileHandle!.write("6. Text in a loop follows: \n".data(using: .utf8)!)
for i in 0...5
{
//Assemble a string then write it to the file.
var s0: String = ""
s0 = String(i)
//s0.append(" ... some text here.\n") // See improvement below
s0 += " ... some text here.\n" // This is a better than .append
fileHandle!.write(s0.data(using: .utf8)!)
}
// End of file-writing segment
fileHandle!.closeFile()
This worked for me in Swift 5:
func writeFile() -> Bool
{
let outFilename: String = "test_file.txt"
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let outFilePath = documentsURL!.appendingPathComponent(outFilename).path
let fileManager = FileManager.default
// If file exists, remove it
if fileManager.fileExists(atPath: outFilePath)
{
do { try fileManager.removeItem(atPath: outFilePath) }
catch { return false }
}
// Create file and open it for writing
fileManager.createFile(atPath: outFilePath, contents:Data(" ".utf8), attributes: nil)
let fileHandle = FileHandle(forWritingAtPath: outFilePath)
if fileHandle == nil
{
return false
}
else
{
// Write data
fileHandle!.write("Test line 1\n".data(using: .utf8)!)
fileHandle!.write("Test line 2\n".data(using: .utf8)!)
fileHandle!.write("Test line 3\n".data(using: .utf8)!)
// Close file
fileHandle!.closeFile()
return true
}
}

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

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

Alamofire not saving file to disk

I have a video file on S3 that I am trying to save to disk. However, if the file already exists on disk, I want to overwrite it. I wrote this function to download the file but it never saves the file. I can see the progress % increasing. But, how do I access the resulting file and save it to disk?
var finalPath: NSURL?
Alamofire.download(.GET, s3Url) { temporaryURL, response in
let fileManager = NSFileManager.defaultManager()
if let directoryURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0] as? NSURL {
let pathComponent = response.suggestedFilename
finalPath = directoryURL.URLByAppendingPathComponent(pathComponent!)
println(finalPath)
//remove the file if it exists
if fileManager.fileExistsAtPath(finalPath!.absoluteString!) {
println("file exists on disk, removing..")
fileManager.removeItemAtPath(finalPath!.absoluteString!, error: nil)
}
return finalPath!
}
return temporaryURL
}
.validate()
.progress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
let progress = (Double(totalBytesRead) / Double(totalBytesExpectedToRead)) * 100
println(String(format: "%.2f", progress))
}
.response { request, response, data, error in
println(request)
println(response)
if let mediaData = data {
println("saving file to disk")
mediaData.writeToURL(finalPath!, atomically: true)
}
}
Normally I would use the example provided in the docs, but It fails if the file already exists. ie:
let destination = Alamofire.Request.suggestedDownloadDestination(directory: .DocumentDirectory, domain: .UserDomainMask)
So, how can I download the file, overwrite it if it exists and record the path that the file is written to to my coreData database?
You need to delete the file first. Alamofire only tries to move the file from the temp location to the final location that you provide in the destination closure.
You can create an extension on Alamofire.DownloadRequest to provide options for how to download the file where you can use the option DownloadRequest.DownloadOptions.removePreviousFile.
for details on how to do that see my answer to this question.