Repository: parse-community/Parse-Swift (swift)

Issue:
Update ParseFile with already known url
I am able to save the ParseFile struct encoded as data in to a local realm database and then convert it back, but after I try to update the content i cannot get it saved:

    file.save { results in
          switch results {
          case .success(let savedFile):
              ....
          case .failure(let error):
              print("error saving file: \(file)")
              print(error)
          }

> error saving file: ParseFile(__type: "File", localId: 2478C4A2-3302-46D1-BB75-19A0E7B09641, name: "2adcf21bf5a9bb8223ba693fd58963dc_0.jpg", url: Optional(https://parsefiles.back4app.com/coYfuTYV3z35Z8iwv8q3NOTdGQs2ywJtt7tUUg4p/2adcf21bf5a9bb8223ba693fd58963dc_0.jpg), localURL: nil, cloudURL: nil, data: Optional(71997 bytes), mimeType: nil, metadata: nil, tags: nil, options: Set([]))

> ParseError(code: ParseSwift.ParseError.Code.unknownError, message: "Error decoding parse-server response: Optional(<NSHTTPURLResponse: 0x7feec2f04d00> { URL: https://parseapi.back4app.com/files/2adcf21bf5a9bb8223ba693fd58963dc_0.jpg } { Status Code: 404, Headers {... }) with error: The data couldn’t be read because it is missing. Format: Optional(\"{\\\"message\\\":\\\"Not Found\\\",\\\"error\\\":{}}\")")

as you can see the url in request is not the url that I have already in the file. Is it correct that the the following SDK code is generating the url again?

    private static func updateFileCommand(_ object: ParseFile) -> API.Command<ParseFile, ParseFile> {
            API.Command(method: .PUT,
                        path: .file(fileName: object.name),
                        uploadData: object.data,
                        uploadFile: object.localURL) { (data) -> ParseFile in
                try ParseCoding.jsonDecoder().decode(FileUploadResponse.self, from: data).apply(to: object)
            }
        }

the **path: .file(fileName: object.name),** seems to get it wrong.. shouldn't there be **path: object.url** ?

Pull request description:
Should error when attempting to update a ParseFile that is already saved. It seems the parse-server doesn't support updating a ParseFile from the [server tests](https://github.com/parse-community/parse-server/blob/9ea355b4635226ae4da17c8cc5fb0321e3fdec5e/spec/ParseFile.spec.js) (no `PUT`) and also from what's shown in the [REST documentation](https://docs.parseplatform.org/rest/guide/#files).  Instead another file should be created and the older file should be deleted later by the developer.

- [x] Remove updateFileCommand
- [x] Add testcases 
- [x] Add changelog

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index da98b3bd4..2a3afbdd9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 * _Contributing to this repo? Add info about your change here to be included in the next release_
 
 __Fixes__
+- ParseFiles can't be updated from the client and will now throw an error if attempted. Instead another file should be created and the older file should be deleted by the developer. ([#144](https://github.com/parse-community/Parse-Swift/pull/144)), thanks to [Corey Baker](https://github.com/cbaker6).
 - Fixed issue where Swift SDK prevented fetching of Parse objects when custom objectId was enabled ([#139](https://github.com/parse-community/Parse-Swift/pull/139)), thanks to [Corey Baker](https://github.com/cbaker6).
 
 __Improvements__
diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift
index e10771902..0478d4534 100644
--- a/Sources/ParseSwift/API/API+Commands.swift
+++ b/Sources/ParseSwift/API/API+Commands.swift
@@ -254,11 +254,13 @@ internal extension API {
 
 internal extension API.Command {
     // MARK: Uploading File
-    static func uploadFileCommand(_ object: ParseFile) -> API.Command<ParseFile, ParseFile> {
-        if object.isSaved {
-            return updateFileCommand(object)
+    static func uploadFileCommand(_ object: ParseFile) throws -> API.Command<ParseFile, ParseFile> {
+        if !object.isSaved {
+            return createFileCommand(object)
+        } else {
+            throw ParseError(code: .unknownError,
+                             message: "File is already saved and cannot be updated.")
         }
-        return createFileCommand(object)
     }
 
     // MARK: Uploading File - private
@@ -271,15 +273,6 @@ internal extension API.Command {
         }
     }
 
-    private static func updateFileCommand(_ object: ParseFile) -> API.Command<ParseFile, ParseFile> {
-        API.Command(method: .PUT,
-                    path: .file(fileName: object.name),
-                    uploadData: object.data,
-                    uploadFile: object.localURL) { (data) -> ParseFile in
-            try ParseCoding.jsonDecoder().decode(FileUploadResponse.self, from: data).apply(to: object)
-        }
-    }
-
     // MARK: Downloading File
     static func downloadFileCommand(_ object: ParseFile) -> API.Command<ParseFile, ParseFile> {
         API.Command(method: .GET,
diff --git a/Sources/ParseSwift/Types/ParseFile.swift b/Sources/ParseSwift/Types/ParseFile.swift
index 9730c9814..0e33d04d2 100644
--- a/Sources/ParseSwift/Types/ParseFile.swift
+++ b/Sources/ParseSwift/Types/ParseFile.swift
@@ -427,14 +427,25 @@ extension ParseFile {
                 switch result {
 
                 case .success(let fetched):
-                    fetched.uploadFileCommand()
-                        .executeAsync(options: options,
+                    do {
+                        try fetched.uploadFileCommand()
+                            .executeAsync(options: options,
                                       callbackQueue: callbackQueue,
                                       uploadProgress: progress) { result in
-                            callbackQueue.async {
-                                completion(result)
+                                callbackQueue.async {
+                                    completion(result)
+                                }
+                            }
+                    } catch {
+                        callbackQueue.async {
+                            if let parseError = error as? ParseError {
+                                completion(.failure(parseError))
+                            } else {
+                                let parseError = ParseError(code: .unknownError, message: error.localizedDescription)
+                                completion(.failure(parseError))
                             }
                         }
+                    }
                 case .failure(let error):
                     callbackQueue.async {
                         completion(.failure(error))
@@ -442,20 +453,31 @@ extension ParseFile {
                 }
             }
         } else {
-            uploadFileCommand()
-                .executeAsync(options: options,
-                              callbackQueue: callbackQueue,
-                              uploadProgress: progress) { result in
-                    callbackQueue.async {
-                        completion(result)
+            do {
+                try uploadFileCommand()
+                    .executeAsync(options: options,
+                                  callbackQueue: callbackQueue,
+                                  uploadProgress: progress) { result in
+                        callbackQueue.async {
+                            completion(result)
+                        }
+                    }
+            } catch {
+                callbackQueue.async {
+                    if let parseError = error as? ParseError {
+                        completion(.failure(parseError))
+                    } else {
+                        let parseError = ParseError(code: .unknownError, message: error.localizedDescription)
+                        completion(.failure(parseError))
                     }
                 }
+            }
         }
 
     }
 
-    internal func uploadFileCommand() -> API.Command<Self, Self> {
-        return API.Command<Self, Self>.uploadFileCommand(self)
+    internal func uploadFileCommand() throws -> API.Command<Self, Self> {
+        try API.Command<Self, Self>.uploadFileCommand(self)
     }
 }
 


Test patch:
diff --git a/Tests/ParseSwiftTests/ParseFileTests.swift b/Tests/ParseSwiftTests/ParseFileTests.swift
index fd50b6842..d140e4561 100644
--- a/Tests/ParseSwiftTests/ParseFileTests.swift
+++ b/Tests/ParseSwiftTests/ParseFileTests.swift
@@ -68,14 +68,14 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length
         wait(for: [expectation1, expectation2], timeout: 20.0)
     }
 
-    func testUploadCommand() {
+    func testUploadCommand() throws {
         guard let url = URL(string: "http://localhost/") else {
             XCTFail("Should have created url")
             return
         }
         let file = ParseFile(name: "a", cloudURL: url)
 
-        let command = file.uploadFileCommand()
+        let command = try file.uploadFileCommand()
         XCTAssertNotNil(command)
         XCTAssertEqual(command.path.urlComponent, "/files/a")
         XCTAssertEqual(command.method, API.Method.POST)
@@ -84,7 +84,7 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length
 
         let file2 = ParseFile(cloudURL: url)
 
-        let command2 = file2.uploadFileCommand()
+        let command2 = try file2.uploadFileCommand()
         XCTAssertNotNil(command2)
         XCTAssertEqual(command2.path.urlComponent, "/files/file")
         XCTAssertEqual(command2.method, API.Method.POST)
@@ -92,6 +92,17 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length
         XCTAssertNil(command2.body)
     }
 
+    func testUploadCommandDontAllowUpdate() throws {
+        guard let url = URL(string: "http://localhost/") else {
+            XCTFail("Should have created url")
+            return
+        }
+
+        var file = ParseFile(cloudURL: url)
+        file.url = url
+        XCTAssertThrowsError(try file.uploadFileCommand())
+    }
+
     func testDeleteCommand() {
         guard let url = URL(string: "http://localhost/") else {
             XCTFail("Should have created url")
@@ -375,6 +386,19 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length
         }
     }
 
+    func testUpdateFileError() throws {
+        guard let sampleData = "Hello World".data(using: .utf8) else {
+            throw ParseError(code: .unknownError, message: "Should have converted to data")
+        }
+        var parseFile = ParseFile(name: "sampleData.txt",
+                                  data: sampleData,
+                                  metadata: ["Testing": "123"],
+                                  tags: ["Hey": "now"])
+        parseFile.url = URL(string: "http://localhost/")
+
+        XCTAssertThrowsError(try parseFile.save())
+    }
+
     func testFetchFileStream() throws {
         let tempFilePath = URL(fileURLWithPath: "\(temporaryDirectory)sampleData.dat")
         guard let sampleData = "Hello World".data(using: .utf8) else {
@@ -642,6 +666,28 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length
         wait(for: [expectation1], timeout: 20.0)
     }
 
+    func testUpdateErrorAysnc() throws {
+
+        guard let sampleData = "Hello World".data(using: .utf8) else {
+            throw ParseError(code: .unknownError, message: "Should have converted to data")
+        }
+        var parseFile = ParseFile(name: "sampleData.txt", data: sampleData)
+        parseFile.url = URL(string: "http://localhost/")
+
+        let expectation1 = XCTestExpectation(description: "ParseFile async")
+        parseFile.save { result in
+
+            switch result {
+            case .success:
+                XCTFail("Should have returned error")
+            case .failure(let error):
+                XCTAssertTrue(error.message.contains("File is already"))
+            }
+            expectation1.fulfill()
+        }
+        wait(for: [expectation1], timeout: 20.0)
+    }
+
     #if !os(Linux) && !os(Android)
     // swiftlint:disable:next inclusive_language
     func testDeleteNoMasterKeyFileAysnc() throws {