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 ): %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() }