2023-04-22 18:11:21 +01:00
|
|
|
package favicons
|
|
|
|
|
2023-04-22 22:18:39 +01:00
|
|
|
import (
|
|
|
|
"database/sql"
|
2023-07-12 16:55:09 +01:00
|
|
|
_ "embed"
|
2023-04-22 22:18:39 +01:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2023-06-20 16:48:04 +01:00
|
|
|
"github.com/MrMelon54/rescheduler"
|
2023-04-22 22:18:39 +01:00
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"log"
|
|
|
|
"sync"
|
|
|
|
)
|
|
|
|
|
|
|
|
var ErrFaviconNotFound = errors.New("favicon not found")
|
|
|
|
|
2023-07-12 16:55:09 +01:00
|
|
|
//go:embed create-table-favicons.sql
|
|
|
|
var createTableFavicons string
|
|
|
|
|
2023-04-24 01:35:23 +01:00
|
|
|
// Favicons is a dynamic favicon generator which supports overwriting favicons
|
|
|
|
type Favicons struct {
|
|
|
|
db *sql.DB
|
|
|
|
cmd string
|
|
|
|
cLock *sync.RWMutex
|
|
|
|
faviconMap map[string]*FaviconList
|
2023-06-20 16:48:04 +01:00
|
|
|
r *rescheduler.Rescheduler
|
2023-04-24 01:35:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new dynamic favicon generator
|
2023-04-22 22:18:39 +01:00
|
|
|
func New(db *sql.DB, inkscapeCmd string) *Favicons {
|
|
|
|
f := &Favicons{
|
|
|
|
db: db,
|
|
|
|
cmd: inkscapeCmd,
|
|
|
|
cLock: &sync.RWMutex{},
|
|
|
|
faviconMap: make(map[string]*FaviconList),
|
|
|
|
}
|
2023-06-20 16:48:04 +01:00
|
|
|
f.r = rescheduler.NewRescheduler(f.threadCompile)
|
2023-04-22 22:18:39 +01:00
|
|
|
|
|
|
|
// init favicons table
|
2023-07-12 16:55:09 +01:00
|
|
|
_, err := f.db.Exec(createTableFavicons)
|
2023-04-22 22:18:39 +01:00
|
|
|
if err != nil {
|
2023-04-24 01:35:23 +01:00
|
|
|
log.Printf("[WARN] Failed to generate 'favicons' table\n")
|
2023-04-22 22:18:39 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// run compile to get the initial data
|
|
|
|
f.Compile()
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
2023-04-24 01:35:23 +01:00
|
|
|
// GetIcons returns the favicon list for the provided host or nil if no
|
|
|
|
// icon is found or generated
|
|
|
|
func (f *Favicons) GetIcons(host string) *FaviconList {
|
|
|
|
// read lock for safety
|
2023-04-22 22:18:39 +01:00
|
|
|
f.cLock.RLock()
|
|
|
|
defer f.cLock.RUnlock()
|
2023-04-24 01:35:23 +01:00
|
|
|
|
|
|
|
// return value from map
|
|
|
|
return f.faviconMap[host]
|
2023-04-22 22:18:39 +01:00
|
|
|
}
|
|
|
|
|
2023-06-20 16:48:04 +01:00
|
|
|
// Compile downloads the list of favicon mappings from the database and loads
|
|
|
|
// them and the target favicons into memory for faster lookups
|
|
|
|
//
|
|
|
|
// This method makes use of the rescheduler instead of just ignoring multiple
|
|
|
|
// calls.
|
|
|
|
func (f *Favicons) Compile() {
|
|
|
|
f.r.Run()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Favicons) threadCompile() {
|
|
|
|
// new map
|
|
|
|
favicons := make(map[string]*FaviconList)
|
|
|
|
|
|
|
|
// compile map and check errors
|
|
|
|
err := f.internalCompile(favicons)
|
|
|
|
if err != nil {
|
|
|
|
// log compile errors
|
|
|
|
log.Printf("[Favicons] Compile failed: %s\n", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// lock while replacing the map
|
|
|
|
f.cLock.Lock()
|
|
|
|
f.faviconMap = favicons
|
|
|
|
f.cLock.Unlock()
|
|
|
|
}
|
|
|
|
|
2023-04-24 01:35:23 +01:00
|
|
|
// internalCompile is a hidden internal method for loading and generating all
|
|
|
|
// favicons.
|
2023-06-20 16:48:04 +01:00
|
|
|
func (f *Favicons) internalCompile(m map[string]*FaviconList) error {
|
2023-04-22 22:18:39 +01:00
|
|
|
// query all rows in database
|
2023-04-24 01:35:23 +01:00
|
|
|
query, err := f.db.Query(`select host, svg, png, ico from favicons`)
|
2023-04-22 22:18:39 +01:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to prepare query: %w", err)
|
|
|
|
}
|
|
|
|
|
2023-04-24 01:35:23 +01:00
|
|
|
// loop over rows and scan in data using error group to catch errors
|
2023-04-22 22:18:39 +01:00
|
|
|
var g errgroup.Group
|
|
|
|
for query.Next() {
|
|
|
|
var host, rawSvg, rawPng, rawIco string
|
|
|
|
err := query.Scan(&host, &rawSvg, &rawPng, &rawIco)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to scan row: %w", err)
|
|
|
|
}
|
|
|
|
|
2023-04-24 01:35:23 +01:00
|
|
|
// create favicon list for this row
|
2023-04-22 22:18:39 +01:00
|
|
|
l := &FaviconList{
|
|
|
|
Ico: CreateFaviconImage(rawIco),
|
|
|
|
Png: CreateFaviconImage(rawPng),
|
|
|
|
Svg: CreateFaviconImage(rawSvg),
|
|
|
|
}
|
2023-04-24 01:35:23 +01:00
|
|
|
|
|
|
|
// save the favicon list to the map
|
2023-06-20 16:48:04 +01:00
|
|
|
m[host] = l
|
2023-04-24 01:35:23 +01:00
|
|
|
|
|
|
|
// run the pre-process in a separate goroutine
|
2023-04-22 22:18:39 +01:00
|
|
|
g.Go(func() error {
|
|
|
|
return l.PreProcess(f.convertSvgToPng)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return g.Wait()
|
|
|
|
}
|
|
|
|
|
2023-04-24 01:35:23 +01:00
|
|
|
// convertSvgToPng calls svg2png which runs inkscape in a subprocess
|
2023-04-22 22:18:39 +01:00
|
|
|
func (f *Favicons) convertSvgToPng(in []byte) ([]byte, error) {
|
|
|
|
return svg2png(f.cmd, in)
|
|
|
|
}
|