Files
flatrender/services/node-agent/internal/runner/fonts.go
T
soroush.asadi 7f2f65dd8a
Build backend images / build content-svc (push) Failing after 53s
Build backend images / build file-svc (push) Failing after 47s
Build backend images / build gateway (push) Failing after 52s
Build backend images / build identity-svc (push) Failing after 58s
Build backend images / build notification-svc (push) Failing after 55s
Build backend images / build render-svc (push) Failing after 59s
Build backend images / build studio-svc (push) Failing after 48s
feat(render+node-agent+admin): install fonts on all render nodes + verify
Push a font once → every node installs it → admin sees per-node status.

- render-svc: font_requests + node_fonts tables (mig 25); admin GET/POST/DELETE
  /v1/node-fonts (with per-node status matrix); internal (HMAC) GET pending +
  POST status for node-agents
- node-agent: fontSyncLoop polls pending fonts every 60s, downloads, installs
  (Windows Fonts dir + registry / macOS / linux fc-cache), reports Installed/Failed
- gateway: /v1/node-fonts/* → render
- admin /admin/node-fonts: upload a .ttf/.otf → install on all nodes; per-node
  Installed/Pending/Failed badges + counts + delete

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 06:33:48 +03:30

93 lines
2.5 KiB
Go

package runner
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
// InstallFont downloads a font file and installs it into the OS so After Effects
// can use it. Returns nil on success. Best-effort per platform.
func InstallFont(ctx context.Context, fileURL, name string) error {
ext := filepath.Ext(strings.Split(fileURL, "?")[0])
if ext == "" {
ext = ".ttf"
}
fileName := sanitizeFontName(name) + ext
tmpDir, err := os.MkdirTemp("", "fr-font-")
if err != nil {
return fmt.Errorf("tmp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
tmp := filepath.Join(tmpDir, fileName)
if _, err := DownloadFile(ctx, fileURL, tmp); err != nil {
return fmt.Errorf("download: %w", err)
}
switch runtime.GOOS {
case "windows":
return installFontWindows(ctx, tmp, fileName)
case "darwin":
return copyFile(tmp, filepath.Join(os.Getenv("HOME"), "Library", "Fonts", fileName))
default: // linux
dir := filepath.Join(os.Getenv("HOME"), ".local", "share", "fonts")
_ = os.MkdirAll(dir, 0o755)
if err := copyFile(tmp, filepath.Join(dir, fileName)); err != nil {
return err
}
_ = exec.CommandContext(ctx, "fc-cache", "-f").Run()
return nil
}
}
func installFontWindows(ctx context.Context, src, fileName string) error {
// Prefer the machine-wide Fonts dir; fall back to the per-user dir.
dst := filepath.Join(os.Getenv("WINDIR"), "Fonts", fileName)
if err := copyFile(src, dst); err != nil {
userFonts := filepath.Join(os.Getenv("LOCALAPPDATA"), "Microsoft", "Windows", "Fonts")
_ = os.MkdirAll(userFonts, 0o755)
dst = filepath.Join(userFonts, fileName)
if err := copyFile(src, dst); err != nil {
return fmt.Errorf("copy font: %w", err)
}
}
// Register so Windows/AE pick it up without a reboot (best-effort; needs admin).
_ = exec.CommandContext(ctx, "reg", "add",
`HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`,
"/v", fileName, "/t", "REG_SZ", "/d", dst, "/f").Run()
return nil
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}
func sanitizeFontName(s string) string {
s = strings.TrimSpace(s)
repl := strings.NewReplacer("/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_", " ", "_")
s = repl.Replace(s)
if s == "" {
return "font"
}
return s
}