Skip to content

Media Library Step 2: Coding the Backend (Part 1)

Now move into the src folder, and create a file called .env. This will hold the environment variables that will get loaded by the app.

Tip

Do not add .env to your git repo if you're using one.

Here's what you'll want to put in the file; notice we're using just abc123 for the wallet password; if you used something different, update that line accordingly:

GOLEMDB_PASS="abc123"

Next, create a file in the same directory called main.rs. We'll provide the complete code at the end of this file; here are the steps to create it.

First, let's get all the use lines set up. Go ahead and paste the following in:

use axum::{
    extract::{multipart::Multipart, Path, State},
    http::StatusCode,
    response::{Html, IntoResponse, Json},
    routing::{get, post},
    Router,
};
use golem_base_sdk::{
    entity::{Annotation, Create},
    GolemBaseClient, PrivateKeySigner, Url,
};
use bytes::Bytes;
use golem_base_sdk::Hash;
use alloy_primitives::B256;
use hex::FromHex;
use image::{imageops::FilterType, ImageFormat};
use serde_json::json;
use std::{/*fs,*/ sync::Arc};
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;
use std::io::{Cursor };
use dirs::config_dir;

Next, let's build a couple structures that we can use in the app. The first is a structure called AppState. This is the state that we'll maintain between incoming routes.

The second is a structure we'll be using to send data from a special image function back to the route. Here they both are; add them after the use lines:

struct AppState {
    client: GolemBaseClient,
}

pub struct ImageResult {
    pub id: Hash,
    pub image_data: Vec<u8>, // Using Vec<u8> since Bytes will be created at the end for the response
    pub filename: String,
    pub mimetype: String,
}

Tip

Considering we only have one member in our AppState, client, we could in theory just pass client throughout the app. However, we're adding it as a member to the AppState in case you want to add on to this app later.

Now let's build the main. The main will:

  1. Load the environment variables
  2. Load the wallet and decrypt the key
  3. Load the client uising the key
  4. Initialize the routes

Here's the main:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("A");
    dotenvy::dotenv().ok();

    println!("B");
    let password = std::env::var("GOLEMDB_PASS")?;

    println!("C");
    let keypath = config_dir()
        .ok_or("Failed to get config directory")?
        .join("golembase")
        .join("wallet.json");
    let signer = PrivateKeySigner::decrypt_keystore(keypath, password.trim_end())?;

    let rpc_url_str = "http://localhost:8545";
    let rpc_url = Url::parse(rpc_url_str).unwrap();

    // The GolemBaseClient is now part of our application's shared state.
    // It's wrapped in an Arc for thread-safe access from multiple request handlers.
    println!("Here");
    let shared_state = Arc::new(AppState {
        client: GolemBaseClient::builder()
            .wallet(signer.clone())
            .rpc_url(rpc_url.clone())
            .build(),
    });

    println!(
        "Successfully loaded signer with address: {}",
        signer.address()
    );

    // Set up the Axum router and routes.
    let app = Router::new()
        // The "/" route serves the HTML form, replicating the TS app's front end.
        .route("/", get(serve_html))
        // The "/upload" route handles the image upload.
        .route("/upload", post(upload_handler))
        // The "/thumbnails" route. Note: This handler is a placeholder.
        .route("/thumbnails", get(get_thumbnails))
        // The "/parent/:thumbid" route. Note: This handler is a placeholder.
        .route("/parent/:thumbid", get(get_parent))
        // The "/image/:id" route. Note: This handler is a placeholder.
        .route("/image/:id", get(get_full_image))
        // The "/add-resize/:id" route. Note: This handler is a placeholder.
        .route("/add-resize/:id", post(add_resize))
        // The "/query/:search" route. Note: This handler is a placeholder.
        .route("/query/:search", get(query_entities))
        // We add our state to the router so it's available to all handlers.
        .with_state(shared_state)
        // Add a CORS layer for development to allow cross-origin requests from a frontend.
        .layer(CorsLayer::permissive());

    // Start the server.
    let listener = TcpListener::bind("127.0.0.1:3000").await?;
    println!("listening on http://127.0.0.1:3000");
    axum::serve(listener, app).await?;

    Ok(())
}

Nest, let's create a helper function for converting a Hash that's stored as a string into a B256 type:

fn parse_b256(thumbid: &str) -> B256 {
    let hex_str = thumbid.strip_prefix("0x").unwrap_or(thumbid);
    let bytes: [u8; 32] = <[u8; 32]>::from_hex(hex_str)
        .expect("Invalid hex string for B256");
    B256::from(bytes)
}

