From 624c1763ac99e8f8c30407825a60c5698fa635c9 Mon Sep 17 00:00:00 2001 From: Ahmed Al Hafoudh Date: Fri, 1 Aug 2025 18:02:40 +0200 Subject: [PATCH] Add external label service support to label maker (#913) * Add external label service support to label maker * Make external label service fetch to include user agent, limit response size and allow any image type * Fix linting errors * Fix "response body closed" closing the Body to soon --- .../app/api/handlers/v1/v1_ctrl_labelmaker.go | 2 +- backend/internal/sys/config/conf.go | 18 +-- backend/pkgs/labelmaker/labelmaker.go | 107 +++++++++++++++++- 3 files changed, 115 insertions(+), 12 deletions(-) diff --git a/backend/app/api/handlers/v1/v1_ctrl_labelmaker.go b/backend/app/api/handlers/v1/v1_ctrl_labelmaker.go index f3b1613b..91f47492 100644 --- a/backend/app/api/handlers/v1/v1_ctrl_labelmaker.go +++ b/backend/app/api/handlers/v1/v1_ctrl_labelmaker.go @@ -29,7 +29,7 @@ func generateOrPrint(ctrl *V1Controller, w http.ResponseWriter, r *http.Request, _, err = w.Write([]byte("Printed!")) return err } else { - return labelmaker.GenerateLabel(w, ¶ms) + return labelmaker.GenerateLabel(w, ¶ms, ctrl.config) } } diff --git a/backend/internal/sys/config/conf.go b/backend/internal/sys/config/conf.go index 10bca909..bad0ccf0 100644 --- a/backend/internal/sys/config/conf.go +++ b/backend/internal/sys/config/conf.go @@ -61,14 +61,16 @@ type WebConfig struct { } type LabelMakerConf struct { - Width int64 `yaml:"width" conf:"default:526"` - Height int64 `yaml:"height" conf:"default:200"` - Padding int64 `yaml:"padding" conf:"default:32"` - Margin int64 `yaml:"margin" conf:"default:32"` - FontSize float64 `yaml:"font_size" conf:"default:32.0"` - PrintCommand *string `yaml:"string"` - AdditionalInformation *string `yaml:"string"` - DynamicLength bool `yaml:"bool" conf:"default:true"` + Width int64 `yaml:"width" conf:"default:526"` + Height int64 `yaml:"height" conf:"default:200"` + Padding int64 `yaml:"padding" conf:"default:32"` + Margin int64 `yaml:"margin" conf:"default:32"` + FontSize float64 `yaml:"font_size" conf:"default:32.0"` + PrintCommand *string `yaml:"string"` + AdditionalInformation *string `yaml:"string"` + DynamicLength bool `yaml:"bool" conf:"default:true"` + LabelServiceUrl *string `yaml:"label_service_url"` + LabelServiceTimeout *time.Duration `yaml:"label_service_timeout"` } type BarcodeAPIConf struct { diff --git a/backend/pkgs/labelmaker/labelmaker.go b/backend/pkgs/labelmaker/labelmaker.go index fc1406e7..a28bb72a 100644 --- a/backend/pkgs/labelmaker/labelmaker.go +++ b/backend/pkgs/labelmaker/labelmaker.go @@ -9,6 +9,8 @@ import ( "image/png" "io" "log" + "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -138,11 +140,18 @@ func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeig return wrappedLines, "" } -func GenerateLabel(w io.Writer, params *GenerateParameters) error { +func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config) error { if err := params.Validate(); err != nil { return err } + // If LabelServiceUrl is configured, fetch the label from the URL instead of generating it + if cfg != nil && cfg.LabelMaker.LabelServiceUrl != nil && *cfg.LabelMaker.LabelServiceUrl != "" { + log.Printf("LabelServiceUrl configured: %s", *cfg.LabelMaker.LabelServiceUrl) + + return fetchLabelFromURL(w, *cfg.LabelMaker.LabelServiceUrl, params, cfg) + } + bodyText := params.DescriptionText if params.AdditionalInformation != nil { bodyText = bodyText + "\n" + *params.AdditionalInformation @@ -218,7 +227,7 @@ func GenerateLabel(w io.Writer, params *GenerateParameters) error { // Create the actual image with calculated height bounds := image.Rect(0, 0, params.Width, requiredHeight) img := image.NewRGBA(bounds) - draw.Draw(img, bounds, &image.Uniform{color.White}, image.Point{}, draw.Src) + draw.Draw(img, bounds, &image.Uniform{C: color.White}, image.Point{}, draw.Src) // Draw QR code onto the image draw.Draw(img, @@ -279,6 +288,98 @@ func createContext(font *truetype.Font, size float64, img *image.RGBA, dpi float return c } +// fetchLabelFromURL fetches an image from the specified URL and writes it to the writer +func fetchLabelFromURL(w io.Writer, serviceURL string, params *GenerateParameters, cfg *config.Config) error { + // Parse the base URL + baseURL, err := url.Parse(serviceURL) + if err != nil { + return fmt.Errorf("failed to parse service URL %s: %w", serviceURL, err) + } + + // Build query parameters with the same attributes passed to print command + query := url.Values{} + query.Set("Width", fmt.Sprintf("%d", params.Width)) + query.Set("Height", fmt.Sprintf("%d", params.Height)) + query.Set("QrSize", fmt.Sprintf("%d", params.QrSize)) + query.Set("Margin", fmt.Sprintf("%d", params.Margin)) + query.Set("ComponentPadding", fmt.Sprintf("%d", params.ComponentPadding)) + query.Set("TitleText", params.TitleText) + query.Set("TitleFontSize", fmt.Sprintf("%f", params.TitleFontSize)) + query.Set("DescriptionText", params.DescriptionText) + query.Set("DescriptionFontSize", fmt.Sprintf("%f", params.DescriptionFontSize)) + query.Set("Dpi", fmt.Sprintf("%f", params.Dpi)) + query.Set("URL", params.URL) + query.Set("DynamicLength", fmt.Sprintf("%t", params.DynamicLength)) + + // Add AdditionalInformation if it exists + if params.AdditionalInformation != nil { + query.Set("AdditionalInformation", *params.AdditionalInformation) + } + + // Set the query parameters + baseURL.RawQuery = query.Encode() + finalServiceURL := baseURL.String() + + log.Printf("Fetching label from URL: %s", finalServiceURL) + + // Use configured timeout or default to 30 seconds + timeout := 30 * time.Second + if cfg != nil && cfg.LabelMaker.LabelServiceTimeout != nil { + timeout = *cfg.LabelMaker.LabelServiceTimeout + } + + // Create HTTP client with configurable timeout + client := &http.Client{ + Timeout: timeout, + } + + // Create HTTP request with custom headers + req, err := http.NewRequest("GET", finalServiceURL, nil) + if err != nil { + return fmt.Errorf("failed to create request for URL %s: %w", finalServiceURL, err) + } + + // Set custom headers + req.Header.Set("User-Agent", "Homebox-LabelMaker/1.0") + req.Header.Set("Accept", "image/*") + + // Make HTTP request to the label service + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to fetch label from URL %s: %w", finalServiceURL, err) + } + + // Check if the response status is OK + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("label service returned status %d for URL %s", resp.StatusCode, finalServiceURL) + } + + // Check if the response is an image + contentType := resp.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + return fmt.Errorf("label service returned invalid content type %s, expected image/*", contentType) + } + + // Set default max response size (10MB) + maxResponseSize := int64(10 << 20) + if cfg != nil { + maxResponseSize = cfg.Web.MaxUploadSize << 20 + } + limitedReader := io.LimitReader(resp.Body, maxResponseSize) + + // Copy the response body to the writer + _, err = io.Copy(w, limitedReader) + if err != nil { + return fmt.Errorf("failed to write fetched label data: %w", err) + } + + if err := resp.Body.Close(); err != nil { + log.Printf("failed to close response body: %v", err) + } + + return nil +} + func PrintLabel(cfg *config.Config, params *GenerateParameters) error { tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("label-%d.png", time.Now().UnixNano())) f, err := os.OpenFile(tmpFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) @@ -292,7 +393,7 @@ func PrintLabel(cfg *config.Config, params *GenerateParameters) error { } }() - err = GenerateLabel(f, params) + err = GenerateLabel(f, params, cfg) if err != nil { return err }