Getting upload status
In the previous lessons, we have implemented the API for initiating the upload. On this module, we will be focusing on the core functionalities of the resumable upload system. Detail about the core functionalities can be found on the Tus resumable upload protocol.
The core protocol assumes that the client has initiated the upload and has received the Location
header from the server. Authentication and authorization are also assumed to be handled by the server and out of the scope of the protocol.
In resumable upload, before uploading the chunk, the client should check where the upload has been left off. This is important to ensure that the client continues the upload from the right offset.
Assume that we have a file whose size is 5 MB. When the client has uploaded 2 MB of the file and then the network connection is interrupted, client has no idea where the upload has been left off. The client should ask the server about the upload status and continue the upload from the right offset by moving the file pointer to the right offset. You will see this in the detail later, but for now, let’s focus on how the client can ask the server about the upload status.
API definition
From the previous lesson, we already received the Location
header from the server. The Location
header contains the URL of the created resource and based on the Tus protocol, the client should use the HEAD
method to get the upload status from this url.
Request:
HEAD /api/v3/files/d0c5aa97-4ef7-48db-a3e1-6b07170bf3d5 HTTP/1.1
Host: 127.0.0.1:8080
Tus-Resumable: 1.0.0
Response:
HTTP/1.1 200 OK
Upload-Offset: 70
Tus-Resumable: 1.0.0
The key information we want to retrieve from the response is the Upload-Offset
. The Upload-Offset
is the offset of the file that has been uploaded so far. In this case, the client has uploaded 70 bytes of the file. The client should continue the upload from the 70th byte.
On the other side, we also have defined the HEAD /api/v3/files/{id}
endpoint to retrieve the metadata of the file by ID.
Requirements
From the Tus resumable upload protocol, the server should meet the following requirements for the HEAD
request:
- The Server MUST always include the
Upload-Offset
header in the response for a HEAD request, even if the offset is 0, or the upload is already considered completed. - If the size of the upload is known, the Server MUST include the Upload-Length header in the response.
- If the resource is not found, the Server SHOULD return either the 404 Not Found, 410 Gone or 403 Forbidden status without the Upload-Offset header.
- The Server SHOULD acknowledge successful HEAD requests with a 200 OK or 204 No Content status.
- The Server MUST prevent the client and/or proxies from caching the response by adding the Cache-Control: no-store header to the response.
It is straight forward, right? Let’s implement it.
Implementation
Implementing the handler
Registering the route
Let’s start by adding the route to the server handler for the upload status:
apiV3Router.HandleFunc("/files/{file_id}", controller.GetOffset()).Methods(http.MethodHead)
We now define the handler for the HEAD
method. Since the file ID is part of the URL, we can get the file ID from the URL path. We can use mux.Vars(r)
to get the URL parameters.
func (c *Controller) GetOffset() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
fileID := vars["file_id"]
// ... the rest of the code
}
}
Not found case
Now, lets implement the 3rd requirement
If the resource is not found, the Server SHOULD return either the 404 Not Found, 410 Gone or 403 Forbidden status without the Upload-Offset header.
This is straighforward, we can use the store.Find
method to find the file metadata. If the file is not found, we should return 404 Not Found
.
func (c *Controller) GetOffset() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ... the rest of the code
fm, ok, err := c.store.Find(fileID)
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
// ... the rest of the code
}
}
While the specification says that we can use 404
, 410
, or 403
, on the implementation above, we only need to return 404 Not Found
if the file is not found. At this point, we don’t need to return 410 Gone
since this will be fit better if we already implement the expiration of the upload. If the upload time has passed a certain time, we can consider the upload as Gone
hence use 410 Gone
.
Upload length
Now, lets implement the 2nd requirement:
If the size of the upload is known, the Server MUST include the Upload-Length header in the response.
func (c *Controller) GetOffset() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ... the rest of the code
if !fm.IsDeferLength {
w.Header().Add(UploadLengthHeader, fmt.Sprint(fm.TotalSize))
}
// ... the rest of the code
}
}
Since we have IsDeferLength
field in the File
struct, we can use this to determine whether the client has deferred the length or not. If the client has deferred the length, we should not include the Upload-Length
header in the response. Otherwise, we should include the Upload-Length
header in the response.
Upload offset
Now, lets implement the 1st requirement:
The Server MUST always include the
Upload-Offset
header in the response for a HEAD request, even if the offset is 0, or the upload is already considered completed.
func (c *Controller) GetOffset() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ... the rest of the code
w.Header().Add(UploadOffsetHeader, fmt.Sprint(fm.UploadedSize))
// ... the rest of the code
}
}
It is quite straighforward, isn’t it? We just need to add the Upload-Offset
header to the response as it is. The value of the Upload-Offset
header is the UploadedSize
of the file metadata.
Cache-Control
Since offset can change over time quickly as the client uploads the file, we should prevent the client and/or proxies from caching the response. We can add the Cache-Control: no-store
header to the response.
The Server MUST prevent the client and/or proxies from caching the response by adding the Cache-Control: no-store header to the response.
To prevent caching, we can add the Cache-Control: no-store
header to the response.
func (c *Controller) GetOffset() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ... the rest of the code
w.Header().Add("Cache-Control", "no-store")
// ... the rest of the code
}
}
Returning the response
Finally, we should return the response with the status code 200 OK
or 204 No Content
.
The Server SHOULD acknowledge successful HEAD requests with a 200 OK or 204 No Content status.
func (c *Controller) GetOffset() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ... the rest of the code
w.WriteHeader(http.StatusNoContent)
}
}
While the specification says that we can use 200 OK
or 204 No Content
, on the implementation above, I only return 204 No Content
since there is no body needs to be returned. You may also use 200 OK
even if there is no body to be returned, but I prefer to use 204 No Content
to indicate that there is no content to be returned.
Conclusion
Implementation of the HEAD
request is quite straightforward. We just need to return the Upload-Offset
and Upload-Length
headers in the response. We also need to prevent the client and/or proxies from caching the response by adding the Cache-Control: no-store
header to the response.
At the end, your GetOffset
handler should look like this:
func (c *Controller) GetOffset() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
fileID := vars["file_id"]
fm, ok, err := c.store.Find(fileID)
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
w.Header().Add(UploadOffsetHeader, fmt.Sprint(fm.UploadedSize))
if !fm.IsDeferLength {
w.Header().Add(UploadLengthHeader, fmt.Sprint(fm.TotalSize))
}
w.Header().Add("Cache-Control", "no-store")
w.WriteHeader(http.StatusNoContent)
}
}
In the next lesson, we will implement the PATCH
request to upload the file chunk by chunk. Stay tuned!