I recently got asked this question and realized how poorly documented the answer is, and that the solution is scattered across the forum (and will soon be different, since Node 18 is rolling out): “how do you upload a base64 file to the file manager”?
I’ll just dump my experiences as a plugin developer for anyone who stumbles across them.
FYI
- this is advice IMHO; I don’t pretend this is a perfect answer, and it may be incomplete and imperfect (such as my lack-of-knowledge surrounding how to upload private files, as noted below; I’ve never needed to learn how, so I just don’t know)
- this is meant to be a good resource for anyone just starting out with plugin development and needs a quick answer to the question (as to how to get a raw file into the file manager from a plugin action)
- I’m trying to save others the time and energy that took me to learn the information below, not tell others the correct way or best way (again, this is all IMHO)
Basics
- Bubble uses an unprotected file upload endpoint
/fileupload
relative to the website home address (yes, I know, this is awful, I’ve warned them privately and publicly and Bubble just doesn’t care). Since the endpoint is here and here to stay, I choose to utilize it. - To upload a file, you need to POST a json to
website-home-url/fileupload
with the following config:- headers: set content-type to
application/json
- body: an object containing the filename and data (more below)
- headers: set content-type to
- I recommend using Axios (more below) to handle your HTTP request
- server-side actions (make a private plugin for yourself, even, if this is something you need for only your app) with the HTTP dependency (such as Axios) have proved the best for me
- you can take in the website home url dynamically by creating a dynamic text input and just specifying in the documentation/help-text to
please enter the dynamic bubble value 'website home url'
(you can’t enforce this, but it will likely break if you/your plugin’s users don’t follow this - the
website home url
value will correctly adjust to your test environments versus your production environments and still work correctly
- you can take in the website home url dynamically by creating a dynamic text input and just specifying in the documentation/help-text to
Structure of the POST Request
The body of the POST request should be a JSON object (note that, depending on which library you use to make the HTTP request, you may need to first JSON.strinigfy the data [required to use the fetch
API (example Bubble implementation), optional in Axios: POST JSON with Axios - Mastering JS]) with the following structure (in typescript, for type demonstration):
interface BubbleFileUpload {
name: string;
contents: string;
private: false; // as mentioned below, I never use the private option and do not have experience with it, so I always set it to false
}
Example
Let us assume we have:
- a base64 uri for a PDF
- our
website home url
ishttps://test-example.my-bubble-app.co
- we want to use the filename
invoice-12345.pdf
Our server-side plugin action would look like (this is a quick example that I’m pulling from several of my plugins for and is intended as a demonstration; I may have syntax and/or logic errors. Please read through the example and structure your calls similarly, not copy-and-paste. This also leaves numerous room for errors (such as failing to include file extension in filename, if taken in dynamically) so you’ll want to add your own error-handling, as robust as your use-case requires):
async function (properties, context) {
const axios = require("axios");
// assumes we defined a dynamic text field with the `name` option set to `website_home_url`
const home = properties.website_home_url; // in our case, the exact string would be "https://test-example.my-bubble-app.co/", note the trailing `/` would be included automatically if using Bubble's `website home url` dynamic variable
const uploadEndpoint = `${home}fileupload`;
// you'll likely get this from an external API call or take it as input like with `website_home_url`
const filename = "invoice-12345.pdf";
// you'll likely get this from an external API call or take it as input like with `website_home_url`
const base64URI = "data:application/pdf;base64,JVBERi0xLjYK.............TSszDR5NIPSCx";
const uploadableBase64Data = base64URI.split(';base64,').pop(); // we only want everything after the comma, and this removes `data:application/pdf;base64,` as we need
// our BubbleFileUpload interface type from above, the data being POSTed
const payload = {
name: filename,
contents: uploadableBase64Data,
private: false // again, I don't pretend to know enough about this to explain it
}
const result = await axios.post(uploadEndpoint, payload);
const fileURL = result.data;
const httpsFileURL = `https:${fileURL}`; // Bubble will return the url without `https:` and just as `//website.com/...`, so we need to add that back in for most use-cases
// make sure to define your return type to include a `url` key with type `text` in your plugin action
return {
url: httpsFileURL
}
}
You would beed to check off the this action uses node modules
box and have the following:
{
"dependencies": {
"axios": "latest"
}
}
Note that I do not recommend using ‘latest’ for the version, but the Bubble editor often automatically puts this here and overrides whatever manual version you enter; I left ‘latest’ in this example for new developers who notice the Bubble editor is automatically and sometimes overridingly putting ‘latest’ for the version; stable/fixed versions that you test with are always better (you may swap out the latest
for a version like 1.3.6
or 1.4.0
, but like I said, be careful because the editor likes to auto-swap it for whatever reason, unless they’ve fixed this issue when you’re reading this, but they haven’t at time of writing).
Real-World Example
To see a real implementation of the above logic, check out the source code (use the convert svg to png
action in the left, it’s the most concise example) for my Free Barcode & QR code Generator Plugin.
What I’m Leaving Out
My file is not base64, how do I upload it?
Simple: convert it to base64. Bubble only allows base64 file uploads (to my knowledge), so the easiest thing is to convert your Buffer/ArrayBuffer/ReadableStream/etc into a base64 data URI. StackOverflow is your best friend here (although ChatGPT may work for a quick-and-dirty fix, too).
Node 18 Changes
As I mentioned in my intro, part of this post is motivated by that fact that most of the forum posts on this topic use context.async
, which is being deprecated. Instead, Bubble is now moving to allowing async
functions, and @aaronsheldon did a great job documenting the many ways you can use this: Promise Patterns in the Plugin API Version 4. His methods, of course, have direct applications here, as file uploads are all asynchronous operations.
Private File Uploads
I don’t know, care to know, nor use the private file uploads. They serve a purpose, surely; I’ve just never needed them, so I’ve never had to learn how to use them (and write/debug the code for them). I did some quick searching, and it seems like you might need to set the private
to true
and pass in an attach_to
(I’m guessing the Bubble object’s id
property, but do your own research to confirm (here’s a quick start in the right direction: [SOLVED] How to upload an image using POST API - #36 by eric3).
Protected File Upload Endpoints
You can create protected endpoints by using backend workflow actions and setting privacy on who can run that action; basically, you would POST your data to that endpoint, with whatever authentication method you choose, then utilize a file upload. However, I’m very unfamiliar with this method (I only know of it from conversing with @vini_brito and how his PDF Conjurer achieves file uploads [note that this is also a free/open-source plugin, go look at the source code to see how Vine achieved file uploads if you want another example]). Since the /fileupload
endpoint is what Bubble uses for the File Uploader, and it’s clearly here to stay, I myself choose to utilize it as the fastest and easiest way to develop plugin-action file-uploads (this doesn’t mean you have to do the same). My point is simply that even if you go through all of the effort to implement a protected file upload, there is no way (currently, as of writing this) to disable the /fileupload
endpoint; if it’s there anyway, and we have no choice and no way to disable it, perhaps we use it?
Closing Thoughts
You’ll likely want to generate a good, robust, copy-and-pastable version of the above logic (similar to the one I created in my Real-World Example
) to use across your plugins. I hope this gives you a good place to start with your own!