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 +}