Files
kjv_api/internal/kjv/assets.go
T
2026-10-26 20:30:43 -07:00

114 lines
2.8 KiB
Go

package kjv
import (
"database/sql"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
)
// Everything the program serves or reads besides the Bible itself (pages,
// scripts, styles, the 1828 dictionary, word lists) lives in the assets table
// of the companion database, so the program and the database are the only two
// files that need to be distributed.
var assetMIME = map[string]string{
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".json": "application/json",
".svg": "image/svg+xml",
".dic": "text/plain; charset=utf-8",
".aff": "text/plain; charset=utf-8",
}
func isAssetFile(rel string) bool {
dir := path.Dir(rel)
switch {
case rel == "kjv_fe.html":
return true
case dir == "html", dir == "js", dir == "static":
return true
case dir == "styles":
return strings.HasSuffix(rel, ".css")
case dir == "data":
ext := path.Ext(rel)
return ext == ".json" || ext == ".dic" || ext == ".aff"
}
return false
}
// ImportAssets replaces the assets table with the files of a source checkout.
func ImportAssets(dbPath, srcDir string) error {
db, err := sql.Open("sqlite", "file:"+dbPath+"?_pragma=busy_timeout(5000)")
if err != nil {
return err
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
for _, stmt := range []string{
"CREATE TABLE IF NOT EXISTS assets (path TEXT PRIMARY KEY, mime TEXT NOT NULL, body BLOB NOT NULL)",
"DELETE FROM assets",
} {
if _, err := tx.Exec(stmt); err != nil {
return err
}
}
n := 0
err = filepath.WalkDir(srcDir, func(p string, d fs.DirEntry, err error) error {
if err != nil || !d.Type().IsRegular() { // no directories, and never follow a symlink out of the tree
return err
}
rel, _ := filepath.Rel(srcDir, p)
rel = filepath.ToSlash(rel)
if !isAssetFile(rel) {
return nil
}
body, err := os.ReadFile(p)
if err != nil {
return err
}
mime := assetMIME[path.Ext(rel)]
if mime == "" {
mime = "application/octet-stream"
}
n++
_, err = tx.Exec("INSERT INTO assets (path, mime, body) VALUES (?, ?, ?)", rel, mime, body)
return err
})
if err != nil {
return err
}
if n == 0 {
return fmt.Errorf("no assets found under %s (expected html/, js/, styles/, data/*.json ...)", srcDir)
}
fmt.Printf("imported %d assets into %s\n", n, dbPath)
return tx.Commit()
}
// LoadAssets reads every asset into memory.
func (s *Store) LoadAssets() (map[string][]byte, error) {
rows, err := s.db.Query("SELECT path, body FROM assets")
if err != nil {
return nil, fmt.Errorf("no assets table (run: kjv-api import-assets <source dir>): %w", err)
}
defer rows.Close()
out := map[string][]byte{}
for rows.Next() {
var p string
var b []byte
if err := rows.Scan(&p, &b); err != nil {
return nil, err
}
out[p] = b
}
return out, rows.Err()
}