Compare commits
2
Commits
0969cf43bb
...
0e345482bc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e345482bc | ||
|
|
5058fd532b |
+6
-3
@@ -22,8 +22,9 @@ FROM golang:1.25.4-alpine@sha256:d3f0cf7723f3429e3f9ed846243970b20a2de7bae6a5b66
|
|||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
# script/bootstrap --cgo installs the build dependencies (a C compiler
|
# script/bootstrap --cgo installs the build dependencies (a C compiler,
|
||||||
# and the libvips and libheif headers) and downloads the Go modules.
|
# the libvips and libheif headers, and libvips' JPEG XL support, which
|
||||||
|
# the tests need) and downloads the Go modules.
|
||||||
COPY script/ ./script/
|
COPY script/ ./script/
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN script/bootstrap --cgo
|
RUN script/bootstrap --cgo
|
||||||
@@ -80,9 +81,11 @@ RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
|||||||
# alpine:3.21, 2026-02-25
|
# alpine:3.21, 2026-02-25
|
||||||
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
||||||
|
|
||||||
# Install runtime dependencies only
|
# Install runtime dependencies only. vips-jxl is libvips' JPEG XL
|
||||||
|
# support, without which pixad does not start.
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
vips \
|
vips \
|
||||||
|
vips-jxl \
|
||||||
libheif \
|
libheif \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ another part of pixa failed to stop. A request not finished by then is cut off.
|
|||||||
`docker stop` waits 10 seconds before it kills the container.
|
`docker stop` waits 10 seconds before it kills the container.
|
||||||
|
|
||||||
Outside Docker, pixa needs libvips (the image has 8.15) and libheif to run, as
|
Outside Docker, pixa needs libvips (the image has 8.15) and libheif to run, as
|
||||||
it uses libvips through CGO; building it also needs their development files,
|
it uses libvips through CGO. pixad does not start unless libvips has its JPEG XL
|
||||||
|
support, which on Alpine is the `vips-jxl` package and which the nix and brew
|
||||||
|
packages of libvips include, as do the apt ones from Debian 12 and Ubuntu 24.04
|
||||||
|
on. Building pixa also needs the development files of libvips and libheif,
|
||||||
`pkg-config` and a C compiler. `script/bootstrap --cgo` installs all of these,
|
`pkg-config` and a C compiler. `script/bootstrap --cgo` installs all of these,
|
||||||
as the `Dockerfile` does where it compiles pixa. Plain `script/bootstrap`, which
|
as the `Dockerfile` does where it compiles pixa. Plain `script/bootstrap`, which
|
||||||
`script/setup` and `script/cibuild` run, installs git, make and Go, and Node,
|
`script/setup` and `script/cibuild` run, installs git, make and Go, and Node,
|
||||||
@@ -529,10 +532,13 @@ Key settings in more detail:
|
|||||||
- `signing_key` — HMAC secret for URL signatures
|
- `signing_key` — HMAC secret for URL signatures
|
||||||
- `db_url` — the SQLite database to open; omitted, it is
|
- `db_url` — the SQLite database to open; omitted, it is
|
||||||
`file:<state_dir>/state.sqlite3?_pragma=journal_mode(WAL)`, which keeps the
|
`file:<state_dir>/state.sqlite3?_pragma=journal_mode(WAL)`, which keeps the
|
||||||
database in WAL mode. pixa adds `_pragma=busy_timeout(5000)` to any `db_url`,
|
database in WAL mode. pixa opens one connection to it, so its own reads and
|
||||||
so a write that finds another in progress waits up to five seconds for it
|
writes run one at a time. Requests wait while eviction runs one of its
|
||||||
instead of failing. WAL mode comes only from the URL: keep
|
queries, some of which read a whole table. pixa adds
|
||||||
`_pragma=journal_mode(WAL)` in one you set
|
`_pragma=busy_timeout(5000)` to any `db_url`, so a write that finds another
|
||||||
|
program writing to the file waits up to five seconds for it instead of
|
||||||
|
failing. WAL mode comes only from the URL: keep `_pragma=journal_mode(WAL)` in
|
||||||
|
one you set
|
||||||
- `cache_max_bytes` — disk cache size limit in bytes; `0` disables the disk
|
- `cache_max_bytes` — disk cache size limit in bytes; `0` disables the disk
|
||||||
cache entirely; omitted defaults to 75% of the sum of the free space on the
|
cache entirely; omitted defaults to 75% of the sum of the free space on the
|
||||||
filesystem containing `<state_dir>/cache/` and the bytes of source and
|
filesystem containing `<state_dir>/cache/` and the bytes of source and
|
||||||
@@ -582,8 +588,8 @@ provide:
|
|||||||
|
|
||||||
- `script/bootstrap` — install git, make, Go, Node, Yarn and prettier and
|
- `script/bootstrap` — install git, make, Go, Node, Yarn and prettier and
|
||||||
download the Go modules (idempotent); with `--cgo`, the C compiler and the
|
download the Go modules (idempotent); with `--cgo`, the C compiler and the
|
||||||
libvips and libheif libraries that compiling pixa needs instead of Node, Yarn
|
libvips (with its JPEG XL support) and libheif libraries that compiling and
|
||||||
and prettier
|
testing pixa need instead of Node, Yarn and prettier
|
||||||
- `script/setup` — make a fresh clone ready for development (bootstrap, then
|
- `script/setup` — make a fresh clone ready for development (bootstrap, then
|
||||||
install-precommit)
|
install-precommit)
|
||||||
- `script/projectname` — output the project name ("pixa")
|
- `script/projectname` — output the project name ("pixa")
|
||||||
|
|||||||
@@ -30,6 +30,21 @@ P2: security: per-IP rate limiting on the image routes
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-08 libvips' JPEG XL support is installed and required (part of #222):
|
||||||
|
`script/bootstrap --cgo` installs `vips-jxl` when its package manager is apk,
|
||||||
|
as Alpine's `vips` package lacks the support, and the runtime stage of the
|
||||||
|
`Dockerfile` installs it too. `imgcache.NewService` fails, naming the fix,
|
||||||
|
when `imageprocessor.CheckJPEGXLSupport` finds that libvips cannot load and
|
||||||
|
save JPEG XL, so pixad does not start without it. A test saves an image as
|
||||||
|
JPEG XL with govips and loads it back. JPEG XL is not yet a format pixa
|
||||||
|
serves.
|
||||||
|
- 2026-10-08 SQLite writes no longer fail with "database is locked" under load
|
||||||
|
(closes #223): `internal/database` opens the database with one connection, so
|
||||||
|
pixa's own reads and writes run on it one at a time instead of competing for
|
||||||
|
SQLite's lock, where a write that kept losing could wait past the five-second
|
||||||
|
busy timeout and be lost. The busy timeout stays, for another program writing
|
||||||
|
to the same file. No code in pixa keeps rows or a transaction open while it
|
||||||
|
runs another query, which with one connection would wait forever.
|
||||||
- 2026-10-05 the format `auto` (closes #88): a format in the `/v1/image/` path,
|
- 2026-10-05 the format `auto` (closes #88): a format in the `/v1/image/` path,
|
||||||
an encrypted URL's token and the generator page's format choice, chosen for
|
an encrypted URL's token and the generator page's format choice, chosen for
|
||||||
each request from `Accept` once the signature or token is checked: AVIF when
|
each request from `Accept` once the signature or token is checked: AVIF when
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TestConcurrentWritesAllSucceed opens a database the way pixad does and
|
// TestConcurrentWritesAllSucceed opens a database the way pixad does and
|
||||||
// writes to it from several goroutines at once, so the writes run on
|
// writes to it from several goroutines at once, as one request's writes and
|
||||||
// separate connections, as one request's writes and the background eviction
|
// the background eviction pass do. Every write must succeed, none failing
|
||||||
// pass do. Every write must succeed, none failing with "database is locked",
|
// with "database is locked", whether or not db_url already has parameters,
|
||||||
// whether or not db_url already has parameters, and the parameters it has
|
// and the parameters it has must still apply.
|
||||||
// must still apply.
|
|
||||||
func TestConcurrentWritesAllSucceed(t *testing.T) {
|
func TestConcurrentWritesAllSucceed(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -237,17 +237,18 @@ func ApplyMigrations(ctx context.Context, db *sql.DB, log *slog.Logger) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DB returns the underlying sql.DB.
|
// DB returns the underlying sql.DB. It has one connection, so close any
|
||||||
|
// rows and end any transaction before running another query on it; a
|
||||||
|
// query run while they are open waits forever.
|
||||||
func (s *Database) DB() *sql.DB {
|
func (s *Database) DB() *sql.DB {
|
||||||
return s.db
|
return s.db
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Database) connect(ctx context.Context) error {
|
func (s *Database) connect(ctx context.Context) error {
|
||||||
// Requests and the eviction pass write on separate connections. With
|
// With a busy timeout, a write that finds another program writing to
|
||||||
// a busy timeout, a write that finds another one in progress waits up
|
// the same database file waits up to five seconds for it instead of
|
||||||
// to five seconds for it instead of failing at once with "database is
|
// failing at once with "database is locked". The driver runs each
|
||||||
// locked". The driver runs each _pragma parameter on every connection
|
// _pragma parameter on every connection it opens.
|
||||||
// it opens.
|
|
||||||
separator := "?"
|
separator := "?"
|
||||||
if strings.Contains(s.config.DBURL, "?") {
|
if strings.Contains(s.config.DBURL, "?") {
|
||||||
separator = "&"
|
separator = "&"
|
||||||
@@ -264,6 +265,11 @@ func (s *Database) connect(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One connection: pixa's own reads and writes run on it one at a
|
||||||
|
// time instead of competing for SQLite's lock, where a write that
|
||||||
|
// keeps losing can wait past the busy timeout and be lost.
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
|
||||||
err = db.PingContext(ctx)
|
err = db.PingContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Error("failed to ping database", "error", err)
|
s.log.Error("failed to ping database", "error", err)
|
||||||
|
|||||||
@@ -37,6 +37,24 @@ func initVips() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errNoJPEGXL is returned by CheckJPEGXLSupport.
|
||||||
|
var errNoJPEGXL = errors.New("libvips lacks JPEG XL support: install " +
|
||||||
|
"vips-jxl on Alpine, or use a libvips built with libjxl")
|
||||||
|
|
||||||
|
// CheckJPEGXLSupport returns an error, naming the fix, when libvips
|
||||||
|
// cannot load and save JPEG XL.
|
||||||
|
func CheckJPEGXLSupport() error {
|
||||||
|
initVips()
|
||||||
|
|
||||||
|
// govips counts a format as supported when libvips has its loader;
|
||||||
|
// libvips builds the JPEG XL loader and saver together.
|
||||||
|
if !vips.IsTypeSupported(vips.ImageTypeJXL) {
|
||||||
|
return errNoJPEGXL
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Format represents supported output image formats.
|
// Format represents supported output image formats.
|
||||||
type Format string
|
type Format string
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package imageprocessor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davidbyttow/govips/v2/vips"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCheckJPEGXLSupport fails when libvips lacks JPEG XL support, as on
|
||||||
|
// Alpine without the vips-jxl package.
|
||||||
|
func TestCheckJPEGXLSupport(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := CheckJPEGXLSupport()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CheckJPEGXLSupport() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLibvipsSavesAndLoadsJPEGXL saves an image as JPEG XL with govips and
|
||||||
|
// loads it back. It fails when libvips lacks JPEG XL support, as on Alpine
|
||||||
|
// without the vips-jxl package.
|
||||||
|
func TestLibvipsSavesAndLoadsJPEGXL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
img, err := vips.NewImageFromBuffer(createTestJPEG(t, 64, 48))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load test JPEG: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer img.Close()
|
||||||
|
|
||||||
|
jxl, _, err := img.ExportJxl(vips.NewJxlExportParams())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExportJxl() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded, err := vips.NewImageFromBuffer(jxl)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load the JPEG XL image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer loaded.Close()
|
||||||
|
|
||||||
|
if loaded.Format() != vips.ImageTypeJXL {
|
||||||
|
t.Errorf("loaded format = %s, want jxl", vips.ImageTypes[loaded.Format()])
|
||||||
|
}
|
||||||
|
|
||||||
|
if loaded.Width() != 64 || loaded.Height() != 48 {
|
||||||
|
t.Errorf("loaded size = %dx%d, want 64x48", loaded.Width(), loaded.Height())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,6 +100,13 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
|
|||||||
allowHTTP = cfg.FetcherConfig.AllowHTTP
|
allowHTTP = cfg.FetcherConfig.AllowHTTP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JPEG XL is to become the default output format, so pixad does not
|
||||||
|
// start without it.
|
||||||
|
err := imageprocessor.CheckJPEGXLSupport()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
maxResponseSize := fetcherCfg.MaxResponseSize
|
maxResponseSize := fetcherCfg.MaxResponseSize
|
||||||
processor := imageprocessor.New(imageprocessor.Params{
|
processor := imageprocessor.New(imageprocessor.Params{
|
||||||
MaxInputBytes: maxResponseSize,
|
MaxInputBytes: maxResponseSize,
|
||||||
|
|||||||
+16
-5
@@ -14,11 +14,12 @@
|
|||||||
# script/fmt-check: all the host needs, as
|
# script/fmt-check: all the host needs, as
|
||||||
# the checks compile pixa in Docker
|
# the checks compile pixa in Docker
|
||||||
# script/bootstrap --cgo git, make, Go, and a C compiler and the
|
# script/bootstrap --cgo git, make, Go, and a C compiler and the
|
||||||
# CGO image libraries (pkg-config, vips,
|
# CGO image libraries (pkg-config, vips
|
||||||
# libheif) for the govips bindings instead
|
# with its JPEG XL support, libheif) for
|
||||||
# of Node: to compile pixa, in the
|
# the govips bindings instead of Node: to
|
||||||
# Dockerfile's test phase and build stage,
|
# compile pixa, in the Dockerfile's test
|
||||||
# which format nothing
|
# phase and build stage, which format
|
||||||
|
# nothing
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
@@ -150,6 +151,16 @@ ensure_cgo_deps() {
|
|||||||
if ! pkg-config --exists vips; then
|
if ! pkg-config --exists vips; then
|
||||||
pkg_install vips libvips-dev vips vips-dev
|
pkg_install vips libvips-dev vips vips-dev
|
||||||
fi
|
fi
|
||||||
|
# libvips' JPEG XL loader and saver are in the nix and brew vips
|
||||||
|
# packages, and in apt's from Debian 12 and Ubuntu 24.04 on, but in
|
||||||
|
# the package vips-jxl on Alpine. detect_pkgmgr is called only where
|
||||||
|
# apk exists, as on apt it updates the package lists.
|
||||||
|
if ! missing apk; then
|
||||||
|
detect_pkgmgr
|
||||||
|
fi
|
||||||
|
if [ "$PKGMGR" = "apk" ] && ! apk info -e vips-jxl >/dev/null; then
|
||||||
|
apk add --no-cache vips-jxl
|
||||||
|
fi
|
||||||
if ! pkg-config --exists libheif; then
|
if ! pkg-config --exists libheif; then
|
||||||
pkg_install libheif libheif-dev libheif libheif-dev
|
pkg_install libheif libheif-dev libheif libheif-dev
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user