0

Here is my swift code that uploads the file from application.

final class UploadService {

  // I'm renting a remote server and the php code is on that
  private let videoUploadPath = "http://myUploadPath.php"

  var uploadsSession: URLSession!
  var activeUploads: [URL: Upload] = [:]

  func startUpload(medium: Medium) {

    guard let url = URL(string: videoUploadPath) else { return }

    var request = URLRequest(url: url)
    request.httpMethod = "POST"

    let upload = Upload(medium: medium)
    upload.task = self.uploadsSession.uploadTask(with: request, fromFile: medium.avUrlAsset.url)
    upload.task?.resume()
    upload.isUploading = true
    activeUploads[upload.medium.avUrlAsset.url] = upload
  } 
}

Note that I have to use

func uploadTask(with request: URLRequest, fromFile fileURL: URL) -> URLSessionUploadTask

// Apple documentation: Parameters request A URL request object that provides the URL, cache policy, request type, and so on. The body stream and body data in this request object are ignored.

to upload video from phone, since that's the only method that allows background upload (videos are normally pretty big, therefore I HAVE TO make background upload possible). However, this function discard all body data.

Here is the my PHP code that receives the uploaded file and stores it in the desired location:

$directory = "../storage/videos/";
$destination = $directory . basename($_FILES["file"]["name"]);

if (move_uploaded_file($_FILES["file"]["tmp_name"], $destination)) {
  $returnArray["status"] = "200";
  $returnArray["message"] = "Upload success";
  echo json_encode($returnArray);
} else {
  $returnArray["status"] = "300";
  $returnArray["message"] = "Upload failed" . $_FILES["file"]["error"];
  echo json_encode($returnArray);
  return;
}

To be honest, I know it won't succeed, because I have no way to specify the file type and name, which are required by $_FILES["file"]["name"] in the PHP code.

Therefore, my question is, how to specify file name and type to be uploaded? Since the function func uploadTask(with request: URLRequest, fromFile fileURL: URL) -> URLSessionUploadTask discards all http body data, as I mentioned, perhaps making an http header that contains the file type and name? OR, is there another way for PHP to receive the file I just uploaded? For now, the $_FIELS is completely empty. I'm new to PHP, sorry if I didn't say it clearly enough.

Also, for Content-Type, I was using multipart/form-data when I upload an image to change user's profile image, I append the image data together with other data like uid, then attach those data as the body data in the http body. But now, I need to upload a single file, because all body data in the request will be ignored per iOS requirement. How can I do this?

Many days stuck in this, please help. Many thanks!!

David Wu
  • 105
  • 2
  • 13
  • Any http clients, include iOS client, can specify Content-type , the http body MIME type, in the http header. Then when you get the request header you can read it out. I’m not familiar with PHP, so how to do it in your language specific region is up to your effort. – Neal.Marlin Sep 08 '18 at 03:00
  • @Neal.Marlin, yes, I know that Content-Type could be specified in the http header, but multipart/form-data is not the way that could be transmitted in the background. Instead, I need to parse http request with a raw body in the PHP file. – David Wu Sep 08 '18 at 03:51
  • why you have to use `multipart/form-data` format? If you just transfer a single audio file, you don't have to use `multipart/form-data` format. You can just transfer it the way just like a image file, the raw body in http request is the file data. If you must transfer it as a `multipart/form-data` format, you can make the file to a `multipart/form-data` format and upload the `multipart/form-data` format content as the file. Then when you get the raw http body data, you can use a `multipart/form-data` parser to get your file data and related file information, such as file name, type and etc. – Neal.Marlin Sep 08 '18 at 09:28
  • @Neal.MarlinThanks very much for your detailed reply. iOS only allows background upload when the data is written in a file and you use a function that uploads that file only, in which case any body data will be ignored. Therefore, I'm puzzled about if I need to use `multipart/form-data` or something else? What I know so far is that when I change profile image, I need to upload a new image, to do this, I need to get the image data, then append other body data like uid, and upload those data using `multipart/form-data`. But now I need to upload a single file only, what Content-Type should I use? – David Wu Sep 08 '18 at 16:18
  • 1
    You need ‘Content-Type: multipart/form-data; boundary = $(bound)’ in http header. The most important thing is to build a multipart form body and serialize it to a file, then post it. You can use Alamofire, a third party Swift networking framework to accomplish your task more easily. – Neal.Marlin Sep 08 '18 at 17:19
  • @Neal.Marlin Thanks for your solution. This makes a lot of sense to me. I will try it in a moment and see how it will work. Also I'm practicing using all native functions to get it done, thus I won't use a 3rd party framework for now. – David Wu Sep 10 '18 at 18:44
  • Of course, you can get it done with all native functions and trying to build a multipart/form-data post body all by yourself is such good thing for you. So, good luck. :) – Neal.Marlin Sep 11 '18 at 00:50

1 Answers1

0

There is a way to keep the URLSession.shared.uploadTask call and use the raw data on the PHP side. Here's an example that worked for me with JSON data:

Using sample code from https://developer.apple.com/documentation/foundation/url_loading_system/uploading_data_to_a_website for the Swift side

SWIFT side:

struct Order: Codable {
    let customerId: String
    let items: [String]
}

// ...

let order = Order(customerId: "12345",
                  items: ["Cheese pizza", "Diet soda"])
guard let uploadData = try? JSONEncoder().encode(order) else {
    return
}
let url = URL(string: "https://example.com/upload.php")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.uploadTask(with: request, from: uploadData) { data, response, error in
    if let error = error {
        print ("error: \(error)")
        return
    }
    guard let response = response as? HTTPURLResponse,
        (200...299).contains(response.statusCode) else {
        print ("server error")
        return
    }
    if let mimeType = response.mimeType,
        mimeType == "application/json",
        let data = data,
        let dataString = String(data: data, encoding: .utf8) {
        print ("got data: \(dataString)")
    }
}
task.resume()

PHP side: (upload.php)

<?php
header('Content-Type: application/json; charset=utf-8');
$response = array();
try {  
  $uploaddir = './uploads/';
  $uploadfile = $uploaddir . 'test.json';
  $rawdata = file_get_contents('php://input');

  if (!file_put_contents($uploadfile, $rawdata)) {
    throw new RuntimeException('Failed to create file.');
  }
  $response = array(
    "status" => "success",
    "error" => false,
    "message" => "File created successfully"
  );
  echo json_encode($response);
  
} catch (RuntimeException $e) {
  $response = array(
    "status" => "error",
    "error" => true,
    "message" => $e->getMessage()
  );
  echo json_encode($response);
}
?>

This will also write binary data into a file. In this simple case, there is no need to know the file name or content type. It may not work for a generic case, but this way of proceeding allows you to keep using URLSession.shared.uploadTask and avoid HTTP Body parameters. HTH.

Didier B.
  • 61
  • 4