2023-04-22 18:11:21 +01:00
|
|
|
package favicons
|
|
|
|
|
2023-04-22 22:18:39 +01:00
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/sha256"
|
|
|
|
"database/sql"
|
|
|
|
"encoding/hex"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"github.com/mrmelon54/png2ico"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"image/png"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"sync"
|
|
|
|
)
|
|
|
|
|
|
|
|
var ErrFaviconNotFound = errors.New("favicon not found")
|
|
|
|
|
|
|
|
func New(db *sql.DB, inkscapeCmd string) *Favicons {
|
|
|
|
f := &Favicons{
|
|
|
|
db: db,
|
|
|
|
cmd: inkscapeCmd,
|
|
|
|
cLock: &sync.RWMutex{},
|
|
|
|
faviconMap: make(map[string]*FaviconList),
|
|
|
|
}
|
|
|
|
|
|
|
|
// init favicons table
|
|
|
|
_, err := f.db.Exec(`create table if not exists favicons (id integer primary key autoincrement, host varchar, svg varchar, png varchar, ico varchar)`)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("[WARN] Failed to generate 'domains' table\n")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// run compile to get the initial data
|
|
|
|
f.Compile()
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
type Favicons struct {
|
|
|
|
db *sql.DB
|
|
|
|
cmd string
|
|
|
|
cLock *sync.RWMutex
|
|
|
|
faviconMap map[string]*FaviconList
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Favicons) Compile() {
|
|
|
|
go func() {
|
|
|
|
favicons := make(map[string]*FaviconList)
|
|
|
|
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()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Favicons) GetIcons(host string) (*FaviconList, bool) {
|
|
|
|
f.cLock.RLock()
|
|
|
|
defer f.cLock.RUnlock()
|
|
|
|
if a, ok := f.faviconMap[host]; ok {
|
|
|
|
return a, true
|
|
|
|
}
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Favicons) internalCompile(faviconMap map[string]*FaviconList) error {
|
|
|
|
// query all rows in database
|
|
|
|
query, err := f.db.Query(`select * from favicons`)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to prepare query: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
l := &FaviconList{
|
|
|
|
Ico: CreateFaviconImage(rawIco),
|
|
|
|
Png: CreateFaviconImage(rawPng),
|
|
|
|
Svg: CreateFaviconImage(rawSvg),
|
|
|
|
}
|
|
|
|
faviconMap[host] = l
|
|
|
|
g.Go(func() error {
|
|
|
|
return l.PreProcess(f.convertSvgToPng)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return g.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Favicons) convertSvgToPng(in []byte) ([]byte, error) {
|
|
|
|
return svg2png(f.cmd, in)
|
|
|
|
}
|
|
|
|
|
|
|
|
type FaviconList struct {
|
|
|
|
Ico *FaviconImage // can be generated from png with wrapper
|
|
|
|
Png *FaviconImage // can be generated from svg with inkscape
|
|
|
|
Svg *FaviconImage
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *FaviconList) ProduceIco() ([]byte, error) {
|
|
|
|
if l.Ico == nil {
|
|
|
|
return nil, ErrFaviconNotFound
|
|
|
|
}
|
|
|
|
return l.Ico.Raw, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *FaviconList) ProducePng() ([]byte, error) {
|
|
|
|
if l.Png == nil {
|
|
|
|
return nil, ErrFaviconNotFound
|
|
|
|
}
|
|
|
|
return l.Png.Raw, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *FaviconList) ProduceSvg() ([]byte, error) {
|
|
|
|
if l.Svg == nil {
|
|
|
|
return nil, ErrFaviconNotFound
|
|
|
|
}
|
|
|
|
return l.Svg.Raw, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error {
|
|
|
|
var err error
|
|
|
|
|
|
|
|
// SVG
|
|
|
|
if l.Svg != nil {
|
|
|
|
// download SVG
|
|
|
|
l.Svg.Raw, err = getFaviconViaRequest(l.Svg.Url)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("[Favicons] Failed to fetch SVG icon: %w", err)
|
|
|
|
}
|
|
|
|
l.Svg.Hash = hex.EncodeToString(sha256.New().Sum(l.Svg.Raw))
|
|
|
|
}
|
|
|
|
|
|
|
|
// PNG
|
|
|
|
if l.Png != nil {
|
|
|
|
// download PNG
|
|
|
|
l.Png.Raw, err = getFaviconViaRequest(l.Png.Url)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("[Favicons] Failed to fetch PNG icon: %w", err)
|
|
|
|
}
|
|
|
|
} else if l.Svg != nil {
|
|
|
|
// generate PNG from SVG
|
|
|
|
l.Png = &FaviconImage{}
|
|
|
|
l.Png.Raw, err = convert(l.Svg.Raw)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("[Favicons] Failed to generate PNG icon: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ICO
|
|
|
|
if l.Ico != nil {
|
|
|
|
// download ICO
|
|
|
|
l.Ico.Raw, err = getFaviconViaRequest(l.Ico.Url)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("[Favicons] Failed to fetch ICO icon: %w", err)
|
|
|
|
}
|
|
|
|
} else if l.Png != nil {
|
|
|
|
// generate ICO from PNG
|
|
|
|
l.Ico = &FaviconImage{}
|
|
|
|
decode, err := png.Decode(bytes.NewReader(l.Png.Raw))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("[Favicons] Failed to decode PNG icon: %w", err)
|
|
|
|
}
|
|
|
|
b := decode.Bounds()
|
|
|
|
l.Ico.Raw, err = png2ico.ConvertPngToIco(l.Png.Raw, b.Dx(), b.Dy())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("[Favicons] Failed to generate ICO icon: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
l.genSha256()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *FaviconList) genSha256() {
|
|
|
|
if l.Svg != nil {
|
|
|
|
l.Svg.Hash = genSha256(l.Svg.Raw)
|
|
|
|
}
|
|
|
|
if l.Png != nil {
|
|
|
|
l.Png.Hash = genSha256(l.Png.Raw)
|
|
|
|
}
|
|
|
|
if l.Ico != nil {
|
|
|
|
l.Ico.Hash = genSha256(l.Ico.Raw)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getFaviconViaRequest(url string) ([]byte, error) {
|
|
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("[Favicons] Failed to send request '%s': %w", url, err)
|
|
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("[Favicons] Failed to do request '%s': %w", url, err)
|
|
|
|
}
|
|
|
|
rawBody, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("[Favicons] Failed to read response '%s': %w", url, err)
|
|
|
|
}
|
|
|
|
return rawBody, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func genSha256(in []byte) string {
|
|
|
|
h := sha256.New()
|
|
|
|
_, err := h.Write(in)
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
type FaviconImage struct {
|
|
|
|
Url string
|
|
|
|
Hash string
|
|
|
|
Raw []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func CreateFaviconImage(url string) *FaviconImage {
|
|
|
|
if url == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return &FaviconImage{Url: url}
|
|
|
|
}
|