Now let's build a helper function that reads an image from GolemBase. It includes the ability to load an image that was broken up into multiple chunks:

async fn get_full_image_data(
    client: &GolemBaseClient,
    id: Hash,
) -> Result<ImageResult, Box<dyn std::error::Error>> {
    let metadata = client.get_entity_metadata(id).await?;

    // --- 1. EXTRACT METADATA ---
    let mut filename = "image".to_string();
    let mut mime_type = "application/octet-stream".to_string();
    let mut part_of: u64 = 1;

    for annot in metadata.string_annotations {
        if annot.key == "filename" {
            filename = annot.value;
        } else if annot.key == "mime-type" {
            mime_type = annot.value;
        }
    }
    for annot in metadata.numeric_annotations {
        if annot.key == "part-of" {
            part_of = annot.value;
        }
    }

    println!("Fetching raw data for {} (MIME: {})", filename, mime_type);

    // --- 2. FETCH FIRST CHUNK (Storage Value) ---
    // The main entity hash contains the first chunk of data.
    //let first_chunk = client.get_storage_value(id).await?.to_vec();
    let first_chunk = client.get_storage_value::<Vec<u8>>(id).await?.to_vec();
    let mut all_chunks = vec![first_chunk];

    // --- 3. FETCH REMAINING CHUNKS IF NECESSARY ---
    if part_of > 1 {
        for i in 2..=(part_of as usize) {
            // Build the query to find the next chunk entity key
            let query = format!(
                "parent=\"{}\" && type=\"image_chunk\" && app=\"golem-images-0.1\" && part={}",
                id, i
            );
            println!("Querying for chunk {}: {}", i, query);

            let chunk_info = client.query_entities(&query).await?;

            if let Some(chunk_entity) = chunk_info.first() {
                // Fetch the storage value (the chunk data) using the found entity key
                //let chunk_data = client.get_storage_value(chunk_entity.entity_key).await?.to_vec();
                let chunk_data = client.get_storage_value::<Vec<u8>>(chunk_entity.key).await?.to_vec();
                all_chunks.push(chunk_data);
            } else {
                eprintln!("Warning: Expected chunk {} not found.", i);
                // In a real app, you might return an error here, but we'll continue.
            }
        }
        println!("Combined {} chunks.", all_chunks.len());
    }

    // --- 4. COMBINE AND RETURN ---
    // Flatten the vector of Vec<u8> chunks into a single Vec<u8>.
    let image_data = all_chunks.into_iter().flatten().collect::<Vec<u8>>();

    Ok(ImageResult {
        id,
        image_data,
        filename,
        mimetype: mime_type,
    })
}

And finally, before we move to the next step, let's include a route that returns some static HTML so you can use and test the app without buildin a separate front end. This is mainly for test purposes:

async fn serve_html() -> Html<&'static str> {
    Html(r#"
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Rust Image Uploader</title>
            <style>
                body { font-family: sans-serif; max-width: 600px; margin: 2em auto; }
                form { display: flex; flex-direction: column; gap: 1em; }
                input, button { padding: 0.5em; }
            </style>
        </head>
        <body>
            <h1>Upload an Image</h1>
            <form action="http://localhost:3000/upload" method="POST" enctype="multipart/form-data">
                <div>
                    <label for="imageFile">Choose image:</label>
                    <input type="file" id="imageFile" name="imageFile" accept="image/*" required />
                </div>
                <div>
                    <label for="filename">Filename (if you want it different from original):</label>
                    <input type="text" id="filename" name="filename" />
                </div>
                <div>
                    <label for="tags">Tags (comma-separated):</label>
                    <input type="text" id="tags" name="tags" value="landscape, nature, sunset" required />
                </div>
                <div for="custom_key1">Optional Custom Tags (Key, Value)</div>
                <div>
                    <input type="text" id="custom_key1" name="custom_key1" value="" />
                    <input type="text" id="custom_value1" name="custom_value1" value="" />
                </div>
                <div>
                    <input type="text" id="custom_key2" name="custom_key2" value="" />
                    <input type="text" id="custom_value2" name="custom_value2" value="" />
                </div>
                <div>
                    <input type="text" id="custom_key3" name="custom_key3" value="" />
                    <input type="text" id="custom_value3" name="custom_value3" value="" />
                </div>
                <button type="submit">Upload</button>
            </form>
        </body>
        </html>
    "#)
}

Next, let's build the routes.

Head to Step 3.