alexiscn / SynologyKit

Synology File Station SDK for Swift
MIT License
37 stars 8 forks source link

HELP WANTED - Upload image to Synology NAS #9

Closed CPiersigilli closed 4 years ago

CPiersigilli commented 4 years ago

I have this code to upload image to Synology NAS.

/// Upload di una immagine nella cartella "/Test/Prova/Immagine_01.jpeg".
    func uploadImage (imageName:String, filename:String, destinationFolderPath:String) {
        guard let img = UIImage(named: imageName) else {
            self.error = "L'immagine non esiste nel Preview Assets."
            print("L'immagine non esiste in Preview Assets.")
            return
        }
        guard let pngdata = img.pngData() else {
            return
        }
        print("pngData \(imageName): \(pngdata.count)")

        self.progress = 0
        self.isFinished = false

        var options = SynologyClient.UploadOptions()
        options.overwrite = true
        options.modificationTime = Int64(NSDate().timeIntervalSince1970*1000)
//        options.modificationTime = Int64(Date().timeIntervalSince1970*1000) - This not work

        client.upload(data: pngdata, filename: filename, destinationFolderPath: destinationFolderPath, createParents: true, options: options) { (result) in
            switch result {
            case .success(let request, let isStream, let streamFileURL):
                print("isStream: \(isStream) - streamFileURL: \(String(describing: streamFileURL?.absoluteURL))")
                request.uploadProgress { progress in
                    print("progress: \(self.progress)")
                    self.progress = progress.fractionCompleted*100
                }.response { response in
                    if response.error == nil {
                        self.isFinished = true
                        print("Uploaded: \(filename)")
                    }
                }
            case .failure(let error):
                self.isShowingAlert = true
                self.error = error.localizedDescription
                print("error: \(error.localizedDescription)")
            }
        }
    }

In this video I have not error (see output) but the image don't upload to NAS (see folder). Upload_1 In this video I have no errors (see output) but the upload of the image to the NAS (see folder) has been made, even if after a while that the loading is finished (progress = 1 * 100). Upload_2

How can I change the code to make sure the image has been uploaded to the NAS? Thank you.

CPiersigilli commented 4 years ago

I did other tests and noticed that if the loading time of the image exceeds 10 seconds, the NAS will not load the image even though it does not return any errors. I don't know how to modify the code so that it doesn't happen that an image is not loaded in the NAS, without having returned an error message. I hope you can figure out how to solve this problem.

CPiersigilli commented 4 years ago

