There are countless services on the internet to share large files. But using someone else’s service is no fun when you can build your own in under 15 mins using nothing more than your OCI tenancy.
In this post I’ll show you the how I did just that with an Object Store bucket and a Lifecycle Policy rule. And for the nerdy nerds in the audience I’ll also take you through the Python script I whipped up to make it even more convenient than using the browser.
Original photo by Esther Bubley, available from the United States Library of Congress‘s Prints and Photographs division under the digital ID fsa.8d30861. Link
First things first let’s get your prerequisites out of the way.
To follow along and build your own you will need:
- an OCI tenancy
- permission to use the Object Store service in that tenancy
- there is no third thing
OCI Object Store
What is OCI Object Store?
OCI Object Store provides scalable, durable, low-cost storage for any type of data. Benefit from 11 nines of durability. Scale storage to nearly unlimited capacity for your unstructured data.
Or (in fewer words) it’s a highly reliable cloud service where you can put files to be stored for later use. If you’re familiar with AWS S3 or Azure Blob Storage you know Object Store.
Object Store buckets can be either Public of Private. Public buckets allow anyone to download objects (i.e. files) stored in the bucket with only their name. Private buckets require you to have authorization to retrieve the files.
We’re going create a Private bucket for safety.
Pre-Authenticated Requests
Pre-authenticated requests (PARs) offer a mechanism by which you can share data stored in object storage with a third party. PARs eliminate the need to access the object storage data using programmatic interfaces, such as the API, SDK, or the CLI.
…
When you create a PAR, a unique PAR URL is generated. Anyone with access to this URL can access the resources identified in the pre-authenticated request. PARs have an expiration date, which determines the length of time the PAR stays active. Once a PAR expires, it can no longer be used.
Source: https://www.oracle.com/cloud/storage/object-storage/faq/#category-preauth
The great thing about a PAR is that you don’t have to have to be an authorized user in the tenant to read it. The danger of a PAR is that anyone with the URL can read the file.
We usually only want files we’re sharing to be available for a short time. So after uploading the file we’re sharing to the bucket we’ll create a PAR that’s valid for only a few days. After that the file will still be available in the bucket, but only someone with the ability to log into to the tenancy and with authorization to download the file can retrieve it. If you send the PAR off to the recipient but they don’t get around to actually downloading it you can always create another PAR without needing to upload the file again.
Lifecycle Policies
We don’t want to keep files around forever – firstly because it’s generally a bad idea[tm], but also because you don’t want to be paying Oracle for files you simply forgot to go back and delete.
If only there was a way to automatically delete them!
Enter Object Lifecycle Policies:
Object lifecycle management lets you manage the lifecycle of your Object Storage data through automated archiving and deletion, reducing storage costs and saving time. Lifecycle management works by creating a set of rules for a bucket (a lifecycle policy) that archive or delete objects depending on their age.
Source: https://www.oracle.com/cloud/storage/object-storage/faq/#category-lifecycle
1 + 1 + 1 = ?
So there’s the broad strokes.
Your initial setup is just 2 steps:
- create a Private Object Store Bucket
- create a Lifecycle Policy to delete anything older than some number of days
When you need to share a file with someone you:
- sign into the console
- which in my case (and hopefully your’s!) means logging in via corporate SSO
- and probably also doing MFA
- navigate to the Bucket
- upload the file
- click through to create a PAR
- picking an appropriate expiration date!
Automation is Better
Using the browser to upload the file and create a PAR using the steps above isn’t at all hard or time consuming. So it’s fine-ish and what I’ve been doing for a few years whenver I needed to share a large file with someone. But it does mean switching from whatever I was doing to a web browser and clicking around a bunch.
Can I make it easier and faster?
Uploading with the CLI
The OCI command line tool can upload files to a bucket. So I can save myself a little time by using that.
And the command line is easy enough:
oci os object put --bucket-name fileshare --file thefile.zip
I also use rclone for a bunch of stuff so I can save a few more key presses by using that:
rclone copy thefile.zip fileshare:
Then I just click a bookmark in my browser that takes me to the bucket and I can finish up creating the PAR.
Creating a PAR with the CLI
We can also use the CLI to create the PAR. It’s not quite as easy as the upload, but it’s close:
$ oci os preauth-request create --help
Usage: oci os preauth-request create [OPTIONS]
Creates a pre-authenticated request specific to the bucket. [Command
Reference](createPreauthenticatedRequest)
Options:
-ns, --namespace-name, --namespace TEXT
The Object Storage namespace used for the
request. If not provided, this parameter
will be obtained internally using a call to
'oci os ns get'
-bn, --bucket-name TEXT The name of the bucket. Avoid entering
confidential information. Example: `my-new-
bucket1` [required]
--name TEXT A user-specified name for the pre-
authenticated request. Names can be helpful
in managing pre-authenticated requests.
Avoid entering confidential information.
[required]
--access-type [ObjectRead|ObjectWrite|ObjectReadWrite|AnyObjectWrite|AnyObjectRead|AnyObjectReadWrite]
The operation that can be performed on this
resource. [required]
--time-expires DATETIME The expiration date for the pre-
authenticated request as per [RFC 3339].
After this date the pre-authenticated
request will no longer be valid. The
following datetime formats are supported:
…
So name, bucket, and access type are required. And since I want it to expire I need to provide that as well.
Meaning I’d do something like
oci os preauth-request create --bucket-name fileshare --access-type ObjectRead --name $RANDOM --object-name thefile.zip --time-expires 2025-10-27
$RANDOM here gives it a random number as the name for the PAR. Which gets me down to 3 arguments to pass in – the bucket name, file name, and an expiration date.
That gets me down to just 2 commands to run from the command line to both upload the file and create the PAR.
Wrapping those two commands in a shell script is easy too – just take the file name and the expiration date as arguments and then call the OCI CLI.
Scripting To The Rescue
Automating with a Shell Script
A bash script to do this is easy as pie:
#/bin/bash
BUCKETNAME=fileshare
usage()
{
echo Invalid command line argument/s
echo Usage: $0 filename expiration
exit
}
if [ -z "$1" ] || [ -z "$2" ]; then
usage
fi
set -x
oci os object put --bucket-name ${BUCKETNAME} --file "$1"
oci os preauth-request create --bucket-name ${BUCKETNAME} --access-type ObjectRead --name $RANDOM --object-name "$1" --time-expires "$2"
Which is pretty simple. And I should just stop there.
What About a Python Script?
A shell script works fine if you are on a Mac or are using Linux. But a Python script works on those plus Windows. And for some reason I always tend to reach for Python when I am automating something. So that’s what I did.
Much of my script is boilerplate – copy/pasted from other scripts or autogenerated by my IDE.
Uploading the file in Python
The one chunk of code that actually required me to actually read a doc was this:
with open(args.file, "rb") as file:
logging.info("Starting upload")
oc.put_object(name_space, args.bucket, file_name, file)
The variable oc is the already initalized instance of the Object Store client, and I’m just calling the put_object() method (relevant doc) and passing in the namespace, bucket name, and the name I want the file called. The 4th argument is the only one that required a little reading on my part.
All of my previous code and handy examples (including the official one) pass in a bunch of bytes for the 4th parameter. My first iteration on the code did the same – I opened the file, read its content (as bytes) into a variable, and passed that variable in as the 4th parameter. Which works fine when you have a small file, but with a large file (100 MB) file you’re reading all those megabytes into memory. And as an added pain point the actual upload was much slower than with the CLI.
So that just won’t do!
Digging in a little deeper and actually reading the docs you can see that the function protoype for put_object says this:
-
put_object(namespace_name, bucket_name, object_name, put_object_body, **kwargs)
And the SDK doc says this:
- put_object_body (stream) – (required) The object to upload to the object store.
The word stream there tells us that we can do something smarter than just handing it a bag of bytes; it tells us that we can pass it a stream instead. In this case where I have a file on disk so all we need to do is call Python’s open() function and pass the stream we get back from that as the 4th parameter.
Creating a PAR with the Python SDK
Once the file is uploaded to the Bucket the last bit is to create the PAR. But we need a date for that.
In the Bash script I forced the user to provide a date. But for Python I wanted to do that automatically. So take “now” and add the number of days the user specifies. Then when you call CreatePreauthenticatedRequestDetails you pass in the file name and that (calculated) expiration date in.
Like so:
expires = datetime.now(timezone.utc)+timedelta(days=args.days)
par_details = CreatePreauthenticatedRequestDetails(
name=args.file,
object_name=args.file,
access_type=CreatePreauthenticatedRequestDetails.ACCESS_TYPE_OBJECT_READ,
time_expires=expires
)
result = oc.create_preauthenticated_request(
name_space,
args.bucket,
par_details
)
Check out my code for yourself
The Bash script is under 20 lines so I pasted it in above. The Python script is more like 100 so check it out in my Github Gist @ https://gist.github.com/therealcmj/11788e16d49e0214bebd2
Python vs Shell Script
Experience is what you get immediately after it would do you any good.
Creating the Python script was useful as a learning experience but looking at both bits of code I think it’s obvious I should have started with the Bash script and just stopped.
Lesson learned.
