|
|||||
|
対象: ファイルアップロード(Swift)アプリからサーバに写真等のファイルをアップロードする。ファイルのアップロードは基本的にはHTTPのPOSTになる。 まずは送信対象のファイルをData(以前はNSMutableData)にまとめていく。と言っても、たった今撮影した画像をアップロードする場合と、既存の画像ファイルをアップロードする場合とではDataの作成の仕方が若干異なる。最初に、撮影直後の画像をJPEGに変換してアップロードすることを考える。 もし、UIImagePickerControllerを使っているなら、UIImageからDataを作成することになるだろう。その場合はUIImageJPEGRepresentation関数を利用してDataに変換する。
// UIImageからJPEGに変換してアップロード
let imageData = UIImageJPEGRepresentation(UIImage(named: fileName)!, 1.0)
AVFoundationを使っているなら、AVCaptureStillImageOutput.jpegStillImageNSDataRepresentationメソッドかAVCapturePhotoのfileDataRepresentationメソッド等でDataに変換する。この辺りが何を言っているか分からなければ、以下もご参照いただければと思う。 一方、既存のJPEGファイルをアップロードする場合は、Data(contentsOf:)イニシャライザでDataに変換できるだろう。この例では、予めプロジェクトに追加しておいた"DSCF0085.JPG"ファイルをDataに変換している。
let fileName = "DSCF0085.JPG"
let fileNameWithoutExt = (fileName as NSString).deletingPathExtension
let ext = (fileName as NSString).pathExtension
// 読み込んだJPEGファイルをそのままアップロード
let imageData = try! Data(contentsOf: Bundle.main.url(forResource: fileNameWithoutExt, withExtension: ext)!)
送信したいJPEGファイルのDataが準備できたら、アップロードの情報を編集していく。送信対象のデータをバウンダリではさみ、ファイル名やファイル形式の情報を付加していく。尚、ファイルの境界を表すバウンダリはユニークな文字列であれば何でも良いが、ここではWebKit系のそれを模してある。Content-Dispositionヘッダはアップロードするファイル名等のヘッダとなる。Content-Typeヘッダは今回で言えば"image/jpeg"となる。
let boundary = "----WebKitFormBoundaryZLdHZy8HNaBmUX0d"
func httpBody(_ fileAsData: Data, fileName: String) -> Data {
var data = "--\(boundary)\r\n".data(using: .utf8)!
// サーバ側が想定しているinput(type=file)タグのname属性値とファイル名をContent-Dispositionヘッダで設定
data += "Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!
data += "Content-Type: image/jpeg\r\n".data(using: .utf8)!
data += "\r\n".data(using: .utf8)!
data += fileAsData
data += "\r\n".data(using: .utf8)!
data += "--\(boundary)--\r\n".data(using: .utf8)!
return data
}
この後、ファイルアップロードに必要なものは、HTTP POSTするのと同様にURLRequestとURLSessionの2つである。まずはURLを指定してURLRequestを生成する。URLSessionはFoundation.URLSessionから新たに生成する。尚、マルチパートでアップロードする事になるので、ファイルの他にもJSON等のデータを載せる事ができるが、それについては割愛する。 取得したセッションのuploadTaskメソッドにURLRequestを渡して呼び出し、生成されたタスクのresumeメソッドを呼び出せばファイルをアップロードできる。今回はアップロード部分をメソッドにし、アップロード完了時にクロージャが呼ばれるようにしてみた。
// リクエストを生成してアップロード
func fileUpload(_ url: URL, data: Data, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
// マルチパートでファイルアップロード
let headers = ["Content-Type": "multipart/form-data; boundary=\(boundary)"]
let urlConfig = URLSessionConfiguration.default
urlConfig.httpAdditionalHeaders = headers
let session = Foundation.URLSession(configuration: urlConfig)
let task = session.uploadTask(with: request, from: data, completionHandler: completionHandler)
task.resume()
}
uploadTaskメソッドに渡すcompletionHandlerにはHTTPリクエスト完了時、Data、URLResponse、及びErrorの3つの引数が渡される。DataはHTTPリクエストが成功した場合にサーバから取得したデータ(Body)、URLResponseはHTTPヘッダやHTTPステータスコード、Errorはエラーがなければnilである。 completionHandler(クロージャ)の中ではHTTPレスポンスのうち、Body部分がData(バイト列)として渡される。URLResponseはHTTPURLResponseにキャストすることによってstatusCodeを得ることができる。 コード全体を以下に示す。
import UIKit
class ViewController: UIViewController {
let boundary = "----WebKitFormBoundaryZLdHZy8HNaBmUX0d"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func upload(_ sender: Any) {
let fileName = "DSCF0085.JPG"
let fileNameWithoutExt = (fileName as NSString).deletingPathExtension
let ext = (fileName as NSString).pathExtension
// UIImageからJPEGに変換してアップロード
//let imageData = UIImageJPEGRepresentation(UIImage(named: fileName)!, 1.0)!
// 読み込んだJPEGファイルをそのままアップロード
let imageData = try! Data(contentsOf: Bundle.main.url(forResource: fileNameWithoutExt, withExtension: ext)!)
let body = httpBody(imageData, fileName: fileName)
let url = URL(string: "http://192.168.0.xx:8080/upload")!
fileUpload(url, data: body) {(data, response, error) in
if let response = response as? HTTPURLResponse, let _: Data = data , error == nil {
if response.statusCode == 200 {
print("Upload done")
} else {
print(response.statusCode)
}
}
}
}
func httpBody(_ fileAsData: Data, fileName: String) -> Data {
var data = "--\(boundary)\r\n".data(using: .utf8)!
// サーバ側が想定しているinput(type=file)タグのname属性値とファイル名をContent-Dispositionヘッダで設定
data += "Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)!
data += "Content-Type: image/jpeg\r\n".data(using: .utf8)!
data += "\r\n".data(using: .utf8)!
data += fileAsData
data += "\r\n".data(using: .utf8)!
data += "--\(boundary)--\r\n".data(using: .utf8)!
return data
}
// リクエストを生成してアップロード
func fileUpload(_ url: URL, data: Data, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "POST"
// マルチパートでファイルアップロード
let headers = ["Content-Type": "multipart/form-data; boundary=\(boundary)"]
let urlConfig = URLSessionConfiguration.default
urlConfig.httpAdditionalHeaders = headers
let session = Foundation.URLSession(configuration: urlConfig)
let task = session.uploadTask(with: request, from: data, completionHandler: completionHandler)
task.resume()
}
}
確認に使用したRubyのスクリプト(uploadserver.rb)も載せておく。これをruby uploadserver.rb -o 0.0.0.0 -p 8080で起動した。
require 'sinatra'
post '/upload' do
filename = params[:file][:filename]
file = params[:file][:tempfile]
File.open("./#{filename}", 'wb') do |f|
f.write(file.read)
end
"Upload"
end
(2018/07/09)
Copyright© 2004-2019 モバイル開発系(K) All rights reserved.
[Home]
|
|||||