Initiating upload
While creation extension here is not the core protocol, but extension, it is still a good starting point to understand how the client can initiate the upload. The creation extension allows the client to create a new upload resource on the server.
If we follow the tus protocol, the client can initiate the upload by calling POST /api/v3/files, the creation endpoint defined in the previous lesson:
POST /api/v3/files HTTP/1.1
Host: localhost:8080
Content-Length: 0
Upload-Length: 100
Tus-Resumable: 1.0.0
Upload-Metadata: filename d29ybGRfZG9taW5hdGlvbl9wbGFuLnBkZg==,content-type YXBwbGljYXRpb24vcGRm,checksum c2hhMjU2OjEyMzQ1Njc4OTA=
The server should respond with the following:
HTTP/1.1 201 Created
Location: /api/v3/files/24e533e02ec3bc40c387f1a0e460e216
Tus-Resumable: 1.0.0
The Tus-Resumable header is part of the protocol boundary, so we will validate it from the beginning. The following middleware adds the version to every response it handles. It rejects a missing or unsupported client version with 412 Precondition Failed, advertises the supported version in Tus-Version, and does not call the creation handler.
const tusVersion = "1.0.0"
func requireTusVersion(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Tus-Resumable", tusVersion)
if r.Header.Get("Tus-Resumable") != tusVersion {
w.Header().Set("Tus-Version", tusVersion)
http.Error(w, "unsupported tus version", http.StatusPreconditionFailed)
return
}
next.ServeHTTP(w, r)
})
}
This middleware wraps non-OPTIONS tus endpoints. An OPTIONS capability handler is different: the client does not need to send Tus-Resumable, and the server advertises its versions with Tus-Version. We will add that endpoint when we implement capability discovery.
Let’s stop for a moment and think about the request and response. If you ever wondering, why the metadata is sent as a header instead of json body? Don’t you think that it is easier to parse the metadata if it is sent as a json body (especially if you are only familiar with json)? As matter of fact, that’s how Google Cloud Storage implement their initiation upload API. They accept the metadata as a json body. You may refer to the Google Cloud Storage documentation to see how they implement the initiation upload API.
On the other hand, tus protocol allows creation with request body set to empty (described by: Content-Length: 0). In this case, the client is not sending the file content yet, it is only sending the metadata of the file. However, tus protocol also has another extension called creation-with-upload that allows the client to create a new upload resource and upload the first chunk in a single request. If you read more here, client can send the file content in the same request as the initialization request to reduce the number of requests needed to upload the file. In this case, body request will always be used for the file content. That’s why the metadata is sent as a header, unlike GCS implementation.
So nothing is right or wrong here. It is up to you to decide how you want to implement the initiation upload API. In this lesson, we will follow the tus protocol and implement the initiation upload API as described above. But, we will not implement the creation-with-upload extension. I will leave it to you to implement it as an exercise.
Defining controller class
Let’s start the code.
Here, I want to define a controller struct that will host the API handler. Other than this, we will possibly keep other business logic here for simplicity.
type Controller struct {
store Storage
}
func NewController(store Storage) *Controller {
return &Controller{store: store}
}
If you have implemeted your own storage implementation, you can pass it to the controller. The controller will then use the storage to store and get the metadata of the file.
Defining the handler
Let’s start by registering the route for the upload initiation. We will use gorilla/mux to handle the routing.
Now, let’s define the handler for the upload initiation:
func (c *Controller) CreateUpload() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// implement this later
}
}
Registering routes
Once we have the controller, let’s register the route. Passing the Storage implementation into newHTTPHandler keeps this snippet independent of any particular constructor name:
func newHTTPHandler(store Storage) http.Handler {
router := mux.NewRouter()
apiV3Router := router.PathPrefix("/api/v3").Subrouter()
controller := NewController(store)
apiV3Router.
Handle("/files", requireTusVersion(controller.CreateUpload())).
Methods(http.MethodPost)
return router
}
The complete route is now POST /api/v3/files everywhere: the public URL, router prefix, route declaration, and later Location values all share /api/v3/files.
The following lessons fill in CreateUpload with metadata parsing, size handling, persistence, and the generated resource ID. After completing those steps, verify the assembled handler with:
curl --include --request POST 'http://localhost:8080/api/v3/files' \
--header 'Tus-Resumable: 1.0.0' \
--header 'Upload-Length: 100' \
--header 'Content-Length: 0'
The response checkpoint is 201 Created with both headers below:
Location: /api/v3/files/<generated-upload-id>
Tus-Resumable: 1.0.0
You can also change the request header to Tus-Resumable: 0.2.2 and confirm that the middleware returns 412 Precondition Failed with Tus-Version: 1.0.0, without creating an upload.
Now we have the route and protocol-version boundary registered. In the next lesson, let’s run through the remaining creation requirements defined by the tus protocol.