Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/testcontainers/testcontainers-go v0.38.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0
golang.org/x/crypto v0.42.0
golang.org/x/image v0.31.0
golang.org/x/term v0.35.0
golang.org/x/text v0.29.0
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -24,7 +25,9 @@ require (
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/chai2010/webp v1.4.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
Expand All @@ -38,6 +41,7 @@ require (
github.com/ebitengine/purego v0.8.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gen2brain/avif v0.4.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
Expand Down Expand Up @@ -71,6 +75,7 @@ require (
github.com/shirou/gopsutil/v4 v4.25.7 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
Expand All @@ -79,7 +84,6 @@ require (
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/image v0.31.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
Expand Down Expand Up @@ -39,6 +43,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gen2brain/avif v0.4.4 h1:Ga/ss7qcWWQm2bxFpnjYjhJsNfZrWs5RsyklgFjKRSE=
github.com/gen2brain/avif v0.4.4/go.mod h1:/XCaJcjZraQwKVhpu9aEd9aLOssYOawLvhMBtmHVGqk=
github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ=
github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
Expand Down Expand Up @@ -148,6 +154,8 @@ github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxd
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0 h1:KFdx9A0yF94K70T6ibSuvgkQQeX1xKlZVF3hEagXEtY=
github.com/testcontainers/testcontainers-go/modules/postgres v0.38.0/go.mod h1:T/QRECND6N6tAKMxF1Za+G2tpwnGEHcODzHRsgIpw9M=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
Expand Down
9 changes: 9 additions & 0 deletions metal/cli/seo/pictures.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

"github.com/oullin/handler/payload"
"github.com/oullin/pkg/cli"
pkgimages "github.com/oullin/pkg/images"
"github.com/oullin/pkg/portal"
)
Expand Down Expand Up @@ -36,6 +37,14 @@ func (g *Generator) preparePostImage(post payload.PostResponse) (preparedImage,

img, format, err := pkgimages.Fetch(source)
if err != nil {
var decodeErr *pkgimages.DecodeError
if errors.As(err, &decodeErr) {
cli.Errorln("Failed to decode remote image. Diagnostics:")
for _, line := range decodeErr.Diagnostics() {
cli.Grayln(" " + line)
}
}

return preparedImage{}, err
}

Expand Down
132 changes: 132 additions & 0 deletions pkg/images/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package images

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
)

type DecodeError struct {
Err error
ContentType string
ContentEncoding string
SniffedType string
Size int
Hash string
PrefixHex string
}

func newDecodeError(err error, payload []byte, contentType, encoding string) *DecodeError {
sample := payload
if len(sample) > 512 {
sample = sample[:512]
}

sniffed := http.DetectContentType(sample)

details := &DecodeError{
Err: err,
ContentType: strings.TrimSpace(contentType),
ContentEncoding: strings.TrimSpace(encoding),
SniffedType: sniffed,
Size: len(payload),
}

if len(payload) > 0 {
sum := sha256.Sum256(payload)
details.Hash = hex.EncodeToString(sum[:])

prefixLen := len(payload)
if prefixLen > 16 {
prefixLen = 16
}

details.PrefixHex = hex.EncodeToString(payload[:prefixLen])
}

return details
}

func (e *DecodeError) Error() string {
if e == nil {
return "decode image: <nil>"
}

var parts []string

if ct := strings.TrimSpace(e.ContentType); ct != "" {
parts = append(parts, fmt.Sprintf("content-type %q", ct))
}

if enc := strings.TrimSpace(e.ContentEncoding); enc != "" {
parts = append(parts, fmt.Sprintf("content-encoding %q", enc))
}

if sniff := strings.TrimSpace(e.SniffedType); sniff != "" {
parts = append(parts, fmt.Sprintf("sniffed %q", sniff))
}

if e.Size > 0 {
parts = append(parts, fmt.Sprintf("size %d bytes", e.Size))
}

if e.Hash != "" {
parts = append(parts, fmt.Sprintf("sha256 %s", e.Hash))
}

if e.PrefixHex != "" {
parts = append(parts, fmt.Sprintf("prefix %s", e.PrefixHex))
}

if len(parts) == 0 {
return fmt.Sprintf("decode image: %v", e.Err)
}

return fmt.Sprintf("decode image (%s): %v", strings.Join(parts, ", "), e.Err)
}

func (e *DecodeError) Unwrap() error {
if e == nil {
return nil
}

return e.Err
}

func (e *DecodeError) Diagnostics() []string {
if e == nil {
return nil
}

var lines []string

if ct := strings.TrimSpace(e.ContentType); ct != "" {
lines = append(lines, fmt.Sprintf("content-type: %s", ct))
} else {
lines = append(lines, "content-type: (missing)")
}

if enc := strings.TrimSpace(e.ContentEncoding); enc != "" {
lines = append(lines, fmt.Sprintf("content-encoding: %s", enc))
} else {
lines = append(lines, "content-encoding: (missing)")
}

if sniff := strings.TrimSpace(e.SniffedType); sniff != "" {
lines = append(lines, fmt.Sprintf("sniffed-type: %s", sniff))
}

lines = append(lines, fmt.Sprintf("payload-bytes: %d", e.Size))

if e.Hash != "" {
lines = append(lines, fmt.Sprintf("payload-sha256: %s", e.Hash))
}

if e.PrefixHex != "" {
lines = append(lines, fmt.Sprintf("payload-prefix-hex: %s", e.PrefixHex))
}

return lines
}
46 changes: 46 additions & 0 deletions pkg/images/globals.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package images

import "io"

const (
DefaultJPEGQuality = 85
supportedImageAcceptHeader = "image/webp,image/png,image/jpeg,image/gif;q=0.9,*/*;q=0.1"
defaultRemoteImageUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.2 Safari/605.1.15"
fallbackPNGAcceptHeader = "image/png,image/*;q=0.8,*/*;q=0.1"
fallbackJPEGAcceptHeader = "image/jpeg,image/*;q=0.8,*/*;q=0.1"
)

type composedReadCloser struct {
io.Reader
io.Closer
}

type multiCloser []io.Closer

func (m multiCloser) Close() error {
var firstErr error

for _, closer := range m {
if closer == nil {
continue
}

if err := closer.Close(); err != nil && firstErr == nil {
firstErr = err
}
}

return firstErr
}

type noErrorCloseFunc func()

func (f noErrorCloseFunc) Close() error {
if f == nil {
return nil
}

f()

return nil
}
Loading
Loading