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
This commit is contained in:
Ahmed Al Hafoudh
2025-08-01 18:02:40 +02:00
committed by GitHub
parent 75c2423fd5
commit 624c1763ac
3 changed files with 115 additions and 12 deletions

View File

@@ -29,7 +29,7 @@ func generateOrPrint(ctrl *V1Controller, w http.ResponseWriter, r *http.Request,
_, err = w.Write([]byte("Printed!")) _, err = w.Write([]byte("Printed!"))
return err return err
} else { } else {
return labelmaker.GenerateLabel(w, &params) return labelmaker.GenerateLabel(w, &params, ctrl.config)
} }
} }

View File

@@ -61,14 +61,16 @@ type WebConfig struct {
} }
type LabelMakerConf struct { type LabelMakerConf struct {
Width int64 `yaml:"width" conf:"default:526"` Width int64 `yaml:"width" conf:"default:526"`
Height int64 `yaml:"height" conf:"default:200"` Height int64 `yaml:"height" conf:"default:200"`
Padding int64 `yaml:"padding" conf:"default:32"` Padding int64 `yaml:"padding" conf:"default:32"`
Margin int64 `yaml:"margin" conf:"default:32"` Margin int64 `yaml:"margin" conf:"default:32"`
FontSize float64 `yaml:"font_size" conf:"default:32.0"` FontSize float64 `yaml:"font_size" conf:"default:32.0"`
PrintCommand *string `yaml:"string"` PrintCommand *string `yaml:"string"`
AdditionalInformation *string `yaml:"string"` AdditionalInformation *string `yaml:"string"`
DynamicLength bool `yaml:"bool" conf:"default:true"` DynamicLength bool `yaml:"bool" conf:"default:true"`
LabelServiceUrl *string `yaml:"label_service_url"`
LabelServiceTimeout *time.Duration `yaml:"label_service_timeout"`
} }
type BarcodeAPIConf struct { type BarcodeAPIConf struct {

View File

@@ -9,6 +9,8 @@ import (
"image/png" "image/png"
"io" "io"
"log" "log"
"net/http"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -138,11 +140,18 @@ func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeig
return wrappedLines, "" 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 { if err := params.Validate(); err != nil {
return err 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 bodyText := params.DescriptionText
if params.AdditionalInformation != nil { if params.AdditionalInformation != nil {
bodyText = bodyText + "\n" + *params.AdditionalInformation bodyText = bodyText + "\n" + *params.AdditionalInformation
@@ -218,7 +227,7 @@ func GenerateLabel(w io.Writer, params *GenerateParameters) error {
// Create the actual image with calculated height // Create the actual image with calculated height
bounds := image.Rect(0, 0, params.Width, requiredHeight) bounds := image.Rect(0, 0, params.Width, requiredHeight)
img := image.NewRGBA(bounds) 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 QR code onto the image
draw.Draw(img, draw.Draw(img,
@@ -279,6 +288,98 @@ func createContext(font *truetype.Font, size float64, img *image.RGBA, dpi float
return c 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 { func PrintLabel(cfg *config.Config, params *GenerateParameters) error {
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("label-%d.png", time.Now().UnixNano())) 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) 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 { if err != nil {
return err return err
} }