My cloud storage is nearly exploding, and I am not in a good position to start subscribing for more storage (Yes, I am still #opentowork). Considering I just moved my domain settings to Cloudflare and started using Cloudflare tunnel, I figure I probably should just back up some of my photos, and host it on my workstation.
I don’t really need a fancy album, just something good enough for my partner and I. So it only needs to fulfil a very small list of requirements:
- Serve everything through FastAPI
- Index photos into JSON(line) files.
- Use React.js as frontend
3 bullets, 3 components, simple enough. Firstly I started by writing a simple script to go through the folders of photos. While jotting down the list of photos into JSONlines, I also generate a thumbnail for each of them.
from PIL import Image
from pillow_heif import register_heif_opener
from thumbnail import generate_thumbnail
def thumbnail_make(path_io: tuple[Path, Path]) -> dict[str, str | bool | int]:
path_input, path_output = path_io
width, height = input_get_size(path_input)
is_portrait = width < height
is_video = False
if path_input.name.split(".")[-1].lower() in ("mp4", "mov"):
is_video = True
generate_thumbnail(
str(path_input),
str(path_output),
options={
"trim": True,
"height": DIM_LONG if is_portrait else DIM_SHORT,
"width": DIM_SHORT if is_portrait else DIM_LONG,
"quality": 100,
"type": "thumbnail",
},
)
else:
image = Image.open(path_input)
image.thumbnail((DIM_SHORT, DIM_LONG) if is_portrait else (DIM_LONG, DIM_SHORT))
image.save(path_output, quality=85)
width_output, height_output = input_get_size(path_output)
return {
"file": path_input.name,
"path": str(path_input),
"is_portrait": is_portrait,
"is_video": is_video,
"width": width,
"height": height,
"width_thumbnail": width_output,
"height_thumbnail": height_output,
"thumbnail": str(path_output),
}
Then the FastAPI application only needs to be able to serve the generated thumbnail and original photos
for album in scandir(Path("./data")):
if album.name == "_meta":
continue
app.mount(
f"/thumbnail/{album.name}",
StaticFiles(directory=Path("./data/_meta") / album.name, html=True),
name=f"photo-{album.name}",
)
app.mount(
f"/photo/{album.name}",
StaticFiles(directory=Path("./data") / album.name, html=True),
name=f"photo-{album.name}",
)
And the API to retrieve albums and photos correspondingly
@app.get("/api/album")
async def album_list() -> list[dict[str, str]]:
return [
{"name": album.name, "url": f"/api/album/{album.name}"}
for album in scandir(Path("./data"))
if album.name != "_meta"
]
@app.get("/api/album/{album_name}")
async def photo_list(album_name: str) -> dict[str, Any]:
result = {"name": album_name, "photos": []}
with open(Path("./data/_meta") / album_name / "index.jsonlines") as index:
for photo in map(json.loads, index):
result["photos"].append(
{
"name": photo["file"],
"photo": f"/photo/{album_name}/{photo['file']}",
"download": f"/download/{album_name}/{photo['file']}",
"thumbnail": f"/thumbnail/{album_name}/{photo['file'].split('.')[0]}.jpg",
"size_thumbnail": (
photo["width_thumbnail"],
photo["height_thumbnail"],
),
"size": (
photo["width"],
photo["height"],
),
"is_portrait": photo["is_portrait"],
"is_video": photo["is_video"],
}
)
return result
Nothing fancy, and tbh, pretty boring stuff. Only notable fixes I did for the frontend react.js application is subclassing StaticFiles
to always load index.html
regardless (something I must have copied from a stackoverflow QA somewhere).
class SPAStaticFiles(StaticFiles):
async def get_response(self, path: str, scope):
try:
return await super().get_response(path, scope)
except (fastapi.HTTPException, StarletteHTTPException) as ex:
if ex.status_code == 404:
return await super().get_response("index.html", scope)
else:
raise ex
Over at frontend, it is similar to how I code my recent projects (legislative data web viewer, and blockedornot). Eventually I discovered an album component, as well as a lightbox component. And after some cleaning up, I have an almost drop-in photo album up (managed by systemd), which took only a weekend to develop (could be faster if I didn’t spend so much time refactoring after getting things up and running).