I want to download file from azure with REST API and I written below code for iOS Swift >3, but when I run the download task, get this error:
InvalidHeaderValueThe value for one of the HTTP headers is not in the correct format.
RequestId:10d9c8f8-001a-00db-283c-1ab1d1000000
Time:2017-08-21T05:14:18.2768791Zx-ms-version
private let account = "myAccount"
private let key = "My key is encrypted as base64"
private let fileName = "My file name which have to download"
private let SHARE_NAME = "My share name"
let date = Date().currentTimeZoneDate() + " GMT"
func downloadFileFromAzure(fileName:String)
{
// Create destination URL
let documentsUrl:URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL!
let destinationFileUrl = documentsUrl.appendingPathComponent(fileName)
//Create URL to the source file you want to download
let fileURL = URL(string: "https://\(account).file.core.windows.net/\(SHARE_NAME)/\(fileName)")!
//create session
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
//create headers field
var request = URLRequest(url:fileURL)
request.setValue(date,forHTTPHeaderField: "x-ms-date")
request.setValue("2014-02-14", forHTTPHeaderField: "x-ms-version")
request.setValue("\(getFileRequest(account:account, fileName: fileName))", forHTTPHeaderField: "Authorization")
//download files
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
// Success
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("Successfully downloaded. Status code: \(statusCode)")
}
do {
try FileManager.default.copyItem(at: tempLocalUrl, to: destinationFileUrl)
print("tempLocalUrl: \(tempLocalUrl)")
print("destinationFileUrl: \(destinationFileUrl)")
//reading
do {
let text = try String(contentsOf: destinationFileUrl, encoding: String.Encoding.utf8)
print("reading files data : \(text)")
}
catch (let writeError){
print("Error reading a file \(writeError)")
}
} catch (let writeError) {
print("Error creating a file \(destinationFileUrl) : \(writeError)")
}
} else {
print("Error took place while downloading a file. Error description: %#", error?.localizedDescription);
}
}
task.resume()
}
public func getFileRequest(account:String,fileName:String)->String
{
let canonicalizedResources = "/\(account)/\(SHARE_NAME)/\(fileName)"
let canonicalizedHeaders = "x-ms-date:\(date)\nx-ms-version:2014-02-14"
let stringToSign = "GET\n\n\n\n\n\n\n\n\n\n\n\n\(canonicalizedHeaders)\n\(canonicalizedResources)"
let auth = getAuthenticationString(stringToSign:stringToSign);
return auth
}
///getAuthenticationString
public func getAuthenticationString(stringToSign:String)->String
{
let authKey: String = stringToSign.hmac(algorithm: HMACAlgorithm.SHA256, key: key)
let auth = "SharedKey " + account + ":" + authKey;
return auth;
}
enum HMACAlgorithm
{
case MD5, SHA1, SHA224, SHA256, SHA384, SHA512;
func toCCHmacAlgorithm() -> CCHmacAlgorithm {
var result: Int = 0
switch self {
case .MD5:
result = kCCHmacAlgMD5
case .SHA1:
result = kCCHmacAlgSHA1
case .SHA224:
result = kCCHmacAlgSHA224
case .SHA256:
result = kCCHmacAlgSHA256
case .SHA384:
result = kCCHmacAlgSHA384
case .SHA512:
result = kCCHmacAlgSHA512
}
return CCHmacAlgorithm(result)
}
func digestLength() -> Int {
var result: CInt = 0
switch self {
case .MD5:
result = CC_MD5_DIGEST_LENGTH
case .SHA1:
result = CC_SHA1_DIGEST_LENGTH
case .SHA224:
result = CC_SHA224_DIGEST_LENGTH
case .SHA256:
result = CC_SHA256_DIGEST_LENGTH
case .SHA384:
result = CC_SHA384_DIGEST_LENGTH
case .SHA512:
result = CC_SHA512_DIGEST_LENGTH
}
return Int(result)
}
}
extension String {
func hmac(algorithm: HMACAlgorithm, key: String) -> String {
let keyBytes = key.base64DecodeAsData()
let dataBytes = self.cString(using: String.Encoding.utf8)
var result = [CUnsignedChar](repeating: 0, count: Int(algorithm.digestLength()))
CCHmac(algorithm.toCCHmacAlgorithm(), keyBytes.bytes, keyBytes.length, dataBytes!, Int(strlen(dataBytes!)), &result)
let hmacData:NSData = NSData(bytes: result, length: (Int(algorithm.digestLength())))
let hmacBase64 = hmacData.base64EncodedString(options: NSData.Base64EncodingOptions.lineLength76Characters)
return String(hmacBase64)
}
func base64DecodeAsData() -> NSData {
let decodedData = NSData(base64Encoded: self, options: NSData.Base64DecodingOptions(rawValue: 0))
return decodedData!
}
Edit:
I only get this error in the Xcode.
InvalidHeaderValueThe value for one of the HTTP headers is not in the correct format.
RequestId:10d9c8f8-001a-00db-283c-1ab1d1000000
Time:2017-08-21T05:14:18.2768791Zx-ms-version
But when I tried sharedKey(which is generated in ios) in the android source get this error:
AuthenticationFailedServer failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature.equestId:379c5c1b-001a-0017-1a19-1bd564000000ime:2017-08-22T07:38:04.8712051ZThe MAC signature found in the HTTP request 'tJcl9LyzF2BzlZMdW9ULtMojDamn9HnEY9LulpDOsYg=' is not the same as any computed signature. Server used following string to sign: 'GETx-ms-date:Tue, 22 Aug 2017 07:32:52 GMT-ms-version:2014-02-14account/SHARE_NAME/fileName'.
SOLVED
Change this line
let hmacBase64 = hmacData.base64EncodedString(options: NSData.Base64EncodingOptions.lineLength76Characters)
To:
let hmacBase64 = hmacData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
Also change this line
let stringToSign = "GET\n\n\n\n\n\n\n\n\n\n\n\n(canonicalizedHeaders)\n(canonicalizedResources)"
To:
let stringToSign = "GET\n"
+ "\n" // content encoding
+ "\n" // content language
+ "\n" // content length
+ "\n" // content md5
+ "\n" // content type
+ "\n" // date
+ "\n" // if modified since
+ "\n" // if match
+ "\n" // if none match
+ "\n" // if unmodified since
+ "\n" // range
+ "(canonicalizedHeaders)\n" //headers
+ "(canonicalizedResources)"// resources
Please delete your stored file before create new one by this func:
func deleteFileFromDirectory(fileNameToDelete:String)
{
var filePath = ""
// Fine documents directory on device
let dirs : [String] = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.allDomainsMask, true)
if dirs.count > 0 {
let dir = dirs[0] //documents directory
filePath = dir.appendingFormat("/" + fileNameToDelete)
print("Local path = \(filePath)")
} else {
print("Could not find local directory to store file")
return
}
do {
let fileManager = FileManager.default
// Check if file exists
if fileManager.fileExists(atPath: filePath) {
// Delete file
try fileManager.removeItem(atPath: filePath)
} else {
print("File does not exist")
}
}
catch let error as NSError {
print("An error took place: \(error)")
}
}
I believe the problem is in these 2 lines of code:
let canonicalizedHeaders = "x-ms-date:\(date)\nx-ms-version:2016-05-31\n"
let stringToSign = "GET\n\n\n\n\n\n\n\n\n\n\n\n\(canonicalizedHeaders)\n\(canonicalizedResources)"
I noticed there's an extra new line character (\n). Can you either remove the \n character at the end of canonicalizedHeaders or between (canonicalizedHeaders) and (canonicalizedResources) in stringToSign?
Related
I have more than 500 image links I want to download those images and store locally in app document directory when app starts. I am using Almofire for download but I am getting error like
"URLSession task was cancelled" and Request timeOut
func downloadAllImages(images:[String: String], retryCount: Int = 0,completion: #escaping((Bool)->Void)){
var success: Bool = true
var failedImages = [String: String]()
for (localName, severPath) in images {
self.dispatchGroup.enter()
let destination: DownloadRequest.Destination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent(localName)
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
let path = severPath.replacingOccurrences(of: "\\", with: "/").addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
//AF.sessionConfiguration.httpShouldSetCookies = false
AF.download(path, to: destination).response { response in
switch response.result {
case .success(_):
break
case .failure(let error):
if response.response?.statusCode != 404 {
success = false
failedImages[localName] = path
print("Image Download Error = \(error.localizedDescription)")
}
break
}
debugPrint(response)
self.dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
//retry If some Images failed to download
if failedImages.isEmpty || retryCount >= self.maximumRetryCount {
completion(success)
}else {
self.downloadAllImages(images: failedImages, retryCount: retryCount + 1) { (success) in
completion(success)
}
}
}
}
images dictionary contains
localName as key
serverPath as value
AFImageDownloaders have limit of active downloads, and I believe changing maximumActiveDownloads or something similar in your API will fix that. The new downloads just cancel the previous ones. But it's better to download them in chunks.
For example this one is for ImageDownloader
public init(session: Session,
downloadPrioritization: DownloadPrioritization = .fifo,
maximumActiveDownloads: Int = 4,
imageCache: ImageRequestCache? = AutoPurgingImageCache()) {
precondition(!session.startRequestsImmediately, "Session must set `startRequestsImmediately` to `false`.")
self.session = session
self.downloadPrioritization = downloadPrioritization
self.maximumActiveDownloads = maximumActiveDownloads
self.imageCache = imageCache
}
UPD:
The limit is not on AF, but URLSession's. And one AF downloader uses one URLSession. You have to pass custom URLSessionConfigurations to handle more active downloads HTTPMaximumConnectionsPerHost. And pass it AF Session class
I am trying to append a string into text file. I am using the following code.
let dirs : [String]? = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true) as? [String]
if (dirs) != nil {
let dir = dirs![0] //documents directory
let path = dir.stringByAppendingPathComponent("votes")
let text = "some text"
//writing
text.writeToFile(path, atomically: true, encoding: NSUTF8StringEncoding, error: nil)
//reading
let text2 = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)
println(text2) //prints some text
}
this does not append the string to file. Even if I call this function repeatedly.
If you want to be able to control whether to append or not, consider using OutputStream. For example:
do {
let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("votes.txt")
guard let outputStream = OutputStream(url: fileURL, append: true) else {
print("Unable to open file")
return
}
outputStream.open()
let text = "some text\n"
try outputStream.write(text)
outputStream.close()
} catch {
print(error)
}
By the way, this is an extension that lets you easily write a String (or Data) to an OutputStream:
extension OutputStream {
enum OutputStreamError: Error {
case stringConversionFailure
case bufferFailure
case writeFailure
}
/// Write `String` to `OutputStream`
///
/// - parameter string: The `String` to write.
/// - parameter encoding: The `String.Encoding` to use when writing the string. This will default to `.utf8`.
/// - parameter allowLossyConversion: Whether to permit lossy conversion when writing the string. Defaults to `false`.
func write(_ string: String, encoding: String.Encoding = .utf8, allowLossyConversion: Bool = false) throws {
guard let data = string.data(using: encoding, allowLossyConversion: allowLossyConversion) else {
throw OutputStreamError.stringConversionFailure
}
try write(data)
}
/// Write `Data` to `OutputStream`
///
/// - parameter data: The `Data` to write.
func write(_ data: Data) throws {
try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws in
guard var pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
throw OutputStreamError.bufferFailure
}
var bytesRemaining = buffer.count
while bytesRemaining > 0 {
let bytesWritten = write(pointer, maxLength: bytesRemaining)
if bytesWritten < 0 {
throw OutputStreamError.writeFailure
}
bytesRemaining -= bytesWritten
pointer += bytesWritten
}
}
}
}
For Swift 2 rendition, see previous revision of this answer.
You can also use FileHandle to append String to your text file. If you just want to append your string the end of your text file just call seekToEndOfFile method, write your string data and just close it when you are done:
FileHandle usage Swift 3 or Later
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// create a new text file at your documents directory or use an existing text file resource url
let fileURL = documentsDirectory.appendingPathComponent("simpleText.txt")
do {
try Data("Hello World\n".utf8).write(to: fileURL)
} catch {
print(error)
}
// open your text file and set the file pointer at the end of it
do {
let fileHandle = try FileHandle(forWritingTo: fileURL)
fileHandle.seekToEndOfFile()
// convert your string to data or load it from another resource
let str = "Line 1\nLine 2\n"
let textData = Data(str.utf8)
// append your text to your text file
fileHandle.write(textData)
// close it when done
fileHandle.closeFile()
// testing/reading the file edited
if let text = try? String(contentsOf: fileURL, encoding: .utf8) {
print(text) // "Hello World\nLine 1\nLine 2\n\n"
}
} catch {
print(error)
}
Please check the below code as its working for me. Just Add the code as it is:
let theDocumetFolderSavingFiles = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let filePath = "/theUserData.txt"
let thePathToFile = theDocumetFolderSavingFiles.stringByAppendingString(filePath)
let theFileManager = NSFileManager.defaultManager()
if(theFileManager.fileExistsAtPath(thePathToFile)){
do {
let stringToStore = "Hello working fine"
try stringToStore.writeToFile(thePathToFile, atomically: true, encoding: NSUTF8StringEncoding)
}catch let error as NSError {
print("we are geting exception\(error.domain)")
}
do{
let fetchResult = try NSString(contentsOfFile: thePathToFile, encoding: NSUTF8StringEncoding)
print("The Result is:-- \(fetchResult)")
}catch let errorFound as NSError{
print("\(errorFound)")
}
}else
{
// Code to Delete file if existing
do{
try theFileManager.removeItemAtPath(thePathToFile)
}catch let erorFound as NSError{
print(erorFound)
}
}
A simple solution that works for me. UPDATE, it looks like I must have gotten this from here, so credit where credit is due:
Append text or data to text file in Swift
Usage:
"Hello, world".appendToURL(fileURL: url)
Code:
extension String {
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
Check the reading part.
The method cotentsOfFile: is a method of NSString class. And you have use it wrong way.
So replace this line
let text2 = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)
Here you have to use NSString instead of String class.
let text2 = NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)
I am using google drive SDK for uploading CSV file. I used code given in google but error is coming
func create(){
let fileData = FileManager.default.contents(atPath: "FL_insurance_sample.csv")
let folderId: String = self.fileId!
let metadata = GTLRDrive_File.init()
metadata.name = "FL_insurance_sample"
metadata.mimeType = "application/vnd.google-apps.spreadsheet"
metadata.parents = [folderId]
let uploadParameters = GTLRUploadParameters(data: fileData! , mimeType: "text/csv")
uploadParameters.shouldUploadWithSingleRequest = true
let query = GTLRDriveQuery_FilesCreate.query(withObject: metadata, uploadParameters: uploadParameters)
query.fields = "id"
self.service.executeQuery(query, completionHandler: {(ticket:GTLRServiceTicket, object:Any?, error:Error?) in
if error == nil {
// print("File ID \(files.identifier)")
}
else {
print("An error occurred: \(error)")
}
})
}
It showing nil error on uploadParameters at fileData. Can anyone help me out.
Variable fileData is nil because you've passed the wrong file path. To get correct file path use:
guard let filePath = Bundle.main.path(forResource: "FL_insurance_sample", ofType: "csv") else {
print("No such file in bundle")
// handle situation
return
}
guard let fileData = FileManager.default.contents(atPath: filePath) else {
print("Can't read file")
// handle situation
return
}
// upload fileData
I have this url https://storage.googleapis.com/user_avatars/63/img_-qLgH80SBqNhMRYbDQeccg.jpg
I need only qLgH80SBqNhMRYbDQeccg image name from this link In ui Image
You can use NSURL to safely isolate the filename then use substring to get the part you want.
let s = "https://storage.googleapis.com/user_avatars/63/img_-qLgH80SBqNhMRYbDQeccg.jpg"
Swift 2
if let url = NSURL(string: s),
withoutExt = url.URLByDeletingPathExtension,
name = withoutExt.lastPathComponent {
let result = name.substringFromIndex(name.startIndex.advancedBy(5))
print(result)
}
Swift 3
if let url = URL(string: s),
withoutExt = try? url.deletingPathExtension(),
name = withoutExt.lastPathComponent {
let result = name.substring(from: name.index(name.startIndex, offsetBy: 5))
print(result)
}
Swift 4
if let url = URL(string: s) {
let withoutExt = url.deletingPathExtension()
let name = withoutExt.lastPathComponent
let result = name.substring(from: name.index(name.startIndex, offsetBy: 5))
print(result)
}
Prints:
qLgH80SBqNhMRYbDQeccg
What about something that uses NSURLComponents to break up the URL:
func parseURLForFileName(url:String) ->String?
{
let components = NSURLComponents(string: url)
if let path:NSString = components?.path
{
let filename = path.lastPathComponent
if let range = filename.rangeOfString("_-")
{
return filename.substringFromIndex(range.endIndex)
}
}
return nil
}
You would then call it like this:
let name = parseURLForFileName("https://storage.googleapis.com/user_avatars/63/img_-qLgH80SBqNhMRYbDQeccg.jpg")
print(name)
I already have read Read and write data from text file
I need to append the data (a string) to the end of my text file.
One obvious way to do it is to read the file from disk and append the string to the end of it and write it back, but it is not efficient, especially if you are dealing with large files and doing in often.
So the question is "How to append string to the end of a text file, without reading the file and writing the whole thing back"?
so far I have:
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
var err:NSError?
// until we find a way to append stuff to files
if let current_content_of_file = NSString(contentsOfURL: fileurl, encoding: NSUTF8StringEncoding, error: &err) {
"\(current_content_of_file)\n\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}else {
"\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}
if err != nil{
println("CANNOT LOG: \(err)")
}
Here's an update for PointZeroTwo's answer in Swift 3.0, with one quick note - in the playground testing using a simple filepath works, but in my actual app I needed to build the URL using .documentDirectory (or which ever directory you chose to use for reading and writing - make sure it's consistent throughout your app):
extension String {
func appendLineToURL(fileURL: URL) throws {
try (self + "\n").appendToURL(fileURL: fileURL)
}
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
//test
do {
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent("logFile.txt")
try "Test \(Date())".appendLineToURL(fileURL: url as URL)
let result = try String(contentsOf: url as URL, encoding: String.Encoding.utf8)
}
catch {
print("Could not write to file")
}
Thanks PointZeroTwo.
You should use NSFileHandle, it can seek to the end of the file
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
let string = "\(NSDate())\n"
let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
if NSFileManager.defaultManager().fileExistsAtPath(fileurl.path!) {
var err:NSError?
if let fileHandle = NSFileHandle(forWritingToURL: fileurl, error: &err) {
fileHandle.seekToEndOfFile()
fileHandle.writeData(data)
fileHandle.closeFile()
}
else {
println("Can't open fileHandle \(err)")
}
}
else {
var err:NSError?
if !data.writeToURL(fileurl, options: .DataWritingAtomic, error: &err) {
println("Can't write \(err)")
}
}
A variation over some of the posted answers, with following characteristics:
based on Swift 5
accessible as a static function
appends new entries to the end of the file, if it exists
creates the file, if it doesn't exist
no cast to NS objects (more Swiftly)
fails silently if the text cannot be encoded or the path does not exist
class Logger {
static var logFile: URL? {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let dateString = formatter.string(from: Date())
let fileName = "\(dateString).log"
return documentsDirectory.appendingPathComponent(fileName)
}
static func log(_ message: String) {
guard let logFile = logFile else {
return
}
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let timestamp = formatter.string(from: Date())
guard let data = (timestamp + ": " + message + "\n").data(using: String.Encoding.utf8) else { return }
if FileManager.default.fileExists(atPath: logFile.path) {
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
} else {
try? data.write(to: logFile, options: .atomicWrite)
}
}
}
Here is a way to update a file in a much more efficient way.
let monkeyLine = "\nAdding a šµ to the end of the file via FileHandle"
if let fileUpdater = try? FileHandle(forUpdating: newFileUrl) {
// Function which when called will cause all updates to start from end of the file
fileUpdater.seekToEndOfFile()
// Which lets the caller move editing to any position within the file by supplying an offset
fileUpdater.write(monkeyLine.data(using: .utf8)!)
// Once we convert our new content to data and write it, we close the file and thatās it!
fileUpdater.closeFile()
}
Here's a version for Swift 2, using extension methods on String and NSData.
//: Playground - noun: a place where people can play
import UIKit
extension String {
func appendLineToURL(fileURL: NSURL) throws {
try self.stringByAppendingString("\n").appendToURL(fileURL)
}
func appendToURL(fileURL: NSURL) throws {
let data = self.dataUsingEncoding(NSUTF8StringEncoding)!
try data.appendToURL(fileURL)
}
}
extension NSData {
func appendToURL(fileURL: NSURL) throws {
if let fileHandle = try? NSFileHandle(forWritingToURL: fileURL) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.writeData(self)
}
else {
try writeToURL(fileURL, options: .DataWritingAtomic)
}
}
}
// Test
do {
let url = NSURL(fileURLWithPath: "test.log")
try "Test \(NSDate())".appendLineToURL(url)
let result = try String(contentsOfURL: url)
}
catch {
print("Could not write to file")
}
In order to stay in the spirit of #PointZero Two.
Here an update of his code for Swift 4.1
extension String {
func appendLine(to url: URL) throws {
try self.appending("\n").append(to: url)
}
func append(to url: URL) throws {
let data = self.data(using: String.Encoding.utf8)
try data?.append(to: url)
}
}
extension Data {
func append(to url: URL) throws {
if let fileHandle = try? FileHandle(forWritingTo: url) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
} else {
try write(to: url)
}
}
}
Update: I wrote a blog post on this, which you can find here!
Keeping things Swifty, here is an example using a FileWriter protocol with default implementation (Swift 4.1 at the time of this writing):
To use this, have your entity (class, struct, enum) conform to this protocol and call the write function (fyi, it throws!).
Writes to the document directory.
Will append to the text file if the file exists.
Will create a new file if the text file doesn't exist.
Note: this is only for text. You could do something similar to write/append Data.
import Foundation
enum FileWriteError: Error {
case directoryDoesntExist
case convertToDataIssue
}
protocol FileWriter {
var fileName: String { get }
func write(_ text: String) throws
}
extension FileWriter {
var fileName: String { return "File.txt" }
func write(_ text: String) throws {
guard let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw FileWriteError.directoryDoesntExist
}
let encoding = String.Encoding.utf8
guard let data = text.data(using: encoding) else {
throw FileWriteError.convertToDataIssue
}
let fileUrl = dir.appendingPathComponent(fileName)
if let fileHandle = FileHandle(forWritingAtPath: fileUrl.path) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
} else {
try text.write(to: fileUrl, atomically: false, encoding: encoding)
}
}
}
All answers (as of now) recreate the FileHandle for every write operation. This may be fine for most applications, but this is also rather inefficient: A syscall is made, and the filesystem is accessed each time you create the FileHandle.
To avoid creating the filehandle multiple times, use something like:
final class FileHandleBuffer {
let fileHandle: FileHandle
let size: Int
private var buffer: Data
init(fileHandle: FileHandle, size: Int = 1024 * 1024) {
self.fileHandle = fileHandle
self.size = size
self.buffer = Data(capacity: size)
}
deinit { try! flush() }
func flush() throws {
try fileHandle.write(contentsOf: buffer)
buffer = Data(capacity: size)
}
func write(_ data: Data) throws {
buffer.append(data)
if buffer.count > size {
try flush()
}
}
}
// USAGE
// Create the file if it does not yet exist
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
let fileHandle = try FileHandle(forWritingTo: fileURL)
// Seek will make sure to not overwrite the existing content
// Skip the seek to overwrite the file
try fileHandle.seekToEnd()
let buffer = FileHandleBuffer(fileHandle: fileHandle)
for i in 0..<count {
let data = getData() // Your implementation
try buffer.write(data)
print(i)
}