In the last part, we left off on uploading a file, then notifying the user with a flash message. Deleting a file will be relatively similar and straight forward, so we’ll handle that next.

One key difference, however, will be that well will need to create a form element for each row in the table. This is because each file will have a different name, or key. Inside of our table for loop, we can add an additional table column.

We will create an empty th tag to line up our column which will only contain buttons. Then, within our table row, we will add a new td element and place a delete form inside. Since we need to pass the key with the post request, we can do this by using a hidden html element. This way, our key will be available to us in the request object.

templates/files.html

...
      <table class="table table-striped">
        <tr>
          <th>Filename</th>
          <th>Last Modified</th>
          <th>Type</th>
          <th></th>
        </tr>
        {% for f in files %}
        <tr>
          <td>{{ f.key }}</td>
          <td>{{ f.last_modified | datetimeformat }}</td>
          <td>{{ f.key | file_type }}</td>
          <td>
            <form class="delete-form" action="{{ url_for('delete') }}" method="POST">
              <input type="hidden" name="key" value="{{ f.key }}">
              <button type="submit" class="btn btn-danger btn-sm">Delete</button>
            </form>
          </td>
        </tr>
        {% endfor %}
      </table>
...

For using buttons in this situation, it would be better to use a simple icon. This is really easy to implement with Font Awesome. Similar to how we’re implementing Bootstrap, we can use a CDN to get this loaded quickly. All we need to do is extend the head block.

templates/files.html

...
{% block head %}
  {{super()}}
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.8/css/solid.css" integrity="sha384-v2Tw72dyUXeU3y4aM2Y0tBJQkGfplr39mxZqlTBDUZAb9BGoC40+rdFCG0m10lXk" crossorigin="anonymous">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.8/css/fontawesome.css" integrity="sha384-q3jl8XQu1OpdLgGFvNRnPdj5VIlCvgsDQTQB6owSOHWlAurxul7f+JpUOVdAiJ5P" crossorigin="anonymous">
{% endblock %}
...

Notice the call to super() in the head block. This is necessary to extend what Flask-Bootstrap already provided us. Otherwise, we would just override everything in this block with our own content. Instead, we want to append to what’s already there.

With Font Awesome installed, we can modify our button element in our table. We will replace the “delete” text with an icon.

<button type="submit" class="btn btn-danger btn-sm">
	<i class="fa fa-trash-alt"></i>
</button>

The Font Awesome stylesheets will load the trash icon based on the element’s class names (fa, fa-trash-alt).

Next, we will return to app.py to add a new delete route along with the functionality to delete the file. We can place this just below the upload function.

...
@app.route('/delete', methods=['POST'])
def delete():
    key = request.form['key']

    s3_resource = boto3.resource('s3')
    my_bucket = s3_resource.Bucket(S3_BUCKET)
    my_bucket.Object(key).delete()

    flash('File deleted successfully')
    return redirect(url_for('files'))


if __name__ == "__main__":
    app.run()

Delete button with icon

The request object will have a form property for us to retrieve the values of a form element. All we need to do is call the delete method on our object. Similar to uploading a file, the user will be redirected to the files page and will be notified that the file was deleted.

Downloading a File

One obvious feature we’re missing is the ability to download any of the files we’ve uploaded. Now would be the perfect time to change that. Let’s begin by adding a download button. This will go in our table right next to the delete button.

template/files.html

...
<td class="td-buttons">
<form class="delete-form" action="{{ url_for('delete') }}" method="POST">
  <input type="hidden" name="key" value="{{ f.key }}">
  <button type="submit" class="btn btn-danger btn-sm">
    <i class="fa fa-trash-alt"></i>
  </button>
</form>
<form class="download-form" action="{{ url_for('download') }}" method="POST">
  <input type="hidden" name="key" value="{{ f.key }}">
  <button type="submit" class="btn btn-default btn-sm">
    <i class="fa fa-download"></i>
  </button>
</form>
</td>
...

Notice, we added the class td-buttons to our td element. We are passing the key as a hidden field again because the will inform us as to which file in the bucket to download. Based on our form action, we will need to create a new download route.

But first, let’s add some styling, otherwise the buttons will be stacked on top of one another instead of from left to right. Since we’ll be adding a custom CSS file, we’ll need to create a new static directory and add a new file styles.css.

mkdir static
touch static/styles.css

static/styles.css

.td-buttons form {
  display: inline-block;
}

The CSS we’re inserting just changes our button elements display property from block to inline-block. This way if there is enough room from left to right in our parent element, then display them without breaking to the next line.

Currently, our files template isn’t aware of our new CSS file. So, we need to add it to the head block:

templates/files.html

...
{% block head %}
  {{super()}}
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.8/css/solid.css" integrity="sha384-v2Tw72dyUXeU3y4aM2Y0tBJQkGfplr39mxZqlTBDUZAb9BGoC40+rdFCG0m10lXk" crossorigin="anonymous">
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.8/css/fontawesome.css" integrity="sha384-q3jl8XQu1OpdLgGFvNRnPdj5VIlCvgsDQTQB6owSOHWlAurxul7f+JpUOVdAiJ5P" crossorigin="anonymous">
  <link rel="stylesheet" href="/static/styles.css">
{% endblock %}
...

Our download button is now created, but there’s no route or function to connect it to. In app.py, we can add a new route. We will restrict this route to post requests just like the upload and delete routes.

from flask import Flask, render_template, request, redirect, url_for, flash, \
    Response

...

@app.route('/download', methods=['POST'])
def download():
    key = request.form['key']

    s3_resource = boto3.resource('s3')
    my_bucket = s3_resource.Bucket(S3_BUCKET)

    file_obj = my_bucket.Object(key).get()

    return Response(
        file_obj['Body'].read(),
        mimetype='text/plain',
        headers={"Content-Disposition": "attachment;filename={}".format(key)}
    )

In order to return a file download, we need to import Response from Flask. Within our download function, we are grabbing our bucket that we previously configured, similar to the other functions.

When we get our S3 object, we are calling the get() method. There is also a download_file() method; however, that will download the file to our local file system. Then we’d have to read the file again from the file system to serve it over http. By using this alternative, we’re eliminating that step.

In our Response object, the read() method will read the file from memory and serve it over http. We also need to set a mimetype so the browser doesn’t think it’s html. By setting it to “text/plain” that should be enough to satisfy our needs. We could improve this by checking the actual mimetype for the file and set it dynamically (i.e. “applicatio/pdf” for pdf files). Finally, we need to add a header to let the browser know that it is a download and the file name of what the user’s downloading it as.

Refreshing the browser should show our new button:

Table with buttons

At this point, if you save and re-run the page, clicking the download buttons should successfully retrieve your files.


Posted in , ,