Using Click and b2sdk Python Libraries to Create a Full-Featured File Upload CLI Tool

February 20, 2024

Do you use Backblaze B2 and want an easy way to upload files from the command line?  Python not only makes this easy but with the Click library, it makes it enjoyable!

Use Case

For me, I store lots of data on B2.  I won't get into the reasons why in this article but just know that I use it quite a bit.  Whenever I wish to post media like pictures, GIFs, or videos to my blog, I choose to upload these files to B2.  I wanted a quick and easy way to upload files with little to no thought yet have them stay organized.

Other use cases could be as simple as backing up local data or storing files in B2 as needed.

Our Script

Environment
  • Python 3.12.2
  • Click 8.1.7
  • b2sdk 1.31.0
Of course, you do not have to match these versions specifically. They're just a guide in case you see something different than what is displayed in this article.

If you're new to Click, I'd suggest following this tutorial first and then tweaking to your needs.

Design

We'll start with the requirements:

  • Must upload a given file to B2
  • Must specify file to upload as an argument

And here are some "nice to haves:"

  • Ability to place into folders based on date (ex. /year/month/slug/file.ext)
  • Return the URL as to where the file is now located

Implementation

First, let's start with the basics.  You already know the drill.  Import everything we need: click, b2sdk, datetime, and os.

import click
from datetime import datetime
import os
from b2sdk.v2 import InMemoryAccountInfo, B2Api

We'll now want to declare some constants.  Keep in mind that you can certainly set these to get values from a secrets manager, an environment file, etc. - totally up to you!

B2_BUCKET = 'Bucket'
B2_KEY = 'Key'
B2_APP_KEY = 'App Key'

You'll need to set those variables to your B2 bucket's name as well as the application key that has access to the bucket.

Warning

Lots of folks try to use the Master Application Key here and that's not a good idea. The Master Application Key has access to ALL of your buckets including the ability to delete them. For this script, you should create a new Application Key that only has access to this one particular bucket.

Now, let's setup our command line arguments with Click.  To start, we'll use the built-in decorator to initiate usage of the library:

@click.command()

Next, we're going to add the positional argument which is the file that we wish to upload.  As you can see, we call click.Path(exists=True) in order to make sure that the given file actually exists.  It makes the code a lot cleaner and less work for us!

@click.argument('file_to_upload', type=click.Path(exists=True))

We then want to add three keyword arguments.  One for the year, month, and the slug.  If you recall back to our design, we were uploading our file to /year/month/slug/file.ext.  We'll also take advantage of the datetime module to obtain the current year and month.  This allows us flexibility to include either the year and/or month argument or...not!  If we do not include them, they'll default to the current year/month.

@click.option('--year', default=datetime.now().year, help='Year of post')
@click.option('--month', default=datetime.now().strftime("%m"), help='Month of post')
@click.option('--slug', help='Slug of post or where to save the media (ex. folder-to-store-files-in)')

Okay, we're half way through!  We're going to put this all in a nice main method to keep things clean.  When the method is called, we'll want to tell the user what values we're using.  Click has another useful method called echo which, as you guessed, echos the string and variable.  In this example, we're also using style to make the keywords bold.

def main(year, month, slug, file_to_upload):
    click.echo(f"{click.style('Year:', bold=True)} {year}, {click.style('Month:', bold=True)} {month}, {click.style('Slug:', bold=True)} {slug}")

Within main, we want to instantiate our B2 client and authorize it.

    info = InMemoryAccountInfo()
    b2_api = B2Api(info)
    b2_api.authorize_account("production", B2_KEY, B2_APP_KEY)
    bucket = b2_api.get_bucket_by_name(B2_BUCKET)

Next, we'll want to grab the source file and determine the destination path.

    original_file_name = os.path.basename(file_to_upload)
    destination_path = f"{year}/{month}/{slug}/{original_file_name}"

To upload the actual file, we'll use the upload_local_file method:

b2_file = bucket.upload_local_file(
        local_file=file_to_upload,
        file_name=destination_path
    )

And, lastly, one of our "nice to haves," was to provide the user with the uploaded file's URL.  Again, we're going to employ echo and style from the Click library.

    click.echo(f"File uploaded to B2: {click.style(f'https://bucket.website.com/{year}/{month}/{slug}/{original_file_name}', fg='bright_blue', bold=True)}")

Now that we're done with the main method, we need to call it:

if __name__ == '__main__':
    main()

Demo

That's it!  Using Click makes the creation and usage of this script easy and enjoyable.  Here's an example output of our script:

> python upload_to_b2.py --year=2024 --month=02 --slug=demo ~/Downloads/demo.png

Year: 2024, Month: 02, Slug: demo
File uploaded to B2: https://bucket.website.com/2024/02/demo/demo.png

 For the full script, you can find it on my GitHub: https://raw.githubusercontent.com/tylwright/Storage/master/Blog/Examples/Python/B2-and-Click/upload_to_b2.py


©2024 Tyler Wright