Walkthrough 1: Image Library App
Let's build an image library app together. This app will store images and image thumbnails in golem-base.
You can find the full github repo here: https://github.com/frecklefacelabs/golembase-images
First let's go over the architecture of the app. This will be a full-stack web app using node.js for the backend, with code written in TypeScript, and Angular for the front end, also with TypeScript for the language.
We'll build the backend first. The backend will listen for the following incoming API requests:
-
POST /upload This is where the user will upload an image through a web form. This function will accept an image upload along with a JSON structure containing optional tags (as a single comma-delimited string), and up to three additional key-value pairs describing the image. Because of limits on data size in Golem Base, this function will split the image into up to 100,000 byte chunks if the image is bigger than 100,000 bytes, and save each chunk to the database. It will also generate a 100x100 pixel thumbnail image, which is also saved to the database.
-
GET /thumbnails This will return an array of IDs for the thumbnail images only (not the original larger images).
-
GET /image/:id This will retrieve the image given by the ID. If the image is chunked up, it will first gather the chunks into a single image. The image can be any image, whether original or thumbnail.
-
GET /parent/:id This will return the full image file associated with the given thumbnail image ID.
-
GET /query/:search This will search for images by the given tags. It will return only the thumbnail IDs of the found images. The idea is that the front end will display only the thumbnails for the resulting images.
Let's talk more about how we'll be saving the images.
First, we'll split the tag string into a separate array of individual strings. Then we'll take those and build a set of string annotations to be stored with the image data.
Next, we'll gather the optional three custom tags, and they'll be converted to string annotations as well.
We'll then add on a few more string annotations, including one annotation called "app" that represents this app itself. That way, when you query, on the back end we'll add in this app="app-name" item to the query itself to make sure we only get back entities for this app itself.
We also need to include an annotation denoting what the item is we're storing, whether it's the original image or the thumbnail. For the original we'll provide a key of "type" and a value of "image."
For the original, we'll also save the filename with the key "filename". When a user uploads a file, we can easily obtain the mimetype (such as image/png or image/jpeg). We'll save that with the key "mimetype" (so we can provide it later in the GET /image/:id route).
And finally, we'll provide the chunk number and number of chunks with the keys "part" and "part-of" respectively. (We'll do this even if there's only one chunk; both will be 1 in that case.) Remember, we'll be saving each chunk separately, and each one will get all of the above same key/value pairs, except each subsequent chunk other than the first will get the type "image_chunk". (That doesn't really serve much purpose in this app, but if you're inspecting the data, that way you'll know you're looking at a chunk that isn't the first one.)
Tip: If you expand this app, you can include many more properties about an image, such as EXIF data. And by saving these as annotations, you can easily search them.
Now a note about storage. Due to the way gas works in Ethereum, it's "cheaper" to save the images individually, even though it's possible to do them all in a single transaction. So to save money (even if it's virtual money on a test server), we'll do it that way, one at a time.
Tip: We're not implementing any kind of "transactions" here. If for some reason the app fails to upload one of the chunks, we won't "rollback" the others. You might want to explore how you could do that; simply keep a list of successes, and if there's a failure, push the list up as a single DELETE.
Tip: In the code we've done some IP address hardcoding. We're assuming you're running a local dev instance of golembase-op-geth. As such, we're initializing the client like so:
export const client = await createClient(
1337,
key,
'http://localhost:8545',
'ws://localhost:8545'
)
where key is the read-in key file; regarding that, we're assuming the private.key file is stored locally alongside your backend code:
const keyBytes = readFileSync('./private.key')
const key: AccountData = new Tagged('privatekey', keyBytes)
Tip: Notice how right now we're only defining the backend; that means you'll have a lot of flexibility on the front end. Although we'll be using Angular, you're free to roll your own. You can even test out the backend with Postman!
Now let's get started with the backend in the next step.
Head to Step 1.