From 7c6d8f420671b08a54a0ce8ae865a914b1105775 Mon Sep 17 00:00:00 2001 From: Tueem Date: Wed, 4 Feb 2026 12:53:00 +0100 Subject: [PATCH 01/13] feat(template): add public storage and temp file method --- pkg/svg/storage.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pkg/svg/storage.go b/pkg/svg/storage.go index 0531469..b3349f7 100644 --- a/pkg/svg/storage.go +++ b/pkg/svg/storage.go @@ -10,12 +10,46 @@ import ( type SvgStorage interface { Create(id string, svg io.Reader) (string, error) Get(id string) (io.Reader, error) + CreateTemp(data io.Reader, filetype string) (string, error) + CreatePublic(data io.Reader, filetype string) (string, error) } +var _ SvgStorage = FileSvgStorage{} + type FileSvgStorage struct { basepath string } +func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, error) { + path := filepath.Join(f.basepath, "public") + if err := os.Mkdir(path, 0755); err != nil { + return "", err + } + + file, err := os.CreateTemp(path, "*."+filetype) + if err != nil { + return "", err + } + defer file.Close() + + return file.Name(), nil +} + +func (f FileSvgStorage) CreateTemp(data io.Reader, filetype string) (string, error) { + path := filepath.Join(f.basepath, "temp") + if err := os.Mkdir(path, 0755); err != nil { + return "", err + } + + file, err := os.CreateTemp(path, "*."+filetype) + if err != nil { + return "", err + } + defer file.Close() + + return file.Name(), nil +} + func NewFileStorage(path string) *FileSvgStorage { err := os.MkdirAll(path, 0755) if err != nil { -- 2.49.1 From 741b1964975769d993739633360b223b09732381 Mon Sep 17 00:00:00 2001 From: Tueem Date: Sun, 8 Feb 2026 19:42:28 +0100 Subject: [PATCH 02/13] feat(formatconverter): add formatconverter method using inkscape --- cmd/svg-templater/main.go | 4 ++ pkg/format/formatconverter.go | 97 +++++++++++++++++++++++++++++++++++ pkg/svg/storage.go | 76 +++++++++++++++------------ 3 files changed, 143 insertions(+), 34 deletions(-) create mode 100644 pkg/format/formatconverter.go diff --git a/cmd/svg-templater/main.go b/cmd/svg-templater/main.go index b6de69e..0eec781 100644 --- a/cmd/svg-templater/main.go +++ b/cmd/svg-templater/main.go @@ -5,9 +5,13 @@ import ( "tomatentum.net/svg-templater/internal/command" "tomatentum.net/svg-templater/internal/database" + "tomatentum.net/svg-templater/pkg/format" ) func main() { + if !format.CheckInkscape() { + panic("Inkscape not found") + } if err := database.OpenSQLite(); err != nil { log.Fatal("Failed opening DB:\n", err) return diff --git a/pkg/format/formatconverter.go b/pkg/format/formatconverter.go new file mode 100644 index 0000000..d497b8e --- /dev/null +++ b/pkg/format/formatconverter.go @@ -0,0 +1,97 @@ +package format + +import ( + "bytes" + "fmt" + "io" + "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("/opt/homebrew/bin/inkscape", args...) + cmd.Dir = svg.TempDir + + out, err := cmd.CombinedOutput() + + 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 +} diff --git a/pkg/svg/storage.go b/pkg/svg/storage.go index b3349f7..45d3e7b 100644 --- a/pkg/svg/storage.go +++ b/pkg/svg/storage.go @@ -9,47 +9,18 @@ import ( type SvgStorage interface { Create(id string, svg io.Reader) (string, error) - Get(id string) (io.Reader, error) - CreateTemp(data io.Reader, filetype string) (string, error) + Get(id string) (io.ReadCloser, error) CreatePublic(data io.Reader, filetype string) (string, error) + GetPublic(path string) (io.ReadCloser, error) } var _ SvgStorage = FileSvgStorage{} +var TempDir string = "" type FileSvgStorage struct { basepath string } -func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, error) { - path := filepath.Join(f.basepath, "public") - if err := os.Mkdir(path, 0755); err != nil { - return "", err - } - - file, err := os.CreateTemp(path, "*."+filetype) - if err != nil { - return "", err - } - defer file.Close() - - return file.Name(), nil -} - -func (f FileSvgStorage) CreateTemp(data io.Reader, filetype string) (string, error) { - path := filepath.Join(f.basepath, "temp") - if err := os.Mkdir(path, 0755); err != nil { - return "", err - } - - file, err := os.CreateTemp(path, "*."+filetype) - if err != nil { - return "", err - } - defer file.Close() - - return file.Name(), nil -} - func NewFileStorage(path string) *FileSvgStorage { err := os.MkdirAll(path, 0755) if err != nil { @@ -76,10 +47,47 @@ func (f FileSvgStorage) Create(id string, svg io.Reader) (string, error) { return file.Name(), nil } -func (f FileSvgStorage) Get(id string) (io.Reader, error) { - file, err := os.Open(filepath.Join(f.basepath, "svg", id+".svg")) +func (f FileSvgStorage) Get(id string) (io.ReadCloser, error) { + file, err := os.Open(filepath.Join(f.basepath, id+".svg")) if err != nil { return nil, err } return file, nil } + +func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, error) { + path := filepath.Join(f.basepath, "public") + if err := os.Mkdir(path, 0755); err != nil { + return "", err + } + + file, err := os.CreateTemp(path, "*."+filetype) + if err != nil { + return "", err + } + defer file.Close() + + return file.Name(), nil +} + +func (f FileSvgStorage) GetPublic(path string) (io.ReadCloser, error) { + file, err := os.Open(filepath.Join(f.basepath, "public", path)) + if err != nil { + return nil, err + } + return file, nil +} + +func CreateTemp(data io.Reader, filetype string) (string, error) { + file, err := os.CreateTemp(TempDir, "*."+filetype) + if err != nil { + return "", err + } + defer file.Close() + _, err = io.Copy(file, data) + if err != nil { + return "", err + } + + return file.Name(), nil +} -- 2.49.1 From 1031ad7e8c18facd6427b30bb60e5b1449d4ada0 Mon Sep 17 00:00:00 2001 From: Tueem Date: Sun, 8 Feb 2026 22:46:58 +0100 Subject: [PATCH 03/13] feat(template): add Fileserver for download --- internal/command/commandline.go | 2 +- internal/server/http.go | 20 ++++++++++++++++---- pkg/svg/storage.go | 16 +++++++++++----- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/internal/command/commandline.go b/internal/command/commandline.go index 32b528b..768256d 100644 --- a/internal/command/commandline.go +++ b/internal/command/commandline.go @@ -30,7 +30,7 @@ func HandleCommandline() { } else if help { flag.PrintDefaults() } else { - svg.Storage = svg.NewFileStorage(datapath) + svg.Storage = svg.NewFileStorage(datapath, "public") server.PrepareHTTP() server.Start() } diff --git a/internal/server/http.go b/internal/server/http.go index ab2c96e..e840cb3 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -4,22 +4,26 @@ import ( "fmt" "log" "net/http" + "path/filepath" "tomatentum.net/svg-templater/internal/routes" "tomatentum.net/svg-templater/pkg/auth" + "tomatentum.net/svg-templater/pkg/svg" ) func PrepareHTTP() { - registerAuthorized("/", func(w http.ResponseWriter, r *http.Request) { + registerAuthorizedFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "You are authorized!") }) - registerAuthorized("/svg/", func(w http.ResponseWriter, r *http.Request) { + registerAuthorizedFunc("/svg/", func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } routes.CreateSVG(w, *r) }) + + registerAuthorized("/public/", http.StripPrefix("/public/", http.FileServer(svg.Storage.GetPublicDir()))) } func Start() { @@ -29,7 +33,15 @@ func Start() { } } -func registerAuthorized(path string, f func(w http.ResponseWriter, r *http.Request)) { - http.HandleFunc(path, auth.AuthMiddleware(http.HandlerFunc(f))) +func registerAuthorized(path string, handler http.Handler) { + http.HandleFunc(path, auth.AuthMiddleware(handler)) log.Println("Registered authorized handler for", path) } + +func registerAuthorizedFunc(path string, f func(w http.ResponseWriter, r *http.Request)) { + registerAuthorized(path, http.HandlerFunc(f)) +} + +func GetPublicPath(path string) string { + return filepath.Join("public", path) +} diff --git a/pkg/svg/storage.go b/pkg/svg/storage.go index 45d3e7b..343106b 100644 --- a/pkg/svg/storage.go +++ b/pkg/svg/storage.go @@ -3,6 +3,7 @@ package svg import ( "io" "log" + "net/http" "os" "path/filepath" ) @@ -12,22 +13,23 @@ type SvgStorage interface { Get(id string) (io.ReadCloser, error) CreatePublic(data io.Reader, filetype string) (string, error) GetPublic(path string) (io.ReadCloser, error) + GetPublicDir() http.Dir } var _ SvgStorage = FileSvgStorage{} var TempDir string = "" type FileSvgStorage struct { - basepath string + basepath, publicSubPath string } -func NewFileStorage(path string) *FileSvgStorage { +func NewFileStorage(path, publicSubPath string) *FileSvgStorage { err := os.MkdirAll(path, 0755) if err != nil { panic(err) } log.Println("Initialized file storage handler") - return &FileSvgStorage{path} + return &FileSvgStorage{path, publicSubPath} } func (f FileSvgStorage) Create(id string, svg io.Reader) (string, error) { @@ -56,7 +58,7 @@ func (f FileSvgStorage) Get(id string) (io.ReadCloser, error) { } func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, error) { - path := filepath.Join(f.basepath, "public") + path := filepath.Join(f.basepath, f.publicSubPath) if err := os.Mkdir(path, 0755); err != nil { return "", err } @@ -71,13 +73,17 @@ func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, e } func (f FileSvgStorage) GetPublic(path string) (io.ReadCloser, error) { - file, err := os.Open(filepath.Join(f.basepath, "public", path)) + file, err := os.Open(filepath.Join(f.basepath, f.publicSubPath, path)) if err != nil { return nil, err } return file, nil } +func (f FileSvgStorage) GetPublicDir() http.Dir { + return http.Dir(filepath.Join(f.basepath, f.publicSubPath)) +} + func CreateTemp(data io.Reader, filetype string) (string, error) { file, err := os.CreateTemp(TempDir, "*."+filetype) if err != nil { -- 2.49.1 From 541caffd65267ce27795156b84a288d52de1ae13 Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 08:18:00 +0100 Subject: [PATCH 04/13] feat(template): add DB Get functions --- internal/database/svgdb.go | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/internal/database/svgdb.go b/internal/database/svgdb.go index d91f2ee..ef4f941 100644 --- a/internal/database/svgdb.go +++ b/internal/database/svgdb.go @@ -13,6 +13,8 @@ const SVGTABLECREATE string = ` );` const INSERTSVGSQL string = "INSERT INTO svg VALUES (?, ?);" +const GETSPECIFICSVGSQL string = "SELECT * FROM svg WHERE id = ?;" +const GETSVGSQL string = "SELECT * FROM svg;" func InsertSVG(data *svg.TemplateData) error { json, err := json.Marshal(data.TemplateKeys) @@ -26,3 +28,47 @@ func InsertSVG(data *svg.TemplateData) error { 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 +} -- 2.49.1 From 823f5f091a97d1a6aca3ae3581d80d63e50eda02 Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 08:18:44 +0100 Subject: [PATCH 05/13] fix(storage): fix return value of CreatePublic to return the trimmed file name --- pkg/svg/storage.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/svg/storage.go b/pkg/svg/storage.go index 343106b..fef8cd4 100644 --- a/pkg/svg/storage.go +++ b/pkg/svg/storage.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path/filepath" + "strings" ) type SvgStorage interface { @@ -69,7 +70,7 @@ func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, e } defer file.Close() - return file.Name(), nil + return strings.TrimPrefix(file.Name(), path), nil } func (f FileSvgStorage) GetPublic(path string) (io.ReadCloser, error) { -- 2.49.1 From 6cf349bafaa34a246a43532d0bb037998c466a80 Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 08:19:30 +0100 Subject: [PATCH 06/13] refactor(upload): move Regex to variable --- pkg/svg/actions/upload.go | 6 +++--- pkg/svg/templates.go | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/svg/actions/upload.go b/pkg/svg/actions/upload.go index a6d26a4..ea7577c 100644 --- a/pkg/svg/actions/upload.go +++ b/pkg/svg/actions/upload.go @@ -27,9 +27,9 @@ func Create(svgbuf []byte) (svg.TemplateData, error) { return data, nil } -func populateKeys(data *svg.TemplateData, svg []byte) { - regex := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`) - result := regex.FindAllSubmatch(svg, -1) +func populateKeys(data *svg.TemplateData, svgblob []byte) { + regex := regexp.MustCompile(svg.KeyRegex) + result := regex.FindAllSubmatch(svgblob, -1) templateKeys := make([]string, len(result)) for i, matches := range result { diff --git a/pkg/svg/templates.go b/pkg/svg/templates.go index 398396e..6061f01 100644 --- a/pkg/svg/templates.go +++ b/pkg/svg/templates.go @@ -1,5 +1,7 @@ package svg +const KeyRegex string = `\{\{\s*(.*?)\s*\}\}` + type TemplateData struct { Id string TemplateKeys []string -- 2.49.1 From ba90d5a0ed2aa284f0a827ddf56628aadf16b236 Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 09:56:03 +0100 Subject: [PATCH 07/13] fix(db): incorrect columen name --- internal/database/svgdb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/database/svgdb.go b/internal/database/svgdb.go index ef4f941..97d03df 100644 --- a/internal/database/svgdb.go +++ b/internal/database/svgdb.go @@ -13,7 +13,7 @@ const SVGTABLECREATE string = ` );` const INSERTSVGSQL string = "INSERT INTO svg VALUES (?, ?);" -const GETSPECIFICSVGSQL string = "SELECT * FROM svg WHERE id = ?;" +const GETSPECIFICSVGSQL string = "SELECT * FROM svg WHERE name = ?;" const GETSVGSQL string = "SELECT * FROM svg;" func InsertSVG(data *svg.TemplateData) error { -- 2.49.1 From 343abe9bb540e5ffcd7c9fe37469217874e2fba8 Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 09:59:03 +0100 Subject: [PATCH 08/13] fix(storage): add missing io.Copy --- pkg/svg/storage.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/svg/storage.go b/pkg/svg/storage.go index fef8cd4..1455a3b 100644 --- a/pkg/svg/storage.go +++ b/pkg/svg/storage.go @@ -69,6 +69,10 @@ func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, e return "", err } defer file.Close() + _, err = io.Copy(file, data) + if err != nil { + return "", err + } return strings.TrimPrefix(file.Name(), path), nil } -- 2.49.1 From de3ba44093cc559e2b06cd4eebc71be82e73ad8b Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 11:08:48 +0100 Subject: [PATCH 09/13] fix(storage): change to mkdirall to avoid error --- pkg/svg/storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/svg/storage.go b/pkg/svg/storage.go index 1455a3b..a73facd 100644 --- a/pkg/svg/storage.go +++ b/pkg/svg/storage.go @@ -60,7 +60,7 @@ func (f FileSvgStorage) Get(id string) (io.ReadCloser, error) { func (f FileSvgStorage) CreatePublic(data io.Reader, filetype string) (string, error) { path := filepath.Join(f.basepath, f.publicSubPath) - if err := os.Mkdir(path, 0755); err != nil { + if err := os.MkdirAll(path, 0755); err != nil { return "", err } -- 2.49.1 From 205b70eeabf4b16c67c78b22a02bb4c4dff3a804 Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 11:09:19 +0100 Subject: [PATCH 10/13] feat(template): add Templating functionality and endpoint --- go.mod | 4 +- go.sum | 2 + internal/routes/download.go | 105 ++++++++++++++++++++++++++++++++++++ internal/routes/upload.go | 2 +- internal/server/http.go | 16 +++--- pkg/svg/actions/download.go | 22 ++++++++ pkg/svg/actions/template.go | 66 +++++++++++++++++++++++ 7 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 internal/routes/download.go create mode 100644 pkg/svg/actions/download.go create mode 100644 pkg/svg/actions/template.go diff --git a/go.mod b/go.mod index 231ae21..9cd88bb 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,12 @@ module tomatentum.net/svg-templater go 1.24.9 +require github.com/glebarez/go-sqlite v1.22.0 + require ( github.com/dustin/go-humanize v1.0.1 // indirect - github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/google/uuid v1.5.0 // indirect + github.com/hymkor/exregexp-go v0.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/sys v0.15.0 // indirect diff --git a/go.sum b/go.sum index 6c1a0f6..1c65730 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ 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/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/internal/routes/download.go b/internal/routes/download.go new file mode 100644 index 0000000..9235603 --- /dev/null +++ b/internal/routes/download.go @@ -0,0 +1,105 @@ +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() +} diff --git a/internal/routes/upload.go b/internal/routes/upload.go index b3fbb92..1c19dab 100644 --- a/internal/routes/upload.go +++ b/internal/routes/upload.go @@ -10,7 +10,7 @@ import ( "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") if contentType != "image/svg+xml" { diff --git a/internal/server/http.go b/internal/server/http.go index e840cb3..746b72e 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "net/http" - "path/filepath" "tomatentum.net/svg-templater/internal/routes" "tomatentum.net/svg-templater/pkg/auth" @@ -20,10 +19,17 @@ func PrepareHTTP() { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - routes.CreateSVG(w, *r) + routes.CreateSVG(w, r) + }) + registerAuthorizedFunc("/svg/{id}", func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + routes.DownloadSVG(w, r) }) - registerAuthorized("/public/", http.StripPrefix("/public/", http.FileServer(svg.Storage.GetPublicDir()))) + http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(svg.Storage.GetPublicDir()))) } func Start() { @@ -41,7 +47,3 @@ func registerAuthorized(path string, handler http.Handler) { func registerAuthorizedFunc(path string, f func(w http.ResponseWriter, r *http.Request)) { registerAuthorized(path, http.HandlerFunc(f)) } - -func GetPublicPath(path string) string { - return filepath.Join("public", path) -} diff --git a/pkg/svg/actions/download.go b/pkg/svg/actions/download.go new file mode 100644 index 0000000..28c977c --- /dev/null +++ b/pkg/svg/actions/download.go @@ -0,0 +1,22 @@ +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) +} diff --git a/pkg/svg/actions/template.go b/pkg/svg/actions/template.go new file mode 100644 index 0000000..029d4ca --- /dev/null +++ b/pkg/svg/actions/template.go @@ -0,0 +1,66 @@ +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 +} -- 2.49.1 From 7c5d319043f6f6f3a68f1c9ca8f2064dcc894b0e Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 23:23:24 +0100 Subject: [PATCH 11/13] fix(database): move database file to storage dir --- cmd/svg-templater/main.go | 9 --------- internal/command/commandline.go | 14 +++++++++++++- internal/database/database.go | 5 +++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/cmd/svg-templater/main.go b/cmd/svg-templater/main.go index 0eec781..e025df7 100644 --- a/cmd/svg-templater/main.go +++ b/cmd/svg-templater/main.go @@ -1,10 +1,7 @@ package main import ( - "log" - "tomatentum.net/svg-templater/internal/command" - "tomatentum.net/svg-templater/internal/database" "tomatentum.net/svg-templater/pkg/format" ) @@ -12,12 +9,6 @@ func main() { if !format.CheckInkscape() { panic("Inkscape not found") } - if err := database.OpenSQLite(); err != nil { - log.Fatal("Failed opening DB:\n", err) - return - } - defer database.Close() - database.InitDB() command.PrepareCommandLine() command.HandleCommandline() } diff --git a/internal/command/commandline.go b/internal/command/commandline.go index 768256d..609c076 100644 --- a/internal/command/commandline.go +++ b/internal/command/commandline.go @@ -2,7 +2,9 @@ package command import ( "flag" + "log" + "tomatentum.net/svg-templater/internal/database" "tomatentum.net/svg-templater/internal/server" "tomatentum.net/svg-templater/pkg/svg" ) @@ -23,6 +25,17 @@ func PrepareCommandLine() { func HandleCommandline() { 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 { GenerateTokenCommand() } else if deleteTokenFlag { @@ -30,7 +43,6 @@ func HandleCommandline() { } else if help { flag.PrintDefaults() } else { - svg.Storage = svg.NewFileStorage(datapath, "public") server.PrepareHTTP() server.Start() } diff --git a/internal/database/database.go b/internal/database/database.go index 64bb0a1..03a4d12 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -3,6 +3,7 @@ package database import ( "database/sql" "log" + "path/filepath" _ "github.com/glebarez/go-sqlite" ) @@ -11,8 +12,8 @@ const FILENAME string = "storage.db" var database *sql.DB -func OpenSQLite() error { - db, err := sql.Open("sqlite", FILENAME) +func OpenSQLite(basepath string) error { + db, err := sql.Open("sqlite", filepath.Join(basepath, FILENAME)) if err != nil { return err } -- 2.49.1 From 56abe1dee44faa260d771b4b087e215c297ebdb3 Mon Sep 17 00:00:00 2001 From: Tueem Date: Mon, 9 Feb 2026 23:23:44 +0100 Subject: [PATCH 12/13] fix(docker): fix Dockerfile --- Dockerfile | 7 ++++++- go.mod | 2 +- pkg/format/formatconverter.go | 5 +++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index d786009..e6ffa9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,16 @@ +# Build stage FROM golang:latest AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN go build -v -o svg-templater ./... +RUN CGO_ENABLED=0 GOOS=linux go build -v -o svg-templater ./cmd/svg-templater +# Final stage FROM alpine:latest 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 CMD ["svg-templater"] diff --git a/go.mod b/go.mod index 9cd88bb..081845e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require github.com/glebarez/go-sqlite v1.22.0 require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.5.0 // indirect - github.com/hymkor/exregexp-go v0.2.0 // indirect + github.com/hymkor/exregexp-go v0.2.0 github.com/mattn/go-isatty v0.0.20 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/sys v0.15.0 // indirect diff --git a/pkg/format/formatconverter.go b/pkg/format/formatconverter.go index d497b8e..b88530e 100644 --- a/pkg/format/formatconverter.go +++ b/pkg/format/formatconverter.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "log" "os" "os/exec" "path/filepath" @@ -74,11 +75,11 @@ func runCommand(input string, param ConversionParameters) (io.ReadCloser, error) args = append(args, "-o", outFile) args = append(args, input) - cmd := exec.Command("/opt/homebrew/bin/inkscape", args...) + 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)) } -- 2.49.1 From 26e543ac48752269b3e834c0b8d069d1774083e2 Mon Sep 17 00:00:00 2001 From: Tueem Date: Fri, 13 Feb 2026 16:01:06 +0100 Subject: [PATCH 13/13] feat(format): switch to resvg backend --- cmd/svg-templater/main.go | 4 ++-- pkg/format/formatconverter.go | 42 +++++++++++++++++------------------ 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/cmd/svg-templater/main.go b/cmd/svg-templater/main.go index e025df7..d441d36 100644 --- a/cmd/svg-templater/main.go +++ b/cmd/svg-templater/main.go @@ -6,8 +6,8 @@ import ( ) func main() { - if !format.CheckInkscape() { - panic("Inkscape not found") + if !format.CheckResvg() { + panic("Resvg not found") } command.PrepareCommandLine() command.HandleCommandline() diff --git a/pkg/format/formatconverter.go b/pkg/format/formatconverter.go index b88530e..1610752 100644 --- a/pkg/format/formatconverter.go +++ b/pkg/format/formatconverter.go @@ -2,6 +2,7 @@ package format import ( "bytes" + "errors" "fmt" "io" "log" @@ -43,26 +44,23 @@ func ConvertReader(svgblob []byte, param ConversionParameters) (io.ReadCloser, e } 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) + return convertPNG(file, param) } + return nil, errors.New("Format not supported") +} + +func convertPNG(input string, param ConversionParameters) (io.ReadCloser, error) { + var args []string + var outExt string + + fontsdir, err := svg.Storage.GetFontsDir() + if err != nil { + return nil, err + } + args = append(args, "--skip-system-fonts") + args = append(args, "--use-fonts-dir", fontsdir) if param.Width > 0 { args = append(args, "-w", strconv.Itoa(param.Width)) @@ -72,16 +70,16 @@ func runCommand(input string, param ConversionParameters) (io.ReadCloser, error) } outFile := strings.TrimSuffix(input, filepath.Ext(input)) + outExt - args = append(args, "-o", outFile) args = append(args, input) + args = append(args, outFile) - cmd := exec.Command("inkscape", args...) + cmd := exec.Command("resvg", 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)) + return nil, fmt.Errorf("resvg failed: %w: %s", err, string(out)) } file, err := os.Open(outFile) @@ -92,7 +90,7 @@ func runCommand(input string, param ConversionParameters) (io.ReadCloser, error) return file, nil } -func CheckInkscape() bool { - _, err := exec.LookPath("inkscape") +func CheckResvg() bool { + _, err := exec.LookPath("resvg") return err == nil } -- 2.49.1