A Cool-ass photo album
Our wedding photographer was a little slow in getting us our images, so I got to thinking about what to do. I decided I wanted a static image gallery, and I wanted it to be easy for anyone who came to get copies of photos they like, whether for online use or making prints. For prints, people should be able to download the high-resolution originals, and those are such big files, it makes sense to zip the files before downloading. For digital use, there should be smaller image files for download[^1], but that could easily be handled entirely client-side.
I decided that, in addition to normal "download this photo" usage, I wanted the ability to
- Select any given subset of the images easily; and
- Download that set of images as a zip file
This is a kinda fun UI problem AND has a fun backend problem despite dealing with static data. Which is great: since we're not barring our photos from anyone, there's no need to implement any auth, which simplifies things.
The zipping part means there needs to be some server code running; I decided to use a rails app hosted on elastic beanstalk. I was already hosting the images on s3, and AWS designs all their services for easy interop (naturally, to keep all your money going their way); besides, I had never hosted an app that way and I wanted to learn.
Admittedly, for the initial version of this gallery, rails was overkill: just within the world of ruby development, Sinatra would be plenty for just an image gallery and a single zipping endpoint. But server-side overkill isn't necessarily bad: as lost as it's fast and I don't mind paying for it (let's be real, my family photos aren't going to get millions of distinct views any day soon), there's no real downside[^2]. Besides:
- I have notions of extending the app with the ability to search and filter by name, and rails makes building out the additional models down the line quite straightforward; and
- I wanted to practice working in and testing rails code for professional reasons
So fuck it, rails it is.
Open zipper?
I searched for "zip" on Ruby Toolbox, and found two projects that seemed to actually be intended for zipping files:
I don't know how a popularity rating is calculated, but it has a Science Beaker icon, so it must be important. Rubyzip, it seems, is the gem for me.
Zip it up.
That repo's README.md
has some intro-type example code:
require 'rubygems'
require 'zip'
folder = "Users/me/Desktop/stuff_to_zip"
input_filenames = ['image.jpg', 'description.txt', 'stats.csv']
zipfile_name = "/Users/me/Desktop/archive.zip"
Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
input_filenames.each do |filename|
# Two arguments:
# - The name of the file as it will appear in the archive
# - The original file, including the path to find it
zipfile.add(filename, folder + '/' + filename)
end
zipfile.get_output_stream("myFile") { |os| os.write "myFile contains just this" }
end
It's not clear from the example alone what files do and don't need to already exist to get this to work. I fiddled around until I got a minimal POC working, which looked something like this:
# In the Gemfile, mind, you need
# gem 'rubyzip'
require 'zip'
folder = "/Users/ambirdsall/Desktop/actual_preexisting_directory"
input_filenames = ['actual_preexisting_file.png']
zipfile_name = "/Users/ambirdsall/Desktop/not_yet_existing_archive_file.zip"
Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
input_filenames.each do |filename|
# Two arguments:
# - The name of the file as it will appear in the archive
# - The original file, including the path to find it
zipfile.add(filename, folder + '/' + filename)
end
zipfile.get_output_stream("new_filename_for_streamed_data.txt") do |os|
os.write "I'm a dynamically-created plain text file"
end
end
To work inside a filesystem like this, rubyzip
needs a full path to the
source files and the zipfile's directory (that all throws a big error if the
path given to zipfile.add
isn't valid); but the zipfile_name
doesn't need to
exist yet.
More significantly, that "new_filename_for_streamed_data"
business implies
that the filesystem can be skipped altogether for data which can be
streamed—from a database, say, or s3.
This is plenty to work with: just get a list of selected images from the UI;
use that list to generate the corresponding s3 URLs; and then stream the
contents of each photo into a zipfile which is then sent to the user's browser
for download. The zipfile.get_output_stream
trick can be used for a friendly
index.txt file down the line, after I've mapped each photo to the names of the
people in it.
Coming Soon...
I'll dive into the design of the UI and of the server code soon, each in its own post.
[^1]:
I whipped up some imagemagick
scripts to do batch resizing and
optimizing, and hosted all the photos as public-read files in an s3 bucket.
[^2]:
Certainly nothing compared to sites that make you download megabytes of javascript before the first paint on mobile.