Cloud Based dockerized raspberry picture-frame with NextCloud and Balena Cloud

A docker-composed picture frame

There comes the moment in every software developers life when they get on a journey to accomplish something seemingly simple, and once they reached the final destination the result is much more complicated then what they have previously hoped for. This is one of times this has happened to me.

How it feels getting a picture frame running on a Raspberry - via GIPHY

It all started with a very simple idea - creating a picture frame from a raspberry pi 3. The first bit is buying the components, a raspberry, the display, case and power cable. After I put it together, I flashed a Kodi on a SD card, added some 100 pictures to it, started the Kodi screensaver and was happy.

After some weeks the images became repetitive. So I changed them. After some more weeks, the images became repetitive. So I changed them. This became repetitive, so I tried to find a way to change it.

We have an Alexa Dot at home, and if you say “Alexa, show me my pictures” you’d be given a series of random pictures from our Amazon Drive. That’s convenient, I’d want something for my raspberry too, I thought. But here comes the first bummer - to access the amazon drive programmatically you’d need an API key, and those are no longer given out.

But that’s okay, I have my HomeCloud, so all I need is to load the images from there. Right? RIGHT?!

After some research I learned it’s not so easy. NextCloud uses a webdav and exposes an API for the UI. But it needs for every request an authentication token (fair) and a CSRF token. The CSRF token is a token that makes sure that the next request from the website is not from an outside attacker but from the website itself. Great for security, a little more difficult when requesting the API. Also WebDav is not the easiest API if all you want is getting a random image out of a big lake of files independent of folders.

Also there was no endpoint for what I want: Give me all the files. Or rather, all images only. Independent of folders. And then give me a random image after the other. NextCloud comes with a Gallery that has a slideshow, but this slideshow would only show images in the current folder (and previews of subfolders), and allow you to swipe through them. So much for this.

“Oh, but wait…”, the little devil on my shoulder whispered in my ear, “what if the album frontend devs also does not want to develop against webdav, but something easier?”

So I checked the network requests, and indeed - that was more of a RESTful API. Still with CSRF, but with filtering for images, one less complexity there.

`https://${NEXTCLOUD_HOST}/index.php/apps/gallery/files/list?location=${LOCATION}&mediatypes=image%2Fpng%3Bimage%2Fjpeg%3Bimage%2Fgif%3Bimage%2Fx-xbitmap%3Bimage%2Fbmp`

The API Call to the gallery to get all albums in a location. If the location is empty, it would start at the root path.

The API replies with two fields, files with all the files in the current location filtered by the mediatypes, and some additional files of the subfolders to display the previews.

The second field is albums, which contains a hashmap of albums in this location.

I ran some tests in the browser to make sure that this is what I really get, and then looked into how to get a single image. Each document in NextCloud can be identified via it’s nodeid, and this is how this is downloaded.

`https://${NEXTCLOUD_HOST}/index.php/apps/gallery/files/download/${nodeid}`

API call to download a single file from nextcloud

Given the constraints I started to draw out a first idea of a scenario to get all images. Pretty simple, kinda like this:

Given I have a location
When receiving images, persist them if you don't know them yet
Then do the same for all sub-locations

The if you don't know them yet comes out of the requirement that images might be part of the result for a parent folder already.

The next complexity is that I don’t want to store the files on the computer (redundant file-storage is great, but that’s not the way it is done), so I would persist only the nodeid and request it later.

But how to avoid the CSRF? I ssh’ed to the homecloud, opened the sourcecode of the gallery app in my nextcloud instance and searched for the controllers (can be found in apps/gallery/lib/Controller/). Looking into the directories I noticed there are more then webcontrollers, but also API controllers! Neat!

mkainer@nextcloudpi:/var/www/nextcloud/apps/gallery/lib/Controller# ls *Api*
ConfigApiController.php  FilesApiController.php  PreviewApiController.php

