Compare commits

..

3 Commits

Author SHA1 Message Date
6b61fe1e54 feat(fonts): add font endpoints
All checks were successful
build / Go-Build (push) Successful in 27s
2026-02-12 15:41:23 +01:00
35e873397d Merge pull request 'Add fonts storage and meta parsing' (#11) from feat/fonts into dev
Some checks failed
build / Go-Build (push) Failing after 12s
Reviewed-on: #11
2026-02-12 13:31:52 +00:00
7a9cf7ab93 feat(fonts): add fonts storage
All checks were successful
build / Go-Build (pull_request) Successful in 1m24s
build / Go-Build (push) Successful in 38s
2026-02-12 14:16:37 +01:00
17 changed files with 133 additions and 425 deletions

View File

@@ -1,16 +1,11 @@
# Build stage
FROM golang:latest AS builder FROM golang:latest AS builder
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -v -o svg-templater ./cmd/svg-templater RUN go build -v -o svg-templater ./...
# Final stage
FROM alpine:latest FROM alpine:latest
WORKDIR /root/ WORKDIR /root/
EXPOSE 3000
VOLUME ["/var/lib/svg-templater"]
RUN apk add inkscape fontconfig
COPY --from=builder /app/svg-templater /usr/local/bin/svg-templater COPY --from=builder /app/svg-templater /usr/local/bin/svg-templater
CMD ["svg-templater"] CMD ["svg-templater"]

View File

@@ -1,14 +1,19 @@
package main package main
import ( import (
"log"
"tomatentum.net/svg-templater/internal/command" "tomatentum.net/svg-templater/internal/command"
"tomatentum.net/svg-templater/pkg/format" "tomatentum.net/svg-templater/internal/database"
) )
func main() { func main() {
if !format.CheckInkscape() { if err := database.OpenSQLite(); err != nil {
panic("Inkscape not found") log.Fatal("Failed opening DB:\n", err)
return
} }
defer database.Close()
database.InitDB()
command.PrepareCommandLine() command.PrepareCommandLine()
command.HandleCommandline() command.HandleCommandline()
} }

6
go.mod
View File

@@ -2,15 +2,15 @@ module tomatentum.net/svg-templater
go 1.24.9 go 1.24.9
require github.com/glebarez/go-sqlite v1.22.0
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.22.0
github.com/google/uuid v1.5.0 // indirect github.com/google/uuid v1.5.0 // indirect
github.com/hymkor/exregexp-go v0.2.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/image v0.36.0
golang.org/x/sys v0.15.0 // indirect golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.34.0 // indirect
modernc.org/libc v1.37.6 // indirect modernc.org/libc v1.37.6 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.7.2 // indirect

6
go.sum
View File

@@ -4,15 +4,17 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hymkor/exregexp-go v0.2.0 h1:tyIB8S9gpUwiBi3aDXXXL2yIXqTMWZzW3hs+UNDqcRM=
github.com/hymkor/exregexp-go v0.2.0/go.mod h1:bm661vkJcg9TbcYNr7QOcOgGlr+Jxy1Qb84YjVON3bg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=

View File

@@ -2,9 +2,7 @@ package command
import ( import (
"flag" "flag"
"log"
"tomatentum.net/svg-templater/internal/database"
"tomatentum.net/svg-templater/internal/server" "tomatentum.net/svg-templater/internal/server"
"tomatentum.net/svg-templater/pkg/svg" "tomatentum.net/svg-templater/pkg/svg"
) )
@@ -25,17 +23,6 @@ func PrepareCommandLine() {
func HandleCommandline() { func HandleCommandline() {
flag.Parse() flag.Parse()
if !help {
svg.Storage = svg.NewFileStorage(datapath, "public")
if err := database.OpenSQLite(datapath); err != nil {
log.Fatal("Failed opening DB:\n", err)
return
}
database.InitDB()
}
defer database.Close()
if generateTokenFlag { if generateTokenFlag {
GenerateTokenCommand() GenerateTokenCommand()
} else if deleteTokenFlag { } else if deleteTokenFlag {
@@ -43,6 +30,7 @@ func HandleCommandline() {
} else if help { } else if help {
flag.PrintDefaults() flag.PrintDefaults()
} else { } else {
svg.Storage = svg.NewFileStorage(datapath, "fonts")
server.PrepareHTTP() server.PrepareHTTP()
server.Start() server.Start()
} }

View File

@@ -3,7 +3,6 @@ package database
import ( import (
"database/sql" "database/sql"
"log" "log"
"path/filepath"
_ "github.com/glebarez/go-sqlite" _ "github.com/glebarez/go-sqlite"
) )
@@ -12,8 +11,8 @@ const FILENAME string = "storage.db"
var database *sql.DB var database *sql.DB
func OpenSQLite(basepath string) error { func OpenSQLite() error {
db, err := sql.Open("sqlite", filepath.Join(basepath, FILENAME)) db, err := sql.Open("sqlite", FILENAME)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -13,8 +13,6 @@ const SVGTABLECREATE string = `
);` );`
const INSERTSVGSQL string = "INSERT INTO svg VALUES (?, ?);" const INSERTSVGSQL string = "INSERT INTO svg VALUES (?, ?);"
const GETSPECIFICSVGSQL string = "SELECT * FROM svg WHERE name = ?;"
const GETSVGSQL string = "SELECT * FROM svg;"
func InsertSVG(data *svg.TemplateData) error { func InsertSVG(data *svg.TemplateData) error {
json, err := json.Marshal(data.TemplateKeys) json, err := json.Marshal(data.TemplateKeys)
@@ -28,47 +26,3 @@ func InsertSVG(data *svg.TemplateData) error {
return nil return nil
} }
func GetSVG() ([]svg.TemplateData, error) {
result, err := database.Query(GETSVGSQL)
if err != nil {
return nil, err
}
defer result.Close()
templates := make([]svg.TemplateData, 0)
for result.Next() {
var (
id string
keysjson []byte
keys []string
)
if err := result.Scan(&id, &keysjson); err != nil {
return nil, err
}
if err := json.Unmarshal(keysjson, &keys); err != nil {
return nil, err
}
templates = append(templates, svg.TemplateData{Id: id, TemplateKeys: keys})
}
if err := result.Err(); err != nil {
return nil, err
}
return templates, nil
}
func GetSpecificSVG(id string) (svg.TemplateData, error) {
result := database.QueryRow(GETSPECIFICSVGSQL, id)
var (
keysjson []byte
keys []string
)
if err := result.Scan(&id, &keysjson); err != nil {
return svg.TemplateData{}, err
}
if err := json.Unmarshal(keysjson, &keys); err != nil {
return svg.TemplateData{}, err
}
return svg.TemplateData{Id: id, TemplateKeys: keys}, nil
}

View File

@@ -1,105 +0,0 @@
package routes
import (
"encoding/json"
"log"
"net/http"
"net/url"
"path/filepath"
"strconv"
"tomatentum.net/svg-templater/pkg/format"
"tomatentum.net/svg-templater/pkg/svg/actions"
)
type downloadRequest struct {
TemplateKeys map[string]string
}
type downloadResponse struct {
Url string
}
func DownloadSVG(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
http.Error(w, "Incorrect Content-Type. Needs application/json.", http.StatusUnsupportedMediaType)
return
}
var request downloadRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var err error
id := r.PathValue("id")
if id == "" {
http.Error(w, "No ID provided", http.StatusBadRequest)
return
}
widthString := r.URL.Query().Get("w")
width := 0
if widthString != "" {
width, err = strconv.Atoi(widthString)
if err != nil {
http.Error(w, "Invalid w parameter", http.StatusBadRequest)
return
}
}
heightString := r.URL.Query().Get("h")
height := 0
if heightString != "" {
height, err = strconv.Atoi(heightString)
if err != nil {
http.Error(w, "Invalid h parameter", http.StatusBadRequest)
return
}
}
convParam := format.ConversionParameters{
Format: r.URL.Query().Get("format"),
Width: width,
Height: height,
}
if convParam.Format == "" {
convParam.Format = "svg"
}
templateParam := actions.TemplateParameters{
Id: id,
Keys: request.TemplateKeys,
}
filename, err := actions.ProvideFile(&templateParam, &convParam)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
urlparsed := getPublicUrl(r, filename)
log.Printf("Made %s available as %s\n", id, urlparsed)
response := downloadResponse{
Url: urlparsed,
}
w.Header().Add("Content-Type", "application/json")
json, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(json)
}
func getPublicUrl(r *http.Request, path string) string {
newURL := url.URL{
Host: r.Host,
Path: filepath.Join("public", path),
}
newURL.Scheme = "http"
if r.TLS != nil {
newURL.Scheme = "https"
}
return newURL.String()
}

45
internal/routes/font.go Normal file
View File

@@ -0,0 +1,45 @@
package routes
import (
"encoding/json"
"log"
"net/http"
"strings"
"tomatentum.net/svg-templater/pkg/svg"
)
func AddFont(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
if contentType != "font/ttf" && contentType != "font/otf" {
http.Error(w, "", http.StatusUnsupportedMediaType)
return
}
log.Println("Received font add request")
format := strings.TrimPrefix(contentType, "font/")
if err := svg.Storage.AddFont(r.Body, format); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
}
func GetFonts(w http.ResponseWriter, r *http.Request) {
log.Println("Serving all available fonts")
fonts, err := svg.Storage.GetFonts()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
json, err := json.Marshal(fonts)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Println(err)
return
}
w.Header().Add("Content-Type", "application/json")
w.Write(json)
}

View File

@@ -10,7 +10,7 @@ import (
"tomatentum.net/svg-templater/pkg/svg/actions" "tomatentum.net/svg-templater/pkg/svg/actions"
) )
func CreateSVG(writer http.ResponseWriter, r *http.Request) { func CreateSVG(writer http.ResponseWriter, r http.Request) {
contentType := r.Header.Get("Content-Type") contentType := r.Header.Get("Content-Type")
if contentType != "image/svg+xml" { if contentType != "image/svg+xml" {

View File

@@ -7,29 +7,27 @@ import (
"tomatentum.net/svg-templater/internal/routes" "tomatentum.net/svg-templater/internal/routes"
"tomatentum.net/svg-templater/pkg/auth" "tomatentum.net/svg-templater/pkg/auth"
"tomatentum.net/svg-templater/pkg/svg"
) )
func PrepareHTTP() { func PrepareHTTP() {
registerAuthorizedFunc("/", func(w http.ResponseWriter, r *http.Request) { registerAuthorized("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "You are authorized!") fmt.Fprintln(w, "You are authorized!")
}) })
registerAuthorizedFunc("/svg/", func(w http.ResponseWriter, r *http.Request) { registerAuthorized("/svg/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" { if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
routes.CreateSVG(w, r) routes.CreateSVG(w, *r)
}) })
registerAuthorizedFunc("/svg/{id}", func(w http.ResponseWriter, r *http.Request) { registerAuthorized("/font/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" { switch r.Method {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) case "GET":
return routes.GetFonts(w, r)
case "POST":
routes.AddFont(w, r)
} }
routes.DownloadSVG(w, r)
}) })
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(svg.Storage.GetPublicDir())))
} }
func Start() { func Start() {
@@ -39,11 +37,7 @@ func Start() {
} }
} }
func registerAuthorized(path string, handler http.Handler) { func registerAuthorized(path string, f func(w http.ResponseWriter, r *http.Request)) {
http.HandleFunc(path, auth.AuthMiddleware(handler)) http.HandleFunc(path, auth.AuthMiddleware(http.HandlerFunc(f)))
log.Println("Registered authorized handler for", path) log.Println("Registered authorized handler for", path)
} }
func registerAuthorizedFunc(path string, f func(w http.ResponseWriter, r *http.Request)) {
registerAuthorized(path, http.HandlerFunc(f))
}

View File

@@ -1,98 +0,0 @@
package format
import (
"bytes"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"tomatentum.net/svg-templater/pkg/svg"
)
type ConversionParameters struct {
Format string
Width, Height int
}
func ConvertByte(svgblob []byte, param ConversionParameters) ([]byte, error) {
reader, err := ConvertReader(svgblob, param)
if err != nil {
return nil, err
}
defer reader.Close()
result, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return result, nil
}
func ConvertReader(svgblob []byte, param ConversionParameters) (io.ReadCloser, error) {
if param.Format == "svg" {
return io.NopCloser(bytes.NewReader(svgblob)), nil
}
file, err := svg.CreateTemp(bytes.NewReader(svgblob), "svg")
if err != nil {
return nil, err
}
defer os.Remove(file)
return runCommand(file, param)
}
func runCommand(input string, param ConversionParameters) (io.ReadCloser, error) {
var args []string
var outExt string
switch param.Format {
case "png":
args = append(args, "--export-type=png")
outExt = ".png"
case "jpg":
args = append(args, "--export-type=png") // inkscape doesn't export jpg directly
outExt = ".png"
case "pdf":
args = append(args, "--export-type=pdf")
outExt = ".pdf"
default:
return nil, fmt.Errorf("format not supported: %s", param.Format)
}
if param.Width > 0 {
args = append(args, "-w", strconv.Itoa(param.Width))
}
if param.Height > 0 {
args = append(args, "-h", strconv.Itoa(param.Height))
}
outFile := strings.TrimSuffix(input, filepath.Ext(input)) + outExt
args = append(args, "-o", outFile)
args = append(args, input)
cmd := exec.Command("inkscape", args...)
cmd.Dir = svg.TempDir
out, err := cmd.CombinedOutput()
log.Println(string(out))
if err != nil {
return nil, fmt.Errorf("inkscape failed: %w: %s", err, string(out))
}
file, err := os.Open(outFile)
if err != nil {
return nil, err
}
return file, nil
}
func CheckInkscape() bool {
_, err := exec.LookPath("inkscape")
return err == nil
}

View File

@@ -1,22 +0,0 @@
package actions
import (
"bytes"
"log"
"tomatentum.net/svg-templater/pkg/format"
"tomatentum.net/svg-templater/pkg/svg"
)
func ProvideFile(r *TemplateParameters, conversion *format.ConversionParameters) (string, error) {
templatedSvgblob, err := Template(r)
if err != nil {
return "", err
}
log.Printf("Converting %s to format %s (w=%d,h=%d)", r.Id, conversion.Format, conversion.Width, conversion.Height)
result, err := format.ConvertByte(templatedSvgblob, *conversion)
if err != nil {
return "", err
}
return svg.Storage.CreatePublic(bytes.NewReader(result), conversion.Format)
}

View File

@@ -1,66 +0,0 @@
package actions
import (
"errors"
"io"
"log"
"maps"
"regexp"
"slices"
"github.com/hymkor/exregexp-go"
"tomatentum.net/svg-templater/internal/database"
"tomatentum.net/svg-templater/pkg/svg"
)
type TemplateParameters struct {
Id string
Keys map[string]string
}
func Template(r *TemplateParameters) ([]byte, error) {
mapkeys := slices.Collect(maps.Keys(r.Keys))
ok, err := verifyTemplate(r.Id, mapkeys)
if err != nil {
return nil, err
}
if !ok {
return nil, errors.New("Template does not exist.")
}
log.Printf("Replacing keys of %s template\n", r.Id)
reader, err := svg.Storage.Get(r.Id)
if err != nil {
return nil, err
}
defer reader.Close()
svgblob, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
replaceAll(&svgblob, r.Keys)
log.Printf("Finished replacing keys of %s template\n", r.Id)
return svgblob, nil
}
func replaceAll(svgblob *[]byte, keys map[string]string) {
regex := regexp.MustCompile(svg.KeyRegex)
*svgblob = []byte(exregexp.ReplaceAllStringSubmatchFunc(regex, string(*svgblob), func(s []string) string {
log.Printf("Replacing key %s with %s\n", s[1], keys[s[1]])
return keys[s[1]]
}))
}
func verifyTemplate(id string, keys []string) (bool, error) {
data, err := database.GetSpecificSVG(id)
if err != nil {
return false, err
}
for _, key := range data.TemplateKeys {
if !slices.Contains(keys, key) {
return false, nil
}
}
return true, nil
}

View File

@@ -27,9 +27,9 @@ func Create(svgbuf []byte) (svg.TemplateData, error) {
return data, nil return data, nil
} }
func populateKeys(data *svg.TemplateData, svgblob []byte) { func populateKeys(data *svg.TemplateData, svg []byte) {
regex := regexp.MustCompile(svg.KeyRegex) regex := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`)
result := regex.FindAllSubmatch(svgblob, -1) result := regex.FindAllSubmatch(svg, -1)
templateKeys := make([]string, len(result)) templateKeys := make([]string, len(result))
for i, matches := range result { for i, matches := range result {

View File

@@ -1,36 +1,40 @@
package svg package svg
import ( import (
"errors"
"io" "io"
"log" "log"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"golang.org/x/image/font/sfnt"
) )
type SvgStorage interface { type SvgStorage interface {
Create(id string, svg io.Reader) (string, error) Create(id string, svg io.Reader) (string, error)
Get(id string) (io.ReadCloser, error) Get(id string) (io.Reader, error)
CreatePublic(data io.Reader, filetype string) (string, error) AddFont(reader io.Reader, format string) error
GetPublic(path string) (io.ReadCloser, error) GetFonts() ([]string, error)
GetPublicDir() http.Dir GetFontsDir() (string, error)
} }
var _ SvgStorage = FileSvgStorage{}
var TempDir string = ""
type FileSvgStorage struct { type FileSvgStorage struct {
basepath, publicSubPath string basepath string
fontssubpath string
} }
func NewFileStorage(path, publicSubPath string) *FileSvgStorage { func NewFileStorage(path string, fontssubpath string) *FileSvgStorage {
err := os.MkdirAll(path, 0755) err := os.MkdirAll(path, 0755)
if err != nil { if err != nil {
panic(err) panic(err)
} }
err = os.MkdirAll(filepath.Join(path, fontssubpath), 0755)
if err != nil {
panic(err)
}
log.Println("Initialized file storage handler") log.Println("Initialized file storage handler")
return &FileSvgStorage{path, publicSubPath} return &FileSvgStorage{path, fontssubpath}
} }
func (f FileSvgStorage) Create(id string, svg io.Reader) (string, error) { func (f FileSvgStorage) Create(id string, svg io.Reader) (string, error) {
@@ -50,55 +54,70 @@ func (f FileSvgStorage) Create(id string, svg io.Reader) (string, error) {
return file.Name(), nil return file.Name(), nil
} }
func (f FileSvgStorage) Get(id string) (io.ReadCloser, error) { func (f FileSvgStorage) Get(id string) (io.Reader, error) {
file, err := os.Open(filepath.Join(f.basepath, id+".svg")) file, err := os.Open(filepath.Join(f.basepath, "svg", id+".svg"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return file, nil return file, nil
} }
func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, error) { func (f FileSvgStorage) AddFont(fontreader io.Reader, format string) error {
path := filepath.Join(f.basepath, f.publicSubPath) if format != "ttf" && format != "otf" {
if err := os.MkdirAll(path, 0755); err != nil { return errors.New("Format not supported")
return "", err
} }
file, err := os.CreateTemp(filepath.Join(f.basepath, f.fontssubpath), "*."+format)
file, err := os.CreateTemp(path, "*."+filetype)
if err != nil { if err != nil {
return "", err return err
} }
defer file.Close() defer file.Close()
_, err = io.Copy(file, data) log.Printf("Writing %s font to %s\n", format, file.Name())
_, err = io.Copy(file, fontreader)
if err != nil { if err != nil {
return "", err return err
}
return nil
} }
return strings.TrimPrefix(file.Name(), path), nil func (f FileSvgStorage) GetFonts() ([]string, error) {
} path := filepath.Join(f.basepath, f.fontssubpath)
entries, err := os.ReadDir(path)
func (f FileSvgStorage) GetPublic(path string) (io.ReadCloser, error) {
file, err := os.Open(filepath.Join(f.basepath, f.publicSubPath, path))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return file, nil fonts := make([]string, len(entries))
for i, entry := range entries {
if entry.IsDir() {
continue
}
if strings.HasSuffix(entry.Name(), ".ttf") || strings.HasSuffix(entry.Name(), ".otf") {
fontblob, err := os.ReadFile(filepath.Join(path, entry.Name()))
if err != nil {
return nil, err
}
fontname, err := getFontName(fontblob)
if err != nil {
return nil, err
}
fonts[i] = fontname
}
}
log.Printf("Got fonts \n%s\n", strings.Join(fonts, "\n"))
return fonts, nil
} }
func (f FileSvgStorage) GetPublicDir() http.Dir { func (f FileSvgStorage) GetFontsDir() (string, error) {
return http.Dir(filepath.Join(f.basepath, f.publicSubPath)) return filepath.Join(f.basepath, f.fontssubpath), nil
} }
func CreateTemp(data io.Reader, filetype string) (string, error) { func getFontName(svgblob []byte) (string, error) {
file, err := os.CreateTemp(TempDir, "*."+filetype) font, err := sfnt.Parse(svgblob)
if err != nil { if err != nil {
return "", err return "", err
} }
defer file.Close() name, err := font.Name(nil, sfnt.NameIDFull)
_, err = io.Copy(file, data)
if err != nil { if err != nil {
return "", err return "", err
} }
return name, nil
return file.Name(), nil
} }

View File

@@ -1,7 +1,5 @@
package svg package svg
const KeyRegex string = `\{\{\s*(.*?)\s*\}\}`
type TemplateData struct { type TemplateData struct {
Id string Id string
TemplateKeys []string TemplateKeys []string