1

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

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.

slashlos
  • 913
  • 9
  • 17

2 Answers2

4

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)

Charles Srstka
  • 16,665
  • 3
  • 34
  • 60
  • Well I was still on the fence vs. writing data or a plist, but I'll keep this in mind, thanks! – slashlos Aug 15 '17 at 00:41
  • I ended up creating a variant function that returns a url for writing so I can use the suggested data write method: `try data.write(to: fileURL)` – slashlos Aug 16 '17 at 00:16
3

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;
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • You're a life saver. I spent hours trying to write some files (from a custom background queue) which failed for `data.write(to:)` and the `FileHandle` API but worked with the `manager.createFile` API. Are there any docs that confirm your statement that a file must be created first before writing the data? I have a different code snippet on the main queue which just works out of the box without `manager.createFile` API. This is truly a confusing behavior. – DevAndArtist Aug 01 '18 at 09:22
  • 1
    @DevAndArtist It's in the documentation for `FileHandle init?(forWritingTo: String)`: *"The initialized file handle object or nil if no file exists at path."*. – rmaddy Aug 03 '18 at 04:00