Problem
"You're at 80% of your storage quota," Google warned me yet again. With only 17GB total of Drive space, I'd reach 100% in a few short months. What could I do when each photo is around 10MB, and I refuse to let my precious photos be compressed to "Storage Saver" quality? Google of course had a solution: buy one of their Google One plans. For the best deal, pay $100 and get 2TB of storage (and some other features) for the year. However, armed with $150/month of Azure credits from work, I knew I could build a more cost-effective solution and learn more about Azure along the way.
Design
Implementing the more advanced Google Photos features like editing and object recognition isn't feasible. I really just need basic photo storage and management: upload, view, and delete. Using a Storage Account, I can store petabytes of photos and even enable geo-redundant backup. The UI is served through a Flask Web App. Entra facilitates a secure login flow via my Microsoft account. The Web App communicates with the Storage Account using the azure-storage-blob Python library. Furthermore, Storage Accounts allow for time-boxed, read-only, user-delegated SAS keys, allowing me to serve photos directly from the Storage Account without enabling riskier operations.
Performance
I encountered several performance issues that slowed page loads. First, the page was loading the full-size ~10MB images from the Storage Account, which slowed everything down and wasted users' mobile data. To solve this, I added thumbnail generation when the user uploaded a new image. The page would show this much smaller thumbnail, and the user could click on the thumbnail to view in full resolution.
Second, the page attempted to load all images on the page, even if the user hadn't scrolled far enough to view them
yet. This problem is solved by adding loading="lazy" attributes to each thumbnail img. Now,
the page would only load thumbnails when they are close to entering the user's view.
Finally, if the user uploaded just one image or did something else to refresh the page, all images would be requested
again, which takes time and costs bandwidth. The solution here is to add a cache-control header to the
thumbnail and full-size responses. On subsequent requests for the same resource, the browser will serve the image
from memory or disk.
Feedback
After showing my only user (my wife) the app, she requested two important features. First, the ability to create albums to organize photos. This was pretty simple to implement. I used the azure-data-tables library to create a table tracking each album and the photos it contained. I also needed to select one thumbnail from the photos in the album to represent the entire album. Now I could display that album thumbnail on the main page, alongside any photos not assigned to an album.
The second requested feature was to support videos. At first, I was worried about how to stream the video from the Storage Account in chunks instead of needing to wait to download the whole thing. Thankfully, HTML5 video elements and the Storage Account's support for HTTP range requests handle this out of the box. The next challenge was generating thumbnails. FFmpeg is the obvious choice here. It could select the first key frame, resize it, and even add an icon in the top left to indicate it was a video. I ended up having to ship the FFmpeg binaries with my app as trying to install them during deployment often timed out.
Costs
My solution costs about $0.45 per day, or about $162 per year. Although this cost is a small part of my credits budget, it is considerably more expensive than Google One. Over 85% of the cost is the B1 App Service Plan hosting the app. If it weren't necessary for the web app to be constantly active, I could probably switch to the free tier App Service Plan. If I could distribute these costs among more users like Google does, I would be able to compete with Google One's prices.
Future Features
My user has asked for photos to be arranged chronologically with visual cues for the month and year. Photos are already arranged by either their EXIF metadata or upload time, so this should mostly be a UI change. As a stretch goal, my user would like photo locations to be displayed on a map. Although the location itself is likely in the EXIF metadata, I'm not quite sure how to go from there. Personally, I think further leveraging the user-delegated SAS feature to allow sharing photos with others would be neat.
You can find all the project code on GitHub.