I tried in every way, for example, to verify that the image had been transferred, I also used the function client.md5 (ofFile :, but I discovered that it does not always work, since several times it did not calculate the MD5, even if the image had been transferred. Only when it manages to calculate the MD5, then it is certain that the image was transferred. Could you tell me what you think and if what is happening to me happens to you too? Have you idea how to solve the problem?

alexiscn commented 4 years ago

I will try it in the example.

alexiscn commented 4 years ago

Please check out the example for details.

UploadImage() of BrowserViewController.

CPiersigilli commented 4 years ago

I tested your SynologyKitExample and it works fine, but if I add two image (image_1920.jpg and image_HR.jpg) with difference resolution and upload one of them, your SynologyKitExample doesn't work anymore. The output shows no error, but the image hasn't been uploaded. Here is my modified code for your tests. SynologyKit-master_Rev_CPiersigilli.zip I hope you will be able to modify the code to make it work and be sure that when it is without an error, the image has been uploaded. Thank you.

alexiscn commented 4 years ago

Your image format is jpg, but you get data with pngData(), I think you should use .jpegData(compressionQuality: 1.0).

CPiersigilli commented 4 years ago

You're right, sorry, unfortunately your excellent SynologyKit doesn't always work. I am attaching the output of the same image uploaded (image_1920.jpg) The only difference is that the image that has not been loaded has:

**(Duration) 10.517915 seconds**
(Request Header Bytes) 89
**(Request Body Transfer Bytes) 1933320**
(Request Body Bytes) 1932051
(Response Header Bytes) 180
**(Response Body Transfer Bytes) 56**
**(Response Body Bytes) 38**

and the image that was uploaded has:

**(Duration) 0.502085 seconds**
(Request Header Bytes) 89
**(Request Body Transfer Bytes) 1933194**
(Request Body Bytes) 1932051
(Response Header Bytes) 180
**(Response Body Transfer Bytes) 106**
**(Response Body Bytes) 88**

The problem concerns the loading time, but I don't know how to avoid it and I don't understand why if the time is over 10 seconds the image is not loaded. I hope you can understand and solve.

CPiersigilli commented 4 years ago

I changed your private func uploadImage() { like this:

private func uploadImage() {
        let imageName = "image_HR"
        guard let img = UIImage(named: imageName), let jpgdata = img.jpegData(compressionQuality: 1), let folder = folderPath else {
            print("Error from uploadImage: \(imageName) - \(folderPath)")
            return
        }
        var options = SynologyClient.UploadOptions()
        options.overwrite = true
        options.modificationTime = Int64(Date().timeIntervalSince1970*1000)
        client.upload(data: jpgdata, filename: "\(imageName).jpg", destinationFolderPath: folder, createParents: true, options: options) { (result) in
            switch result {
            case .success(let request, let isStream, let streamFileURL):
                print("isStream: \(isStream) - streamFileURL: \(String(describing: streamFileURL?.absoluteURL))")
                request.uploadProgress { progress in
                    print("progress: \(progress.fractionCompleted)")
                }.response { response in
                    print("------------")
                    print("Status Code: \(response.response!.statusCode)")
                    print("------------")
                    print("Duration: \(response.metrics?.taskInterval.duration ?? -1) sec")
                    print("------------")
                    print("TimeOutInterval: \(response.request?.timeoutInterval ?? -1) sec")
                    if response.error == nil && response.metrics!.taskInterval.duration <= 10.0 {
                        print("\(imageName) uploaded")
                        print("------------")
                    } else {
                        print("\(imageName) not uploaded, but no error.")
                        print("------------")
                    }
                }
            case .failure(let error):
                print("error: \(error.localizedDescription)")
            }
        }
    }

I added the upload duration check with: if response.error == nil && response.metrics!.taskInterval.duration <= 10.0 {. I don't know for which reason, but now it seems to work. Do you have any ideas about this? You didn't tell me if you too experienced my own problems.

alexiscn commented 4 years ago

You may have a look at how Alamofire upload data. https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#uploading-data-to-a-server

SynologyKit upload function just wraps that.

CPiersigilli commented 4 years ago

To carry out further tests to upload file or image in Synology NAS, I would need you to tell me how to get what is indicated in the "Synology File Station" manual. What I expect to find is:

POST /webapi/FileStation/api_upload.cgi ...
Content-Length:20326728
Content-type: multipart/form-data, boundary=AaB03x
--AaB03x
content-disposition: form-data; name="api"
SYNO.FileStation.Upload
--AaB03x
content-disposition: form-data; name="version"
1
--AaB03x
content-disposition: form-data; name="method"
upload
--AaB03x
content-disposition: form-data; name="dest_folder_path"
/upload/test
--AaB03x
content-disposition: form-data; name="create_parents"
true
--AaB03x
content-disposition: form-data; name="file"; filename="file1.txt" Content-Type: application/octet-stream
... contents of file1.txt ... 
--AaB03x-- 

I would like to try to use, only for uploading, a routine other than Alamofire, like the one shown here or here. I am convinced that by trying other ways we will be able to solve the problem. Thank you.

alexiscn commented 4 years ago

You catch the network traffic by Charles, and check out what’s going wrong

CPiersigilli commented 4 years ago

I downloaded and installed CharlesProxi, but the results do not seem different from those obtained by printing the response variable. Can you tell me how to look for something that helps me understand more? Did it also happen to you that, without receiving any error, the image was not uploaded?

CPiersigilli commented 4 years ago

I keep testing on your SynologyKit and I realized you used this: parameters.append (UploadParam (key: "path", value: destinationFolderPath)) instead of parameters.append (UploadParam (key: "dest_folder_path", value: destinationFolderPath)) as stated in the Synology_File_Station_API_Guide at page 64. I'm wondering why is that?

The funny thing is that using dest_folder_path it does not work at all and it doesn't show any errors either. It seems that when duration is more than 10 seconds it doesn't show any errors. Where did you find the key you used to upload the file correctly? Why did you use path instead of dest_folder_path? There may be some other wrong keys.

Best Regards, Cesare Piersigilli

alexiscn commented 4 years ago

Can you provide a demo ?

CPiersigilli commented 4 years ago

This is a demo app in SwiftUI. SynologyKit-SwiftUIExample.zip I have added in your SynologyClient.swift two methods: upload1 and upload2.

In upload1 I have changed only parameters.append (UploadParam (key: "path", value: destinationFolderPath)) with parameters.append (UploadParam (key: "dest_folder_path", value: destinationFolderPath)) as stated in the Synology_File_Station_API_Guide at page 64.

In upload2 I tried to write a routine to upload an image without using Alamofire, but I can't make it work.

I hope that with this demo you will be able to understand why your upload sometimes does not work, while not giving any errors.

I deleted some images, otherwise I would not have been able to upload the file, because it would have exceeded the limit of 10 Mb.

CPiersigilli commented 4 years ago

I downloaded Postman and found the way to properly upload an image to a Synologuy NAS. Here is the screenshot of how I did it after logging in.

Schermata 2019-12-28 alle 17 50 03

Now I have to figure out how to translate it into Swift. Hope you can do it. I confirm that path must be used and not dest_folder_path.

alexiscn commented 4 years ago

I just change dest_folder_path to path, and change the /Test/Prova to my path "/Downloads". And all three upload test pass. So I can not figure out what's going wrong...

CPiersigilli commented 4 years ago

Registrazione schermo 2019-12-29 alle 15 58 05-2 The video above shows how the first three images were uploaded without problems (see folder "Test"), the fourth one was not (see folder "Test"), without giving any errors (status code: 200 - see output). I don't know what the problem is, but sometimes the image doesn't load. I tried to use the Swift code produced by Postman, but it doesn't work. Could you post the video of the upload of the 5 images contained in my app, with my app, using the button 3 Upload Image?

Schermata 2019-12-29 alle 16 24 52
CPiersigilli commented 4 years ago

I have found the solution. I added two methods in your SynologyClient.swift: public func uploadFido ( and func createBody (. I extracted the two methods from here. Please use it and tell me if you can convert the two methods for use with Alamofire, because I have not succeeded.

Schermata 2019-12-31 alle 16 08 56

If you press Upload Image 3, obtain, for example this error: L'immagine non esiste in Preview Assets. or this result:

L'immagine image_01.jpg è stata salvata nel NAS con server_response: {
    data =     {
        blSkip = 0;
        file = "image_01.jpg";
        pid = 24722;
        progress = 1;
    };
    success = 1;
}

and the image was actually saved to the NAS. I take this opportunity to wish you a happy new year.

alexiscn commented 4 years ago

I have replicated the problem. Sometimes NAS response with

{"success": false, "error": { "code": 401 } }

Sometimes NAS response with successful results. I do not know what's wrong.

I have updated my code to report such error.

CPiersigilli commented 4 years ago

I have done many tests and even to me, sometimes, for no reason, the server responds with the error 401, which is not Account disabled but Unknown error of file operation (see pag. 9 of Synology File Station - Official API). In my opinion, the problem continues to be Alamofire, but I have not been able to understand how Alamofire constructs the body of the request and all the other parameters. What we should get is the one shown below and that I get from the routine that I inserted in the app that I attached in the previous post.

POST https://www.mydomain.com:5001/webapi/entry.cgi
Content-Length:20326728
Content-type: multipart/form-data, boundary=AaB03x

--Boundary-50B54811-BBF4-40E5-B687-A9DE4E58963D
content-disposition: form-data; name="api"

SYNO.FileStation.Upload
--Boundary-50B54811-BBF4-40E5-B687-A9DE4E58963D
content-disposition: form-data; name="version"

2
--Boundary-50B54811-BBF4-40E5-B687-A9DE4E58963D
content-disposition: form-data; name="method"

upload
--Boundary-50B54811-BBF4-40E5-B687-A9DE4E58963D
content-disposition: form-data; name="_sid"

Fwtsg4okP8pxI18C0PDN731510
--Boundary-50B54811-BBF4-40E5-B687-A9DE4E58963D
content-disposition: form-data; name="path"

/Test/Prova
--Boundary-50B54811-BBF4-40E5-B687-A9DE4E58963D
content-disposition: form-data; name="create_parents"

true
--Boundary-50B54811-BBF4-40E5-B687-A9DE4E58963D
content-disposition: form-data; name="image_640.jpg";filename="image_640.jpg"
Content-Type: application/octet-stream

----contents of image data----
--Boundary-50B54811-BBF4-40E5-B687-A9DE4E58963D-- 

Do you know how to print the parameters that Alamofire creates in order to compare them with those listed above? I do not give up and continue to test, since I think the use of Alamofire is convenient because with it many parameters can be controlled, otherwise very difficult to control.

alexiscn commented 4 years ago

You may use charles to catch the internet traffic.

CPiersigilli commented 4 years ago

Can you tell me how to use CharlesProxi to check internet traffic? I tried but I was unable to extract the request body, to check if it is correct.

CPiersigilli commented 4 years ago

With Alamofire 4.9.1 the code below works perfectly, but I was unable to implement the 'progress' variable. If you can, I'd be happy.

public func uploadCustomAFRequest(imageData: Data, filename: String, destinationFolderPath: String, createParents: Bool, options: UploadOptions? = nil, progressHandler: UploadRequest.ProgressHandler? = nil, completion: @escaping SynologyCompletion<UploadResponse>) {

    var parameters: [UploadParam] = []
    parameters.append(UploadParam(key: "api", value: SynologyAPI.upload.rawValue))
    parameters.append(UploadParam(key: "version", value: "2"))
    parameters.append(UploadParam(key: "method", value: SynologyMethod.upload.rawValue))
    if let sid = self.sessionid {
        parameters.append(UploadParam(key: "_sid", value: sid))
    }
    parameters.append(UploadParam(key: "path", value: destinationFolderPath))
    parameters.append(UploadParam(key: "create_parents", value: String(createParents)))

    if let options = options {
        if let overwrite = options.overwrite {
            parameters.append(UploadParam(key: "overwrite", value: String(overwrite)))
        }
        if let mtime = options.modificationTime {
            parameters.append(UploadParam(key: "mtime", value: String(mtime)))
        }
        if let crtime = options.createTime {
            parameters.append(UploadParam(key: "crtime", value: String(crtime)))
        }
        if let atime = options.accessTime {
            parameters.append(UploadParam(key: "atime", value: String(atime)))
        }
    }

    let url = URL(string: baseURLString().appending("webapi/entry.cgi"))!

    var folderPath = destinationFolderPath
    if(folderPath == ""){
        folderPath = "/"
    }

    let boundary = "Boundary-\(UUID().uuidString)"

    var urlRequest = URLRequest(url: url,cachePolicy: URLRequest.CachePolicy.useProtocolCachePolicy,timeoutInterval: 60)
    urlRequest.httpMethod = "POST"
    urlRequest.httpShouldHandleCookies = true

    let body = self.createBody(parameters: parameters, contentFile: imageData, folderPath, filename, boundary)

    urlRequest.httpBody = body as Data
    urlRequest.addValue(String(describing: body.length), forHTTPHeaderField: "Content-Length")
    urlRequest.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

    Alamofire.request(urlRequest)
        .response { response in
            guard let data = response.data else {
                completion(.failure(.invalidResponse(response)))
                return
            }
            do {
                let decodedRes = try JSONDecoder().decode(UploadResponse.self, from: data)
                if decodedRes.success {
                    completion(.success(decodedRes))
                } else {
                    let code = decodedRes.error?.code ?? -1
                    let msg = SynologyErrorMapper[code] ?? "Unknow error"
                    completion(.failure(.serverError(code, msg, response)))
                }
            } catch {
                let text = String(data: data, encoding: .utf8)
                completion(.failure(.decodeDataError(response, text)))
            }
    }    
}

func createBody(parameters: [UploadParam], contentFile: Data,_ folderPath: String,_ filename: String,_ boundary: String) -> NSMutableData {
    let body = NSMutableData()

    for param in parameters {
        body.append(String("--\(boundary)\r\n").data(using: .utf8)!)
        body.append(String("content-disposition: form-data; name=\"\(param.key)\"\r\n\r\n\(param.value)\r\n").data(using: .utf8)!)
    }

    body.append(String("--\(boundary)\r\n").data(using: .utf8)!)
    body.append(String("content-disposition: form-data; name=\"\(filename)\";filename=\"\(filename)\"\r\n").data(using: .utf8)!)
    body.append(String("Content-Type: application/octet-stream\r\n\r\n").data(using: .utf8)!)

    print(String(decoding: body, as: UTF8.self)+"----image data----"+"\r\n--\(boundary)--\r\n")

    body.append(contentFile)

    body.append(String("\r\n--\(boundary)--\r\n").data(using: .utf8)!)

    return body
}
CPiersigilli commented 4 years ago

PROBLEM SOLVED I solved the problem by modifying your files to be compatible with Alamofire 5.0.0.beta.1 and now public func uploadManualRequest ( works very well with progress variable. Source-SynologyKit.zip To make them work, the download functions and some other parts of the code remain to be modified. Thank you for your SynologyKit. Cesare Piersigilli

CPiersigilli commented 4 years ago

Here is your SynologyKit updated to Alamofire 5.0.0.beta.1 in all its components. I also tested public func upload ( and it seems to be working fine now. Source-SynologyKit.zip Please update your SynologyKit. Thank you. Cesare Piersigilli

alexiscn commented 4 years ago

@CPiersigilli Hi,I have upgrade Alamofire to 5.1.0

Can you test that whether the upload issue still exists. Thanks.

CPiersigilli commented 4 years ago

I test your SynologyKit, but don't work, because in your Package.swift is write:

dependencies: [
        // Dependencies declare other packages that this package depends on.
         .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.9.1"),
    ],

Please upgrade your Package.swift, to download Alamofire 5.1.

alexiscn commented 4 years ago

I test your SynologyKit, but don't work, because in your Package.swift is write:

dependencies: [
        // Dependencies declare other packages that this package depends on.
         .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.9.1"),
    ],

Please upgrade your Package.swift, to download Alamofire 5.1.

Updated.

CPiersigilli commented 4 years ago

I tested your SynologyKit and now work fine. Remember to update the branch, because I had to import the source manually.

alexiscn commented 4 years ago

Try 1.0.0

CPiersigilli commented 4 years ago

Now your SynologyKit works well in iOS, but, the same code, in MacOS with Catalyst, it doesn't work and I don't understand why. The error from your SynologyError is: Unknown Error This is the code:

fileprivate func clientLogin() {
        client.login(account: self.preferences.username, passwd: self.preferences.password) { response in
            switch response {
            case .success(let authRes):
                client.updateSessionID(authRes.sid)
                self.showFolderListView = true
                self.hideNavigationBar = false
                print(authRes.sid)
            case .failure(let error):
                print("error.description: \(error)")
                self.showFolderListView = false
                self.showAlert = true
                self.hideNavigationBar = true
                self.title = "\(error as SynologyError)"
            }
        }
    }

Can you tell me what I need to change?

CPiersigilli commented 4 years ago

I was wrong. SynologyKit also works on Mac OS with Catalyst.