Upload Files to Webserver Using Rest Api
Welcome to a new, hopefully exciting tutorial! In a previous post I showed to you the process of creating a custom class that manages spider web requests and RESTful APIs. Today, we will proceed building on it, as I would like to focus on a specific use case: How to upload files to a server!
Uploading files might not be one of the virtually common things when dealing with web services. Nonetheless, information technology can be proved to be a tedious task to perform when information technology's time to send files to a server. In the implementation steps that follow we will effort to interruption things downward and shed lite to the key points and the details of the uploading process. Before we get there though, it's necessary to have a quick discussion almost some footing knowledge that nosotros all should have on this topic.
A Quick Intro To "Multipart/form-data" Content Blazon
Before we commencement doing bodily piece of work, it's necessary some important things to exist mentioned showtime. Let me start by saying that in order to upload files to a server, multipart/form-data is the content type that should be specified in the web request. This content type allows to send files or large amounts of information in combination with other usual data that should be posted. "Multipart/course-data" content type tells to HTTP request that posted information should exist broken into parts, every bit if they were to be posted by a web form that expects from users to make full in various fields and select files that should be submitted to a server.
Since posted data is broken into parts, information technology's necessary for the server to know where a part starts and where it ends. For that purpose, a special and unique cord is provided along with the content blazon, called purlieus. That string should not occur in the actual data, so it must be as much unique as possible. It always starts with two dashes ("–"), with an arbitrary combination of other alphanumeric characters coming later on. Usually, boundaries start with multiple dashes, and then they have an alphanumeric suffix (eastward.1000. —————–abc123).
Each office of a multipart body necessarily starts with a Content-Disposition header, with the form-data value coming in pair with it. An aspect called "name" should likewise be provided in the header, equally it specifies the name of the part. Discover that names don't need to be unique, and sometimes server sets the rules that apply to the "name" aspect. These two key-value pairs are enough when adding single data (meaning no files) to the request'southward HTTP body. When appending files data, the filename should be besides included in the "Content-Disposition" header with the original name of the file, too as the content blazon (MIME type) of each file that is near to exist uploaded.
The following is a imitation case of a HTTP request body that uses the "multipart/grade-data" content blazon:
ane two 3 iv 5 6 seven 8 9 ten xi 12 13 14 fifteen xvi 17 18 19 xx 21 22 23 | Content-Type : multipart/class-data ; boundary=-----------------------------abc123 -----------------------------abc123 Content-Disposition : class-data ; name="username" usernameValue -----------------------------abc123 Content-Disposition : form-data ; name="password" passwordValue -----------------------------abc123 Content-Disposition : course-data ; name="aFile" ; filename="avatar.png" Content-Type : image/png . . . contents of avatar . png file . . . -----------------------------abc123 Content-Disposition : form-information ; name="anotherFile" ; filename="info.pdf" Content-Type : application/pdf . . . contents of info . pdf file . . . -----------------------------abc123-- |
Notice how everything mentioned in the previous paragraphs is used. At start, the "multipart/form-data" content type is specified forth with the boundary string that separates the data parts. Run into how purlieus indicates the first of each part and too see how semicolon (";") separates attributes in headers. Line breaks are also important when building a HTTP trunk such the to a higher place i. In unmarried fields, an empty line exists betwixt the "Content-Disposition" header and the actual field value, while the boundary of the next function comes correct afterward in the next line. In file parts, the "filename" attribute contains the name of the file, while an additional empty line exists betwixt the file contents and the adjacent boundary. The trunk catastrophe is highlighted by the boundary, plus two more dashes as a suffix to information technology.
I am encouraging y'all to accept a await at the W3C HTML Specification and read more about encoding content types and the "multipart/form-data" especially. You don't have to cease in that location of course; a general search on the web will return lots of resources to read about this topic.
Well-nigh The Demo App
So, as I said in the beginning of this mail service, nosotros are going to keep building on the custom class nosotros created in the previous tutorial, called RestManager
. To become started, please download a starter packet which contains a Xcode projection with that class and one more than directory with a demo server implementation (see next office). In Xcode project you will discover three files that we'll employ to test file uploading afterwards we finish all implementation steps:
- A text file named SampleText.txt with "lorem ipsum" data generated here.
- A PDF file named SamplePDF.pdf taken from File Examples.
- An paradigm file named SampleImage.jpg downloaded from Pexels (Photo by Oleg Magni from Pexels).
No UI will be in our app, and the results of our last tests will be printed in Xcode panel and in Terminal. Any input values will be difficult-coded. Therefore, nosotros'll entirely focus on the file uploading feature that we'll add together to the RestManager
class. Obviously, you lot are costless to create any UI you desire if you want to create a more dynamic demo application.
Almost The Server
After we end implementing all the new code we'll run into in the following parts, we'll need to test if file uploading is actually working. For that purpose, a simple server implemented in Node.js is included in the starter package that you downloaded; y'all will find it in the Server subdirectory. You can keep information technology in the location that currently is, or copy it anywhere else you want in your disk.
In gild to run the server, you must accept Node.js installed on your computer. If you don't, delight cheque here or here on how to do that. Open Terminal and type the following control:
There is a infinite graphic symbol after the cd
command. Then switch to Finder, and drag and drop the Server directory to terminal and press the Return key:
By doing so, you don't have to blazon the path to the server directory; it'due south automatically appended to the command in final.
To verify that you are successfully in the server directory, merely type:
This command will show the electric current directory contents, and if you see something similar to the next one, then you're just fine:
To start the server just type:
You should see the bulletin:
Server started successfully on port 3000!
The server is now running at address http://localhost:3000. You tin can also verify that if you paste that address in a new tab in your browser. You'll run into a bulletin coming from the server.
Annotation: If you are already running another server at port 3000, edit the index.js file and fix a custom port number to the port
variable. And then restart the server with the node alphabetize.js
control.
Requests made to "http" addresses are non allowed by default in iOS equally they are considered insecure. However, for the sake of the tutorial, localhost has been whitelisted in the Info.plist file of the starter project so you will meet no trouble in testing the app later.
Representing Files
The offset thing nosotros need to take intendance of is how files are going to be represented in the RestManager
grade. For any file that is about to be uploaded, we need to accept the following data available at the time of the HTTP body grooming:
- The actual file contents.
- The original file proper name. Remember that the filename aspect must exist in the "Content-Disposition" header of each part that represents a file.
- The part's name for the proper name aspect in the "Content-Disposition" header.
- The content blazon (MIME blazon) of the file.
Obviously, all that information could exist stored in a dictionary, just that wouldn't exist the best approach in Swift. To practise information technology better, let's create a struct which we'll call FileInfo
. Open the RestManager.swift file in the starter Xcode project, and get to the end of it. Yous volition notice the following empty extension:
// Marking: - File Upload Related Implementation extension RestManager { } |
This is where we'll add together most all new code regarding the file uploading feature. Within this extension, add the following construction:
struct FileInfo { var fileContents : Data ? var mimetype : Cord ? var filename : String ? var proper name : String ? } |
The 4 properties will keep the data described earlier. As yous will meet subsequently, if any of the higher up properties is nil the file won't be added to the HTTP trunk for submission to the server.
We tin can make the initialization of a FileInfo
object more friendly if we add the following custom initializer:
struct FileInfo { . . . init ( withFileURL url : URL ? , filename : Cord , name : String , mimetype : String ) { guard let url = url else { return } fileContents = try ? Data ( contentsOf : url ) self . filename = filename self . name = name cocky . mimetype = mimetype } } |
With this initializer, information technology won't be necessary to provide the actual file contents when creating a FileInfo
object. Specifying the URL of the file will be enough. File contents will be read in the to a higher place initializer.
Creating The Boundary
Having a solution on our hands well-nigh how to correspond files, let's create a method which will be responsible of creating the purlieus string. Remember that a boundary must exist unique and definitely non an ordinary string that could be potentially constitute in the actual data that will be uploaded. As I said in the offset of the post, even though boundaries start with two dashes ("–"), they usually have several more dashes following and a random alphanumeric cord at the terminate. That's not mandatory, only it'south the logic we will follow here.
Right subsequently the FileInfo
struct, define the following private method:
private func createBoundary ( ) -> Cord ? { } |
I will evidence yous two different means to generate the random boundary string.
Using A UUID String
The fastest mode to get a random cord is to generate a UUID value:
var uuid = UUID ( ) . uuidString |
The in a higher place will generate something similar to this:
D41568F4-7175-42BB-9503-DAA282180D70 |
Let's become rid of the dashes in that string, and permit's convert all letters to lowercase:
uuid = uuid . replacingOccurrences ( of : "-" , with : "" ) uuid = uuid . map { $ 0 . lowercased ( ) } . joined ( ) |
The original UUID volition at present expect like this:
d41568f4717542bb9503daa282180d70 |
Let's construct the boundary string. Information technology will be a concatenation of 20 dashes at the beginning and the transformed UUID value:
let boundary = String ( repeating : "-" , count : 20 ) + uuid |
If yous like exaggerating, add together the current timestamp to the stop as well:
let boundary = Cord ( repeating : "-" , count : 20 ) + uuid + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" |
A boundary string created with the above will await similar:
--------------------d41568f4717542bb9503daa282180d70579430569 |
Well, that looks quite unique and random, no?
Here's the implementation of the unabridged method:
private func createBoundary ( ) -> String ? { var uuid = UUID ( ) . uuidString uuid = uuid . replacingOccurrences ( of : "-" , with : "" ) uuid = uuid . map { $ 0 . lowercased ( ) } . joined ( ) let boundary = String ( repeating : "-" , count : 20 ) + uuid + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" return boundary } |
Using Random Characters
As an alternative to the above we tin can create a mechanism which will pick random characters from a collection of available characters, and using them to class a string which will be appended to the boundary string. The drove of available characters will exist parted by all letters ranging from upper cased "A" to "Z", lower cased "a" to "z", and all digits from "0" to "9".
We won't actually need to difficult-lawmaking anything, every bit we can programmatically construct everything. We will be based on the ASCII table for that.
Nosotros'll start by specifying the range of the lower cased characters ("a" to "z") in the ASCII table every bit shown below:
let lowerCaseLettersInASCII = UInt8 ( ascii : "a" ) . . . UInt8 ( ascii : "z" ) |
The above is equivalent to this:
let lowerCaseLettersInASCII = 97 . . . 122 |
where 97 is the position of the "a" graphic symbol and "122" is the position of the "z" grapheme in the ASCII tabular array.
However, the second line of code requires from us to search for an ASCII table online so locate the position of the characters we are interested in into the table. Okay, it'southward easy, but it'southward definitely not the recommended way, since we can get the values nosotros desire by using the UInt8(ascii:)
initializer. And that'southward we do in the first place.
Similarly, we become the ranges of the upper cased A-Z and of the digits:
allow upperCaseLettersInASCII = UInt8 ( ascii : "A" ) . . . UInt8 ( ascii : "Z" ) let digitsInASCII = UInt8 ( ascii : "0" ) . . . UInt8 ( ascii : "9" ) |
Now, let's join all these ranges into a collection, or in other words a sequence of ranges (closed ranges more than particularly) with aim to get the actual characters afterwards:
let sequenceOfRanges = [ lowerCaseLettersInASCII , upperCaseLettersInASCII , digitsInASCII ] . joined ( ) |
If we print the value of the sequenceOfRanges
to the console at runtime we'll get this:
FlattenSequence < Array < ClosedRange < UInt8 > > > ( _base : [ ClosedRange ( 97 . . . 122 ) , ClosedRange ( 65 . . . 90 ) , ClosedRange ( 48 . . . 57 ) ] ) |
Even though it's not obvious unless someone looks up for it, the in a higher place tin be hands converted into a String value:
guard let toString = String ( data : Data ( sequenceOfRanges ) , encoding : . utf8 ) else { return nil } |
Information
struct provides several initializers for creating a data object and there is ane among them that accepts a sequence every bit an statement, exactly as we practice in the Data(sequenceOfRanges)
expression. From that data object, we can create the following string which is assigned to the toString
constant:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 |
That cool! Let's generate a string of twenty random characters at present:
var randomString = "" for _ in 0 . . < twenty { randomString += String ( toString . randomElement ( ) ! ) } |
At first we initialize a string value called randomString
. So, we create a loop that volition be executed 20 times. In it, we pick a random character from the toString
string using the randomElement()
method, and we generate a new String value (String(toString.randomElement()!)
). This new String value is appended to the randomString
.
Note that is safety to forcefulness unwrap the value of the randomElement()
method, equally it returns nil but in cases of empty collections. Hither we know that toString
won't be empty.
The following is a random value of the randomString
:
Finally, we can build the boundary string:
let boundary = String ( repeating : "-" , count : twenty ) + randomString + "\ ( Int ( Date . timeIntervalSinceReferenceDate ))" |
Here is a sample of the purlieus:
--------------------ZveNCE7Ptg3J2HaVLDfN579434247 |
The createBoundary()
method with the second implementation in i place:
private func createBoundary ( ) -> Cord ? { let lowerCaseLettersInASCII = UInt8 ( ascii : "a" ) . . . UInt8 ( ascii : "z" ) let upperCaseLettersInASCII = UInt8 ( ascii : "A" ) . . . UInt8 ( ascii : "Z" ) let digitsInASCII = UInt8 ( ascii : "0" ) . . . UInt8 ( ascii : "9" ) allow sequenceOfRanges = [ lowerCaseLettersInASCII , upperCaseLettersInASCII , digitsInASCII ] . joined ( ) guard let toString = String ( data : Data ( sequenceOfRanges ) , encoding : . utf8 ) else { render nil } var randomString = "" for _ in 0 . . < 20 { randomString += Cord ( toString . randomElement ( ) ! ) } allow boundary = String ( repeating : "-" , count : xx ) + randomString + "\ ( Int ( Engagement . timeIntervalSinceReferenceDate ))" return boundary } |
Utilise the implementation yous prefer the most. The second one is more "Swifty" only it requires a bit of more code. At the stop of the twenty-four hour period, both approaches are going to work equally well.
An important note: I've mentioned already that the boundary cord which separates the parts of a multipart body starts with ii dashes ("–"). These two dashes are not included in the dashes of the purlieus string nosotros generated in both approaches here. This string will be provided every bit-is to the request every bit a asking header along with the content type and server will try to locate it after the 2 dashes prefix. As well, a purlieus string can exist with no dashes at all; we just add together them to minimize the possibility to notice similar string in the uploaded data. As you will see later, the ii dashes prefix will be manually appended whenever necessary.
Extending Data Structure
Our next steps involve the preparation of the HTTP torso using any capricious data provided to the class, as well as using the files data. But earlier nosotros get into that, we volition extend the Information
structure and we will create the following generic method:
mutating func append < T > ( values : [ T ] ) -> Bool { } |
The purpose of this method is to permit us easily append the values of the values
drove to the data object that calls information technology. And as you'll see, we'll be interested for String
and Data
types but.
Just for clarification, nosotros could avoid implementing this method. However, the code that we volition add to it would have to be repeated multiple times in different points in the RestManager
course, and that definitely would non be a wise move.
And then, to continue go to the end of the RestManager.swift file where you will find a Information
extension:
Add together the new method's definition in it:
extension Information { mutating func append < T > ( values : [ T ] ) -> Bool { } } |
At first, we'll declare the following two local variables:
var newData = Information ( ) var status = true |
Adjacent, nosotros'll distinguish the type of the given values. Let's start with the String type. In this instance, we'll brand a loop to access all values in the values
parameter collection:
if T . self == String . cocky { for value in values { } } |
In each repetition nosotros will catechumen the cord value into a Data
object and nosotros will append it to the local newData
variable. If for some reason the string value cannot exist converted into a Information
object, we'll fix the status
flag to faux and we'll break the loop.
guard let convertedString = ( value every bit ! String ) . information ( using : . utf8 ) else { status = simulated ; break } newData . append ( convertedString ) |
We will follow a quite similar approach in case of Data
input values. Of class, there is no demand to initialize any new Data
object or make a conversion of any blazon. Nosotros are appending one data value to some other:
else if T . self == Information . self { for value in values { newData . append ( value as ! Data ) } } |
Lastly, allow's indicate that we don't care nigh any other type of values:
else { condition = false } |
Adjacent, nosotros'll bank check the status
value. If it's true, then we can append the newData
local variable to the self
object (the Data
object that is used to call this method).
if status { cocky . append ( newData ) } |
At the finish, we should not forget to return the status
equally the outcome of the method:
Here's the entire implementation. We are going to put information technology in activity starting from the next part.
1 2 3 4 five 6 7 8 9 10 xi 12 13 fourteen 15 xvi 17 18 19 twenty 21 22 23 24 25 | extension Data { mutating func suspend < T > ( values : [ T ] ) -> Bool { var newData = Data ( ) var status = true if T . self == String . cocky { for value in values { baby-sit let convertedString = ( value every bit ! String ) . data ( using : . utf8 ) else { status = false ; break } newData . append ( convertedString ) } } else if T . self == Data . self { for value in values { newData . append ( value every bit ! Data ) } } else { condition = false } if status { self . append ( newData ) } render status } } |
Creating the HTTP Trunk
In the current implementation of RestManager
there is a method named getHttpBody()
. Its purpose is to prepare the HTTP torso with the information that will exist posted to the server. Although this method works great in whatsoever other instance, unfortunately it's not of much assistance in example of file uploading. There is the boundary cord we take to take into business relationship, as well equally the special headers and formatting required when using the "multipart/form-information" content type. To serve our new needs, we'll implement a similarly named method which will be accepting the purlieus cord as an argument (also known as method overloading).
In the new extension of the RestManager
course, right below the createBoundary
method, add the post-obit:
private func getHttpBody ( withBoundary boundary : String ) -> Data { var body = Data ( ) return body } |
Keep in heed that the HTTP body must be a Data
value, and so nosotros are initializing such a value in this method, and this is also what the method returns. In this method nosotros'll deal with any data that should be posted to the server except for files. That'due south the data that would be usually submitted if there were no files to upload at the same time, and it'due south kept in the httpBodyParameters
property (as a reminder, httpBodyParameters
is a property in the RestManager
class and it's of RestEntity
type, a custom structure – find it in RestManager
and read more in the previous tutorial about it).
httpBodyParameters
has a method chosen allValues()
and returns all data every bit a dictionary (a [Cord: String]
lexicon). We'll apply it to admission all values that should exist sent to the server and append them to the body
variable. Right subsequently the var trunk = Data()
line add together the following:
for ( key , value ) in httpBodyParameters . allValues ( ) { } |
A minor cease here now as nosotros take to hash out what exactly nosotros'll be appending to the trunk. Let's see over again function of the example presented in the beginning of this post:
-----------------------------abc123 Content-Disposition : form-data ; proper name="username" usernameValue -----------------------------abc123 Content-Disposition : form-data ; name="password" passwordValue |
In this example the data is the username and the password. The following utilize to each piece of data:
- At first there is the purlieus string, and right after that a line suspension. In HTTP headers, a line break is marked with "\r\n" (railroad vehicle return and new line character), not but the "\n" that we are mostly used to. Programmatically, this could exist written like:
"--\(purlieus)\r\n"
(come across the 2 dashes before the boundary string). - Next, at that place is the "Content-Disposition" header with the
name
attribute only in information technology. Header is followed by a line interruption ii times. Nosotros could write this similar so:"Content-Disposition: form-information; name=\"\(central)\"\r\n\r\due north"
. - Lastly, it's the actual value followed by a line interruption. That's easy:
"\(value)\r\n"
.
Nosotros will add together the code that represents each stride described higher up into an array:
allow values = [ "--\ ( purlieus )\r\n" , "Content-Disposition: class-data; name=\"\ ( key )\"\r\due north\r\n" , "\ ( value )\r\n" ] |
Nosotros volition use for first time the append(values:)
custom method we implemented in the previous pace in order to catechumen these strings into Data
objects and append them to the body
variable:
_ = body . append ( values : values ) |
And that's the last thing we had to do in this method. Let's see it altogether now:
private func getHttpBody ( withBoundary boundary : String ) -> Information { var body = Data ( ) for ( key , value ) in httpBodyParameters . allValues ( ) { let values = [ "--\ ( purlieus )\r\n" , "Content-Disposition: form-information; name=\"\ ( key )\"\r\n\r\north" , "\ ( value )\r\n" ] _ = body . append ( values : values ) } return body } |
We'll apply the results of this method in a while. For now, nosotros take to add the files information to the HTTP body as well.
Calculation Files To HTTP Body
One could say that the getHttpBody(withBoundary:)
method we just implemented along with the new 1 nosotros volition implement here consist of the most important part of the overall work we have to do in guild to make file uploading possible. And that would be pretty much true, equally nosotros've built all the helper methods we need and now nosotros are dealing with the core functionality.
So, continuing on building the HTTP body, let's define the following new method:
private func add ( files : [ FileInfo ] , toBody body : inout Information , withBoundary boundary : String ) -> [ String ] ? { } |
Let's talk showtime about the parameters. The showtime ane is a drove of FileInfo
objects, and information technology contains the data for all files that are about to be uploaded. The 2nd parameter value is the data object that represents the HTTP body. Any changes that will be made to that object inside this method will be reflected out of it every bit well because information technology's marked with the inout
keyword. The concluding parameter is the purlieus string, equally we necessarily demand information technology to dissever data parts.
Y'all might exist wondering why this method returns an optional array of String values. Well, in case in that location are files whose information cannot exist added to the HTTP body, so we'll keep their names into an array, which in plough the method will return. In normal conditions this method should render nil, significant that information from all files was successfully appended to the HTTP torso data.
Let's start adding some lawmaking, with the first 1 being the following local variables:
var status = true var failedFilenames : [ String ] ? |
condition
volition betoken whether all pieces of data for each single file in the files
collection were successfully combined in one Data
object, which can exist then appended to the body
inout parameter. If status
is fake, nosotros'll exist appending the name of the matching file to the failedFilenames
array.
Let's commencement a loop now:
The kickoff thing nosotros take to do is to make sure that all properties of each file
object take actual values and so we tin go along:
guard let filename = file . filename , let content = file . fileContents , let mimetype = file . mimetype , permit name = file . proper name else { continue } |
Adjacent, we volition set the initial value of the condition
flag on each repetition of the loop to false, and we'll initialize a new Data
object.
condition = simulated var data = Data ( ) |
Now, let's see again the example presented in the beginning of the tutorial then we empathise what we have to practice:
-----------------------------abc123 Content-Disposition : form-data ; proper noun="aFile" ; filename="avatar.png" Content-Blazon : epitome/png . . . contents of avatar . png file . . . -----------------------------abc123 Content-Disposition : form-data ; name="anotherFile" ; filename="info.pdf" Content-Type : application/pdf . . . contents of info . pdf file . . . -----------------------------abc123-- |
Going step by step through the lines that describe a file part:
- At kickoff there is the boundary with the line break at the end. We already know how to write that in code.
- Adjacent, we have the "Content-Disposition" header. The improver here (comparing to the header in the previous function) is the new
filename
attribute which contains the actual file name. In code such a header is written like this:"Content-Disposition: class-data; name=\"\(name)\"; filename=\"\(filename)\"\r\n"
. - Right afterward we have the content type of the file. Meet all the available MIME Media Types. In lawmaking this is like then:
"Content-Type: \(mimetype)\r\due north\r\due north"
.
Permit'southward make a break hither and let's append all the higher up to an array:
let formattedFileInfo = [ "--\ ( boundary )\r\n" , "Content-Disposition: form-information; name=\"\ ( name )\"; filename=\"\ ( filename )\"\r\n" , "Content-Type: \ ( mimetype )\r\n\r\n" ] |
Let'southward convert all strings in that array into Information
objects and suspend them to the data
variable:
if data . append ( values : formattedFileInfo ) { } |
Let's proceed where nosotros had stopped from. The next particular in a file role is the actual file contents. Call up that file contents are represented by the fileContents
property in a FileInfo
object, which is a Information
object. So far we were dealing with strings only. File contents must be appended to the information
variable too:
if data . append ( values : [ content ] ) { } |
Retrieve that suspend(values:)
method expects for an array of values, so it's necessary to include content
into the array's opening and closing brackets higher up.
Lastly, notice in the to a higher place case that at that place is an empty line right afterward the file contents which should be added to the data
likewise:
if data . suspend ( values : [ "\r\n" ] ) { } |
These three weather we wrote must be embedded into each other. If all of them are true, then all data pieces for the current file were successfully added to the data
object, and we'll indicate that by making the status
true:
if data . append ( values : formattedFileInfo ) { if data . append ( values : [ content ] ) { if information . append ( values : [ "\r\northward" ] ) { condition = true } } } |
See that nosotros used the custom append(values:)
custom method iii times in a row here. I promise you agree that its implementation was meaningful since we use it again and over again.
Next, permit'south check the status
value for each file. While still existence on the loop:
if status { trunk . append ( data ) } else { if failedFilenames == nil { failedFilenames = [ String ] ( ) } failedFilenames ? . append ( filename ) } |
If condition
is true, we append the data
variable to the body
which represents the HTTP trunk. If non, then we initialize the failedFilenames
array in example it's non initialized already, and we keep the proper noun of the current file in it.
Ane final matter remaining, to return the failedFilenames
from the method:
Our new method should now expect like this:
1 2 3 4 5 6 7 8 nine 10 eleven 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | individual func add ( files : [ FileInfo ] , toBody body : inout Data , withBoundary boundary : String ) -> [ String ] ? { var status = true var failedFilenames : [ Cord ] ? for file in files { guard let filename = file . filename , let content = file . fileContents , let mimetype = file . mimetype , let name = file . name else { continue } condition = faux var data = Data ( ) allow formattedFileInfo = [ "--\ ( boundary )\r\north" , "Content-Disposition: form-information; name=\"\ ( name )\"; filename=\"\ ( filename )\"\r\n" , "Content-Type: \ ( mimetype )\r\north\r\n" ] if data . suspend ( values : formattedFileInfo ) { if data . append ( values : [ content ] ) { if data . suspend ( values : [ "\r\n" ] ) { status = true } } } if status { body . append ( data ) } else { if failedFilenames == nil { failedFilenames = [ String ] ( ) } failedFilenames ? . append ( filename ) } } render failedFilenames } |
Endmost The HTTP Body
Now that we created methods which build the HTTP body past appending any post information and file information, we must create one more which will close the body. Remember that in "multipart/form-data" the HTTP trunk closing is marked by the boundary string and two dashes as a suffix to it:
-----------------------------abc123-- |
Every bit yous can guess, doing and so doesn't require much of work as all it takes is this:
private func close ( body : inout Data , usingBoundary purlieus : Cord ) { _ = torso . suspend ( values : [ "\r\n--\ ( boundary )--\r\northward" ] ) } |
For one more fourth dimension here the body
parameter is marked as inout
, so the information statement will be passed by reference and the changes made to information technology inside this method will become visible to the caller too. Besides that, notice the line breaks before and after the closing string which ensure that the closing boundary will be the only content in the line.
It's really important not to forget to call this method and point the cease of parts in the multipart HTTP torso.
Uploading Files
Information technology'southward virtually time to put everything together and make file uploading possible. The method we'll write hither will exist public, so you can go and add it to the top of the form along with other two public methods existing already. Here is its definition:
func upload ( files : [ FileInfo ] , toURL url : URL , withHttpMethod httpMethod : HttpMethod , completion : @ escaping ( _ result : Results , _ failedFiles : [ Cord ] ? ) -> Void ) { } |
In accordance to what we did to the other two existing public methods, we are going to perform all deportment in this method asynchronously. Nosotros won't run anything on the main thread since file uploading could take significant amount of fourth dimension and we don't want apps to prove frozen. In code that means:
DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in } |
With userInitiated
value in the quality of service parameter we give our task a relatively high priority in execution. Note that nosotros marking cocky
as weak in the closure since the RestManager
instance used to perform the file uploading can potentially become nil, and that practically ways that cocky
is from now on an optional. This introduces a couple of new needs equally you will run across next.
The first actual action we have to have is to add any URL query parameters specified in the urlQueryParameters
belongings to the URL. This volition happen by calling the addURLQueryParameters(toURL:)
method which we implemented in the previous tutorial:
let targetURL = self ? . addURLQueryParameters ( toURL : url ) |
Next, let's call the createBoundary()
method we implemented today and let's create the purlieus string:
guard let boundary = self ? . createBoundary ( ) else { completion ( Results ( withError : CustomError . failedToCreateBoundary ) , nix ) ; return } |
Notice that since self
is used every bit an optional, purlieus
becomes an optional value also, regardless of the fact that createBoundary()
does not return an optional. So, in case at that place's no boundary cord to go on, nosotros call the completion handler passing the mistake shown above and we return from the method. This custom error doesn't exist still in the class, we'll add it in a while.
Let's get going, and in the side by side stride allow's add together the "multipart/course-data" along with the boundary cord to the drove of the request headers:
self ? . requestHttpHeaders . add ( value : "multipart/form-data; boundary=\ ( boundary )" , forKey : "content-type" ) |
To refresh your memory, requestHttpHeaders
is a RestEntity
holding which keeps all HTTP request headers equally central-value pairs. It'south important to highlight that since nosotros specify the content type header hither, there is no need to provide a content type header manually while preparing the request. Not just information technology's redundant, it's also unsafe as information technology could create conflicts and make the server turn down the asking.
Next, let's start preparing the HTTP trunk. We'll start by calling the getHttpBody(withBoundary:)
method:
guard var body = self ? . getHttpBody ( withBoundary : boundary ) else { completion ( Results ( withError : CustomError . failedToCreateHttpBody ) , naught ) ; return } |
Once over again, since self
is an optional, torso
might be nil in case cocky
is aught. So, in that instance we phone call the completion handler with another custom error and we return from the method.
Time to add together the files to be uploaded to the HTTP torso. Discover in the next line that nosotros pass the body
variable with the "&" symbol as that's an inout
parameter value:
let failedFilenames = self ? . add ( files : files , toBody : &body , withBoundary : purlieus ) |
failedFilenames
is either nothing if all files are successfully added to the HTTP body, or it contains the names of those files that failed to exist appended to the body.
We should not forget to close the HTTP body properly:
self ? . close ( body : &body , usingBoundary : boundary ) |
We are ready now to create the URL asking:
guard let request = self ? . prepareRequest ( withURL : targetURL , httpBody : body , httpMethod : httpMethod ) else { completion ( Results ( withError : CustomError . failedToCreateRequest ) , nothing ) ; return } |
The method nosotros use here is already implemented in the RestManager
form and we discussed about it in the previous tutorial. Notice that we pass the URL with whatever potential query items (targetURL
) and the HTTP body every bit arguments.
Finally, we'll create a new URLSession
and an upload task to brand the request. Upon completion, we'll call the completion handler and we'll pass a Results
object with information regarding the results of the asking, and the failedFiles
array.
let sessionConfiguration = URLSessionConfiguration . default let session = URLSession ( configuration : sessionConfiguration ) let chore = session . uploadTask ( with : request , from : nil , completionHandler : { ( information , response , error ) in completion ( Results ( withData : information , response : Response ( fromURLResponse : response ) , mistake : error ) , failedFilenames ) } ) task . resume ( ) |
The upload method is now ready:
one ii 3 iv 5 6 vii 8 9 ten 11 12 13 fourteen 15 16 17 xviii 19 twenty 21 22 23 24 25 26 27 | func upload ( files : [ FileInfo ] , toURL url : URL , withHttpMethod httpMethod : HttpMethod , completion : @ escaping ( _ result : Results , _ failedFiles : [ String ] ? ) -> Void ) { DispatchQueue . global ( qos : . userInitiated ) . async { [ weak cocky ] in let targetURL = cocky ? . addURLQueryParameters ( toURL : url ) guard let boundary = self ? . createBoundary ( ) else { completion ( Results ( withError : CustomError . failedToCreateBoundary ) , nil ) ; return } self ? . requestHttpHeaders . add together ( value : "multipart/form-information; boundary=\ ( boundary )" , forKey : "content-type" ) guard var body = cocky ? . getHttpBody ( withBoundary : boundary ) else { completion ( Results ( withError : CustomError . failedToCreateHttpBody ) , nil ) ; render } let failedFilenames = cocky ? . add ( files : files , toBody : &body, withBoundary: boundary) self?.close(body: &body, usingBoundary: boundary) guard allow request = self?.prepareRequest(withURL: targetURL, httpBody: body, httpMethod: httpMethod) else { completion(Results(withError: CustomError.failedToCreateRequest), zero); return } let sessionConfiguration = URLSessionConfiguration . default let session = URLSession ( configuration : sessionConfiguration ) permit job = session . uploadTask ( with : request , from : null , completionHandler : { ( data , response , mistake ) in completion ( Results ( withData : information , response : Response ( fromURLResponse : response ) , error : error ) , failedFilenames ) } ) task . resume ( ) } } |
There is one terminal affair to do before we test out everything. To add together the 2 new custom errors to the CustomError
enum. Find it in the RestManager
grade and update it as shown next:
enum CustomError : Fault { case failedToCreateRequest case failedToCreateBoundary case failedToCreateHttpBody } |
Update its extension right below accordingly with the description of the messages:
extension RestManager . CustomError : LocalizedError { public var localizedDescription : String { switch self { case . failedToCreateRequest : return NSLocalizedString ( "Unable to create the URLRequest object" , comment : "" ) case . failedToCreateBoundary : render NSLocalizedString ( "Unable to create boundary string" , annotate : "" ) case . failedToCreateHttpBody : return NSLocalizedString ( "Unable to create HTTP body parameters data" , comment : "" ) } } } |
That'southward it! Time to upload files!
Testing File Uploading
The time to exam file uploading has finally come. Switch to the ViewController.swift
file and add the following method definition:
func uploadSingleFile ( ) { } |
For starters, we are going to upload a single file merely, and here we will prepare the FileInfo
object that will contain its data.
Before we go on, permit me remind yous that in the starter Xcode projection you downloaded there are three files for testing: "sampleText.txt", "samplePDF.txt" and "sampleImage.pdf". We'll utilize the "sampleText.txt" here, just feel costless to change and use any other file you want. Sample files exist in the application'southward parcel only for making the example every bit simple every bit possible, but in real apps the you lot'll almost always fetch them from the documents directory.
So, let's start past creating a FileInfo
object:
func uploadSingleFile ( ) { let fileURL = Parcel . principal . url ( forResource : "sampleText" , withExtension : "txt" ) let fileInfo = RestManager . FileInfo ( withFileURL : fileURL , filename : "sampleText.txt" , name : "uploadedFile" , mimetype : "text/plain" ) } |
See that we are using the custom initializer we created in the FileInfo
structure here. However, in case you don't want to initialize a FileInfo
object that manner and you prefer to manually set all values including the files contents, hither's your alternative:
var fileInfo = RestManager . FileInfo ( ) fileInfo . filename = "sampleText.txt" fileInfo . name = "uploadedFile" fileInfo . mimetype = "text/plain" if permit fileURL = Packet . main . url ( forResource : "sampleText" , withExtension : "txt" ) { fileInfo . fileContents = endeavor ? Data ( contentsOf : fileURL ) } |
Note: Server is implemented in a way that requires the name
aspect in every part of the multipart body to accept the "uploadedFile" value. Therefore, that's the value that we'll be setting in the proper name
property of each FileInfo
object nosotros create hither.
The URL where we'll make the request to upload the file is: http://localhost:3000/upload
. Nosotros volition pass a URL object along with an array that volition contain the fileInfo
object as arguments to a new method (we'll implement information technology right adjacent):
upload ( files : [ fileInfo ] , toURL : URL ( string : "http://localhost:3000/upload" ) ) |
upload(files:toURL:)
is a pocket-size method responsible for making the request as you tin encounter next. We could have put that lawmaking in the uploadSingleFile()
method, but we'll use it again in a while when we'll upload multiple files. So, we'd better avert repeating lawmaking.
1 ii three 4 v half dozen 7 8 9 10 eleven 12 13 xiv 15 16 17 eighteen 19 20 21 22 23 | func upload ( files : [ RestManager . FileInfo ] , toURL url : URL ? ) { if let uploadURL = url { rest . upload ( files : files , toURL : uploadURL , withHttpMethod : . postal service ) { ( results , failedFilesList ) in print ( "HTTP status lawmaking:" , results . response ? . httpStatusCode ? ? 0 ) if permit fault = results . mistake { print ( error ) } if let data = results . data { if allow toDictionary = effort ? JSONSerialization . jsonObject ( with : data , options : . mutableContainers ) { print ( toDictionary ) } } if let failedFiles = failedFilesList { for file in failedFiles { print ( file ) } } } } } |
In the completion handler we don't practice anything particular. We just print the HTTP status lawmaking, we display any potential errors, and the server'south response after nosotros convert it from JSON to a dictionary object. Of class, we also print the list of failed to be uploaded files (in instance there is any).
In the viewDidLoad()
method telephone call the uploadSingleFile()
:
override func viewDidLoad ( ) { super . viewDidLoad ( ) uploadSingleFile ( ) } |
Run the app at present and look at both in Xcode console and in the last where the server's output is printed. If you followed everything step by pace up until hither, you lot should go this in Xcode:
HTTP status code : 200 { result = 1 ; } |
At the same time, in terminal you lot should take the details of the uploaded file:
{ fieldname : 'uploadedFile' , originalname : 'sampleText.txt' , encoding : '7bit' , mimetype : 'text/manifestly' , destination : 'uploads/' , filename : 'sampleText.txt' , path : 'uploads/sampleText.txt' , size : 5575 } |
I wanted to make the small demo server and the file uploading procedure behave equally much naturally every bit possible, then files sent to this server implementation are actually… beingness uploaded! In Finder, become to the Server directory that you downloaded in the starter package and then into the subdirectory chosen "uploads". The uploaded file is there which proves that file uploading is actually working!
Let's make our testing more interesting by also sending boosted data along with the request. Right afterwards the initialization of the FileInfo
object in the uploadSingleFile()
method add the following two lines:
rest . httpBodyParameters . add ( value : "Hello 😀 !!!" , forKey : "greeting" ) rest . httpBodyParameters . add together ( value : "AppCoda" , forKey : "user" ) |
Run the app once again. In the last yous should come across the additional uploaded data also:
{ fieldname : 'uploadedFile' , originalname : 'sampleText.txt' , encoding : '7bit' , mimetype : 'text/plain' , destination : 'uploads/' , filename : 'sampleText.txt' , path : 'uploads/sampleText.txt' , size : 5575 } [ Object : null epitome ] { user : 'AppCoda' , greeting : 'Hello 😀 !!!' } |
Let'due south upload multiple files at present. We'll do that by creating a new method like to the previous one, with the difference being that instead of initializing one FileInfo
object, nosotros'll initialize three of them so we tin upload all sample files we have. Hither information technology is:
func uploadMultipleFiles ( ) { let textFileURL = Packet . chief . url ( forResource : "sampleText" , withExtension : "txt" ) let textFileInfo = RestManager . FileInfo ( withFileURL : textFileURL , filename : "sampleText.txt" , proper name : "uploadedFile" , mimetype : "text/plain" ) let pdfFileURL = Package . main . url ( forResource : "samplePDF" , withExtension : "pdf" ) permit pdfFileInfo = RestManager . FileInfo ( withFileURL : pdfFileURL , filename : "samplePDF.pdf" , proper noun : "uploadedFile" , mimetype : "application/pdf" ) let imageFileURL = Bundle . main . url ( forResource : "sampleImage" , withExtension : "jpg" ) let imageFileInfo = RestManager . FileInfo ( withFileURL : imageFileURL , filename : "sampleImage.jpg" , name : "uploadedFile" , mimetype : "prototype/jpg" ) upload ( files : [ textFileInfo , pdfFileInfo , imageFileInfo ] , toURL : URL ( string : "http://localhost:3000/multiupload" ) ) } |
At the end nosotros phone call over again the upload(files:toURL:)
method which will trigger the actual upload asking. Notice that the upload endpoint is different this fourth dimension ("multiupload"). To see it working, don't forget to phone call it in the viewDidLoad()
:
override func viewDidLoad ( ) { super . viewDidLoad ( ) //uploadSingleFile() uploadMultipleFiles ( ) } |
This time y'all should see the names of the uploaded files in terminal:
Received files : -sampleText . txt -samplePDF . pdf -sampleImage . jpg |
Note that the current server implementation supports up to 10 simultaneous files to be uploaded. Of class y'all are gratuitous to alter that limit according to your preference.
Summary
Starting in the previous tutorial where we created the start version of the RestManager
grade and continuing in this one where nosotros added the file uploading feature, we have managed to build a minor and lightweight course capable of covering our needs in making spider web requests. "Multipart/form-data" content type and the way HTTP torso is built can be sometimes confusing, but if you lot break things down and so everything gets easy. I hope what I shared with you hither today to be of some value, and I wish yous are enjoying RESTful services even more now. You lot are e'er welcome to add more features or adapt the electric current implementation according to your needs. See you lot side by side fourth dimension!
For reference, you can download the total project on GitHub.
Source: https://www.appcoda.com/restful-api-tutorial-how-to-upload-files-to-server/
0 Response to "Upload Files to Webserver Using Rest Api"
Post a Comment