0

I want to store JSON text (as String) in a text file, or rather append each time I have fresh data to add. However, the following code always returns -1 as the return code from output.write(). I'm doing something wrong but I cannot figure out what:

let fileURL = (try! FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask)).first!.appendingPathComponent("data.json")

let json = "..."
let tenGB = 10 * 1000 * 1000 * 1000
if let output = OutputStream(url: fileURL, append: true) {
    output.open()
    let bytes = output.write(json, maxLength: tenGB)
    if bytes < 0 {
        print("Failure writing to disk")
    } else if bytes == 0 {
        print("Failure writing to disk (capacity)")
    } else {
        print("\(bytes) bytes written to disk")
    }
        output.close()
} else {
    print("Unable to open file")
}

I don't expect the data to be 10 GB at all, more in the kB-MB range, but I thought I'd give it a large value.

The output of streamError: Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument" UserInfo={_kCFStreamErrorCodeKey=22, _kCFStreamErrorDomainKey=1}

Max Power
  • 952
  • 9
  • 24
  • What does the streamError property tell you? – MartinM Aug 07 '19 at 08:30
  • Have you checked that the documents directory exists? – Michael Salmon Aug 07 '19 at 08:32
  • Not really an answer to your question but you can't append to a JSON string, it stops being JSON if you do. If you terminate each string with a newline then you can have ndjson. – Michael Salmon Aug 07 '19 at 08:41
  • 1
    did you try with smaller size :? for example et bytes = output.write(json, maxLength: json.utf8.count) :) – m1sh0 Aug 07 '19 at 08:43
  • I intend to create a file of JSON records, i.e. one _valid_ record per line – Max Power Aug 07 '19 at 08:44
  • you don't need to create one big file you just append data to the file – m1sh0 Aug 07 '19 at 08:46
  • `json.utf8.count` seems to do the trick... Damn. But that returns the [number of elements](https://developer.apple.com/documentation/swift/string/utf8view/2944675-count) rather than [bytes](https://developer.apple.com/documentation/foundation/outputstream/1410720-write). Would it be acceptable to multiply by 4 as that's the maximum number of bytes per code point for UTF-8? – Max Power Aug 07 '19 at 08:49
  • I haven't used streams but I don't think that you can do what you want to do. The definition is `func write(_ buffer: UnsafePointer, maxLength len: Int) -> Int` and a String is actually a small struct with the characters being stored elsewhere. I think that you need to convert to Data first. – Michael Salmon Aug 07 '19 at 08:49
  • 1
    @MaxPower it is a number of bytes, don't multiply it. – m1sh0 Aug 07 '19 at 08:57
  • Using the directly encoded JSON `Data` in `output.write` does not work. A `String` appears to be accepted. – Max Power Aug 07 '19 at 09:18

2 Answers2

2

As we understand in the comments the problem comes from the 10 GB

What you need is to write data as the size of the data switch the line:

let bytes = output.write(json, maxLength: tenGB)

with

 bytes = output.write(json, maxLength: json.utf8.count)

you need to append data after that, look this question doing something similar question

m1sh0
  • 2,236
  • 1
  • 16
  • 21
  • 1
    Does `append: true` in the `OutputStream` not already take care of appending? See: https://developer.apple.com/documentation/foundation/outputstream/1414446-init – Max Power Aug 07 '19 at 09:14
1

I wrapped the code in SwiftUI to test it:

import SwiftUI

let json = "[ 1, 2, 3, 4, 5 ]\n"

func stringWrite(_ string: String) {
    let fileURL = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first!.appendingPathComponent("data.json")

    if let output = OutputStream(url: fileURL, append: true) {
        output.open()
        let out = [UInt8](string.utf8)
        let bytes = output.write(out, maxLength: out.count)
        if bytes < 0 {
            print("Failure writing to disk")
            print("Error: \(String(describing: output.streamError))")
        } else if bytes == 0 {
            print("Failure writing to disk (capacity)")
        } else {
            print("\(bytes) bytes written to disk")
        }
            output.close()
    } else {
        print("Unable to open file")
    }
}

struct ContentView: View {
    var body: some View {
        Button(
            action: {stringWrite(json)},
            label: { Text("Do it") }
        )
    }
}

The stream expects a pointer to a UInt8 array. I also added printing the error and took the try away from FileManager as it doesn't throw anything. data.json looks like this after running a few times:

[ 1, 2, 3, 4, 5 ]
[ 1, 2, 3, 4, 5 ]
[ 1, 2, 3, 4, 5 ]
[ 1, 2, 3, 4, 5 ]

This query is more or less a duplicate of Writing a String to an NSOutputStream in Swift

Michael Salmon
  • 1,056
  • 7
  • 15