Looking at the FilesApiController I knew my issues with CSRF were over. Each function was annotated with

    /**
     * @NoAdminRequired
     * @NoCSRFRequired
     * @CORS

NextCloud is in PHP. This language allows function to get additional behavior via annotations. For PHP those are added in the comments. The @NoCSRFRequired removed the requirement for CSRF. Still it requires authentication, and adds protection for CORS (which is important in the browser).

Trying it out in Postman with basicauth gave me a result. Solved!

So let’s check how we can display it on the raspberry pi, a task for which I want to use WPE. WPE is basically a browser-only operating system build on linux. After reading some articles about how to get it running on an raspberry (which involves cross compiling it via Yocto or Buildroot, which takes a considerable amount of time) I ended up reading an article that basically does exactly what I want, only without nextcloud: Make a webframe with raspberry pi in 30 minutes

That sounded exactly like what I wanted to do! At first was a little held back of the proposal though. The idea was basically to use a WPE docker container, and deploy it on your raspberry over the internet via a service called BalenaCloud. Technology-wise it does sound nice, but a little over-engineered for the simple usecase at hand. I dropped the idea, went back building WPE myself, and some hours later crawled back to balena.

The idea is to create a custom ROM for the raspberry based on BalenaOS, which will then (via VPN) download the docker images from balena and run them on the pi. It worked amazingly well. Following the guide it took me the only some minutes to put the ROM on the SD card, and then deploy the github repo they link to my newly created balena account.

Balena Deployment That’s the result of a balena deployment. If you look closely you might see that I just spoilered you

Okay, so we have a browser in docker on the raspberry now, let’s open the nextcloud gallery. We can inject the URL via environment variables, which will open the login. Step 1 - success.

So how will we be able to display the slideshow? There are multiple options. One would be to create an App for NextCloud or an Application running on the NextCloud Odroid that delivers it as a website, probably with a user and an generated auth token. There were however some things that concerned me there - The first is the loading time of the pictures. While this is over a local network, there are pictures of considerable size coming from my Nikon. I could think about preloading them, but that comes with some complexity. Or add a slideshow library, based on jQuery (which NextCloud is providing). Then there is resilience. If the NextCloud Server is offline or WiFi is unstable the raspberry would be broken.

Another approach would be to have a local server running on the raspberry that runs WPE. This app would get the data from the server (but only the image we need to display), and the browser shows the image from the local drive. If the wifi is broken, it would simply show the same picture for a longer time until the network has recovered. But then this comes with the complexity of getting an executable into the docker container for the WPE.

While trying to decide my eyes caught a very peculiar file: docker-compose.yml. It was inside the project that I cloned for the WPE. A docker compose file is basically a configuration format to combine multiple docker containers together. Taking a peak I saw that it contained another container. And taking another look on the Balena dashboard I saw that actually there were two containers running. Note to self: Check what you deploy! I just put something onto a device in my network that I wasn’t aware of… (its a scheduler to turn off the backlight of the raspberry in case you wonder) Another note to self: Option 2 has become more interesting.

By running multiple containers on one raspberry, I was enabled to not extend the dockercontainer of WPE to contain a server, but add another container to it that would be the server. The raspberry is already becoming a datacenter, why not add another container while we are at it?

I hacked together a simple nodejs app that would statically deliver a html page with nothing then a background image and a meta-refresh tag and checked if I can deploy it on the raspberry. I exposed the port that I used for the nodejs application (9000) as part of the docker-compose file, used localhost:9000 on the WPE and deployed everything.

Balena Cloud All three docker containers running on a raspberry

Worked.

What does it need to display a cat? How many docker images does it need to display a cat?

If you want to use the project feel free to download it from github. Note that to have it running you need to set three environment variables in Balena:

NEXTCLOUD_HOST=<The hostname or ip of your nextcloud, without protocol (i.e. nextcloud.local)>
NEXTCLOUD_USER=<The user to access the nextcloud instance>
NEXTCLOUD_KEY=<The key to your nextcloud instance>

For security reasons you might want to generate an AppKey in NextCloud to access the spreadsheet.

And there we are. To show a slideshow on a raspberry we reverse engineered NextCloud, wrote a node.js application and deployed three docker containers to the raspberry using the internet. While this might not be the quickest way to get a slideshow on your raspberry I certainly find it to be the coolest yet.