Added font synchronizer

Ensures the local public fonts folder is in sync with the Core server fonts. This includes deleting files that are no longer registered.

Issue: 287
Milestone: minor
This commit is contained in:
Brooke Kuhlmann
2026-03-18 09:02:09 -06:00
parent 7f6d8a8990
commit b0b5fc23c6
2 changed files with 149 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# frozen_string_literal: true
require "initable"
require "refinements/pathname"
module Terminus
module Aspects
module Fonts
# Synchronizes TRMNL Framework fonts for local use.
class Synchronizer
include Deps[:settings, "aspects.downloader"]
include Initable[
root_uri: "https://trmnl.com/fonts",
names: %w[
BlockKie.ttf
Inter-Italic.ttf
Inter.ttf
NicoBold-Regular.ttf
NicoClean-Regular.ttf
NicoPups-Regular.ttf
dogicapixel.ttf
dogicapixelbold.ttf
]
]
include Dry::Monads[:result]
using Refinements::Pathname
def call
root = settings.fonts_root.make_dir
delete_unknown_files_in root
names.map { download_to root, it }
end
private
def delete_unknown_files_in root
root.files
.map { it.basename.to_s }
.then { |locals| locals - names }
.each { root.join(it).delete }
end
def download_to root, name
path = root.join name
return Success path if path.exist?
downloader.call("#{root_uri}/#{name}").fmap { |response| path.write response.body }
end
end
end
end
end
@@ -0,0 +1,94 @@
# frozen_string_literal: true
require "hanami_helper"
require "trmnl/api"
RSpec.describe Terminus::Aspects::Fonts::Synchronizer do
using Refinements::Pathname
subject(:synchronizer) { described_class.new downloader: }
include_context "with application dependencies"
let(:downloader) { instance_double Terminus::Aspects::Downloader, call: response }
let :response do
Success(
HTTP::Response.new(
uri: "https://trmnl-oss.s3-us-east-2.amazonaws.com/fonts/test.ttf",
verb: :get,
body: [123].pack("N"),
status: 200,
version: 1.0
)
)
end
describe "#call" do
it "deletes unknown files" do
temp_dir.join("test.txt").touch
synchronizer.call
expect(temp_dir.files).not_to include(temp_dir.join("test.txt"))
end
it "doesn't download files that exist" do
downloader = instance_double Terminus::Aspects::Downloader, call: Failure("Skip.")
synchronizer = described_class.new(downloader:)
temp_dir.join("BlockKie.ttf").touch
synchronizer.call
expect(temp_dir.files).to contain_exactly(temp_dir.join("BlockKie.ttf"))
end
it "answers array of mixed results" do
count = 0
allow(downloader).to receive(:call) do
count += 1
count.even? ? response : Failure("Danger!")
end
expect(synchronizer.call).to eq(
[
Failure("Danger!"),
Success(temp_dir.join("Inter-Italic.ttf")),
Failure("Danger!"),
Success(temp_dir.join("NicoBold-Regular.ttf")),
Failure("Danger!"),
Success(temp_dir.join("NicoPups-Regular.ttf")),
Failure("Danger!"),
Success(temp_dir.join("dogicapixelbold.ttf"))
]
)
end
it "downloads remote files" do
synchronizer.call
expect(temp_dir.files).to eq(
[
temp_dir.join("BlockKie.ttf"),
temp_dir.join("Inter-Italic.ttf"),
temp_dir.join("Inter.ttf"),
temp_dir.join("NicoBold-Regular.ttf"),
temp_dir.join("NicoClean-Regular.ttf"),
temp_dir.join("NicoPups-Regular.ttf"),
temp_dir.join("dogicapixel.ttf"),
temp_dir.join("dogicapixelbold.ttf")
]
)
end
context "with failure" do
subject(:synchronizer) { described_class.new names: ["test.ttf"], downloader: }
let(:response) { Failure "Danger!" }
it "answers message" do
expect(synchronizer.call).to contain_exactly(Failure("Danger!"))
end
end
end
end