From c0251c5e0ecad7c8dbd085c0d1a7326628dbd149 Mon Sep 17 00:00:00 2001 From: Henry <chocolatkey@gmail.com> Date: Mon, 17 Jun 2024 00:32:30 -0700 Subject: [PATCH] Upgrades to rwp utility and repo CI/CD (#91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mickaƫl Menu <mickael.menu@gmail.com> --- .github/workflows/build.yml | 10 +- .github/workflows/release.yml | 51 +++++- .goreleaser.yaml | 26 ++- CHANGELOG.md | 11 +- Dockerfile | 56 ++++++ README.md | 5 + cmd/rwp/cmd/serve.go | 112 ++++++++++++ cmd/rwp/cmd/serve/api.go | 257 ++++++++++++++++++++++++++++ cmd/rwp/cmd/serve/cache/local.go | 90 ++++++++++ cmd/rwp/cmd/serve/cache/pubcache.go | 22 +++ cmd/rwp/cmd/serve/helpers.go | 64 +++++++ cmd/rwp/cmd/serve/router.go | 47 +++++ cmd/rwp/cmd/serve/server.go | 32 ++++ go.mod | 22 ++- go.sum | 68 ++++++-- 15 files changed, 819 insertions(+), 54 deletions(-) create mode 100644 Dockerfile create mode 100644 cmd/rwp/cmd/serve.go create mode 100644 cmd/rwp/cmd/serve/api.go create mode 100644 cmd/rwp/cmd/serve/cache/local.go create mode 100644 cmd/rwp/cmd/serve/cache/pubcache.go create mode 100644 cmd/rwp/cmd/serve/helpers.go create mode 100644 cmd/rwp/cmd/serve/router.go create mode 100644 cmd/rwp/cmd/serve/server.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea303847..c26fd3ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,21 +2,21 @@ name: Build and Test on: push: - branches: [ main ] + branches: [ main, develop ] pull_request: branches: [ '**' ] jobs: build: - runs-on: ubuntu-latest + runs-on: [self-hosted, arm64] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.21 + go-version: '>=1.22.0' - name: Build run: go build -v ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d004e69e..de394ebb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,24 +7,59 @@ on: permissions: contents: write + packages: write + +env: + IMAGE_NAME: rwp jobs: release: - runs-on: ubuntu-latest + runs-on: [self-hosted, arm64] steps: - - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 with: fetch-depth: 0 - run: git fetch --force --tags - - uses: actions/setup-go@v3 + - name: Set up Go + uses: actions/setup-go@v5 with: - go-version: '>=1.21.0' - cache: true - - uses: goreleaser/goreleaser-action@v4 + go-version: '>=1.22.0' + - name: Build release + uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser version: latest args: release --clean env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + container: + runs-on: [self-hosted, arm64] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: git fetch --force --tags + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push Docker image + run: docker buildx build --platform=linux/amd64,linux/arm64,linux/arm/v7 . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" + - name: Log in to registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + - name: Push image + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + # This changes all uppercase characters to lowercase. + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + # This strips the git ref prefix from the version. + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + # This strips the "v" prefix from the tag name. + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + # This uses the Docker `latest` tag convention. + [ "$VERSION" == "main" ] && VERSION=latest + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + docker buildx build --push \ + --tag $IMAGE_ID:$VERSION \ + --platform linux/amd64,linux/arm64,linux/arm/v7 . diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7ef8200d..892bcd44 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -2,29 +2,37 @@ before: hooks: - go mod tidy + - go generate ./... builds: - main: ./cmd/rwp/ + env: + - CGO_ENABLED=0 id: rwp binary: rwp goos: - linux - windows - darwin + goamd64: + - v3 - - main: ./cmd/server/ - id: rwp-server - binary: rwp-server - goos: - - linux - - windows - - darwin +# - main: ./cmd/server/ +# env: +# - CGO_ENABLED=0 +# id: rwp-server +# binary: rwp-server +# goos: +# - linux +# - windows +# - darwin archives: - format: tar.gz # this name template makes the OS and Arch compatible with the results of uname. + # Used to start with {{ .ProjectName }} name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ + rwp_ + {{- tolower .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5563e3bd..d04ed03e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,4 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -### Added - -* Add the [Media Type API](https://readium.org/architecture/proposals/001-media-type.html). -* Add the file and archive fetchers of the [Fetcher API](https://readium.org/architecture/proposals/002-composite-fetcher-api.html). - -### Changed - -* Restructuring of the repo's folders -* Removal of legacy models (LCP etc.) -* Updated shared models to latest specs \ No newline at end of file +TODO \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..421c7392 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +FROM --platform=$BUILDPLATFORM golang:1-bookworm as builder +ARG BUILDARCH TARGETOS TARGETARCH + +# Install GoReleaser +RUN wget --no-verbose "https://github.com/goreleaser/goreleaser/releases/download/v1.26.2/goreleaser_1.26.2_$BUILDARCH.deb" +RUN dpkg -i "goreleaser_1.26.2_$BUILDARCH.deb" + +# Create and change to the app directory. +WORKDIR /app + +# Retrieve application dependencies. +# This allows the container build to reuse cached dependencies. +# Expecting to copy go.mod and if present go.sum. +COPY go.* ./ +RUN go mod download + +# Copy local code to the container image. +COPY . ./ + +# RUN git lfs pull && ls -alh publications + +# Run goreleaser +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + GOOS=$TARGETOS GOARCH=$TARGETARCH goreleaser build --single-target --id rwp --skip=validate --snapshot --output ./rwp + +# Run tests +# FROM builder AS tester +# RUN go test ./... + +# Produces very small images +FROM gcr.io/distroless/static-debian12 AS packager + +# Extra metadata +LABEL org.opencontainers.image.source="https://github.com/readium/go-toolkit" + +# Add Fedora's mimetypes (pretty up-to-date and expansive) +# since the distroless container doesn't have any. Go uses +# this file as part of its mime package, and readium/go-toolkit +# has a mediatype package that falls back to Go's mime +# package to discover a file's mimetype when all else fails. +ADD https://pagure.io/mailcap/raw/master/f/mime.types /etc/ + +# Add two demo EPUBs to the container by default +ADD --chown=nonroot:nonroot https://readium-playground-files.storage.googleapis.com/demo/moby-dick.epub /srv/publications/ +ADD --chown=nonroot:nonroot https://readium-playground-files.storage.googleapis.com/demo/BellaOriginal3.epub /srv/publications/ + +# Copy built Go binary +COPY --from=builder "/app/rwp" /opt/ + +EXPOSE 15080 + +USER nonroot:nonroot + +ENTRYPOINT ["/opt/rwp"] +CMD ["serve", "/srv/publications", "--address", "0.0.0.0"] \ No newline at end of file diff --git a/README.md b/README.md index ed7efc10..2bddc6bf 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,8 @@ rwp manifest --infer-a11y=merged publication.epub | jq .metadata | `feature` | `tableOfContents` | If the publications contains a table of contents (check for the presence of a `toc` collection in RWPM) | | `feature` | `MathML` | If the publication contains any resource with MathML (check for the presence of the `contains` property where the value is `mathml` in `readingOrder` or `resources` in RWPM) | | `feature` | `synchronizedAudioText` | If the publication contains any reference to Media Overlays (TBD in RWPM) | + +### HTTP streaming of local publications + +`rwp serve` starts an HTTP server that serves EPUB, CBZ and other compatible formats from a given directory. +A log is printed to stdout. \ No newline at end of file diff --git a/cmd/rwp/cmd/serve.go b/cmd/rwp/cmd/serve.go new file mode 100644 index 00000000..5e4bd192 --- /dev/null +++ b/cmd/rwp/cmd/serve.go @@ -0,0 +1,112 @@ +package cmd + +import ( + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "time" + + "log/slog" + + "github.com/readium/go-toolkit/cmd/rwp/cmd/serve" + "github.com/readium/go-toolkit/pkg/streamer" + "github.com/spf13/cobra" +) + +var debugFlag bool + +var bindAddressFlag string + +var bindPortFlag uint16 + +var serveCmd = &cobra.Command{ + Use: "serve <directory>", + Short: "Start a local HTTP server, serving a specified directory of publications", + Long: `Start a local HTTP server, serving a specified directory of publications. + +This command will start an HTTP serve listening by default on 'localhost:15080', +serving all compatible files (EPUB, PDF, CBZ, etc.) found in the directory +as Readium Web Publications. To get started, the manifest can be accessed from +'http://localhost:15080/<filename in base64url encoding without padding>/manifest.json'. +This file serves as the entry point and contains metadata and links to the rest +of the files that can be accessed for the publication. + +For debugging purposes, the server also exposes a '/list.json' endpoint that +returns a list of all the publications found in the directory along with their +encoded paths. This will be replaced by an OPDS 2 feed in a future release. + +Note: This server is not meant for production usage, and should not be exposed +to the internet except for testing/debugging purposes.`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("expects a directory path to serve publications from") + } else if len(args) > 1 { + return errors.New("accepts a directory path") + } + return nil + }, + + SuggestFor: []string{"server"}, + RunE: func(cmd *cobra.Command, args []string) error { + // By the time we reach this point, we know that the arguments were + // properly parsed, and we don't want to show the usage if an API error + // occurs. + cmd.SilenceUsage = true + + path := filepath.Clean(args[0]) + fi, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("given directory %s does not exist", path) + } + return fmt.Errorf("failed to stat %s: %w", path, err) + } + if !fi.IsDir() { + return fmt.Errorf("given path %s is not a directory", path) + } + + // Log level + if debugFlag { + slog.SetLogLoggerLevel(slog.LevelDebug) + } else { + slog.SetLogLoggerLevel(slog.LevelInfo) + } + + pubServer := serve.NewServer(serve.ServerConfig{ + Debug: debugFlag, + BaseDirectory: path, + JSONIndent: indentFlag, + InferA11yMetadata: streamer.InferA11yMetadata(inferA11yFlag), + }) + + bind := fmt.Sprintf("%s:%d", bindAddressFlag, bindPortFlag) + httpServer := &http.Server{ + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + Addr: bind, + Handler: pubServer.Routes(), + } + slog.Info("Starting HTTP server", "address", "http://"+httpServer.Addr) + if err := httpServer.ListenAndServe(); err != http.ErrServerClosed { + slog.Error("Server stopped", "error", err) + } else { + slog.Info("Goodbye!") + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(serveCmd) + + serveCmd.Flags().StringVarP(&bindAddressFlag, "address", "a", "localhost", "Address to bind the HTTP server to") + serveCmd.Flags().Uint16VarP(&bindPortFlag, "port", "p", 15080, "Port to bind the HTTP server to") + serveCmd.Flags().StringVarP(&indentFlag, "indent", "i", "", "Indentation used to pretty-print JSON files") + serveCmd.Flags().Var(&inferA11yFlag, "infer-a11y", "Infer accessibility metadata: no, merged, split") + serveCmd.Flags().BoolVarP(&debugFlag, "debug", "d", false, "Enable debug mode") + +} diff --git a/cmd/rwp/cmd/serve/api.go b/cmd/rwp/cmd/serve/api.go new file mode 100644 index 00000000..552b6fbc --- /dev/null +++ b/cmd/rwp/cmd/serve/api.go @@ -0,0 +1,257 @@ +package serve + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "log/slog" + "net/http" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/gorilla/mux" + httprange "github.com/gotd/contrib/http_range" + "github.com/pkg/errors" + "github.com/readium/go-toolkit/cmd/rwp/cmd/serve/cache" + "github.com/readium/go-toolkit/pkg/asset" + "github.com/readium/go-toolkit/pkg/manifest" + "github.com/readium/go-toolkit/pkg/pub" + "github.com/readium/go-toolkit/pkg/streamer" + "github.com/zeebo/xxh3" +) + +type demoListItem struct { + Filename string `json:"filename"` + Path string `json:"path"` +} + +func (s *Server) demoList(w http.ResponseWriter, req *http.Request) { + fi, err := os.ReadDir(s.config.BaseDirectory) + if err != nil { + slog.Error("failed reading publications directory", "error", err) + w.WriteHeader(500) + return + } + files := make([]demoListItem, len(fi)) + for i, f := range fi { + files[i] = demoListItem{ + Filename: f.Name(), + Path: base64.RawURLEncoding.EncodeToString([]byte(f.Name())), + } + } + enc := json.NewEncoder(w) + enc.SetIndent("", s.config.JSONIndent) + enc.Encode(files) +} + +func (s *Server) getPublication(filename string) (*pub.Publication, error) { + fpath, err := base64.RawURLEncoding.DecodeString(filename) + if err != nil { + return nil, err + } + + cp := filepath.Clean(string(fpath)) + dat, ok := s.lfu.Get(cp) + if !ok { + pub, err := streamer.New(streamer.Config{ + InferA11yMetadata: s.config.InferA11yMetadata, + }).Open(asset.File(filepath.Join(s.config.BaseDirectory, cp)), "") + if err != nil { + return nil, errors.Wrap(err, "failed opening "+cp) + } + + // TODO: Remove this after we make links relative in the go-toolkit + for i, link := range pub.Manifest.Links { + pub.Manifest.Links[i] = makeRelative(link) + } + for i, link := range pub.Manifest.Resources { + pub.Manifest.Resources[i] = makeRelative(link) + } + for i, link := range pub.Manifest.ReadingOrder { + pub.Manifest.ReadingOrder[i] = makeRelative(link) + } + for i, link := range pub.Manifest.TableOfContents { + pub.Manifest.TableOfContents[i] = makeRelative(link) + } + var makeCollectionRelative func(mp manifest.PublicationCollectionMap) + makeCollectionRelative = func(mp manifest.PublicationCollectionMap) { + for i := range mp { + for j := range mp[i] { + for k := range mp[i][j].Links { + mp[i][j].Links[k] = makeRelative(mp[i][j].Links[k]) + } + makeCollectionRelative(mp[i][j].Subcollections) + } + } + } + makeCollectionRelative(pub.Manifest.Subcollections) + + // Cache the publication + encPub := &cache.CachedPublication{Publication: pub} + s.lfu.Set(cp, encPub) + + return encPub.Publication, nil + } + return dat.(*cache.CachedPublication).Publication, nil +} + +func (s *Server) getManifest(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + filename := vars["path"] + + // Load the publication + publication, err := s.getPublication(filename) + if err != nil { + slog.Error("failed opening publication", "error", err) + w.WriteHeader(500) + return + } + + // Create "self" link in manifest + scheme := "http://" + if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" { + // Note: this is never going to be 100% accurate behind proxies, + // but it's better than nothing for a dev server. + scheme = "https://" + } + rPath, _ := s.router.Get("manifest").URLPath("path", vars["path"]) + selfLink := &manifest.Link{ + Rels: manifest.Strings{"self"}, + Type: conformsToAsMimetype(publication.Manifest.Metadata.ConformsTo), + Href: scheme + req.Host + rPath.String(), + } + + // Marshal the manifest + j, err := json.Marshal(publication.Manifest.ToMap(selfLink)) + if err != nil { + slog.Error("failed marshalling manifest JSON", "error", err) + w.WriteHeader(500) + return + } + + // Indent JSON + var identJSON bytes.Buffer + if s.config.JSONIndent == "" { + _, err = identJSON.Write(j) + if err != nil { + slog.Error("failed writing manifest JSON to buffer", "error", err) + w.WriteHeader(500) + return + } + } else { + err = json.Indent(&identJSON, j, "", s.config.JSONIndent) + if err != nil { + slog.Error("failed indenting manifest JSON", "error", err) + w.WriteHeader(500) + return + } + } + + // Add headers + w.Header().Set("content-type", conformsToAsMimetype(publication.Manifest.Metadata.ConformsTo)+"; charset=utf-8") + w.Header().Set("cache-control", "private, must-revalidate") + w.Header().Set("access-control-allow-origin", "*") // TODO: provide options? + + // Etag based on hash of the manifest bytes + etag := `"` + strconv.FormatUint(xxh3.Hash(identJSON.Bytes()), 36) + `"` + w.Header().Set("Etag", etag) + if match := req.Header.Get("If-None-Match"); match != "" { + if strings.Contains(match, etag) { + w.WriteHeader(http.StatusNotModified) + return + } + } + + // Write response body + _, err = identJSON.WriteTo(w) + if err != nil { + slog.Error("failed writing manifest JSON to response writer", "error", err) + w.WriteHeader(500) + return + } +} + +func (s *Server) getAsset(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + filename := vars["path"] + + // Load the publication + publication, err := s.getPublication(filename) + if err != nil { + slog.Error("failed opening publication", "error", err) + w.WriteHeader(500) + return + } + + // Make sure the asset exists in the publication + href := path.Clean(vars["asset"]) + link := publication.Find(href) + if link == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + // Get the asset from the publication + res := publication.Get(*link) + defer res.Close() + + // Get asset length in bytes + l, rerr := res.Length() + if rerr != nil { + w.WriteHeader(rerr.HTTPStatus()) + w.Write([]byte(rerr.Error())) + return + } + + // Patch mimetype where necessary + contentType := link.MediaType().String() + if sub, ok := mimeSubstitutions[contentType]; ok { + contentType = sub + } + w.Header().Set("content-type", contentType) + w.Header().Set("cache-control", "private, max-age=86400, immutable") + w.Header().Set("content-length", strconv.FormatInt(l, 10)) + w.Header().Set("access-control-allow-origin", "*") // TODO: provide options? + + var start, end int64 + // Range reading assets + rangeHeader := r.Header.Get("range") + if rangeHeader != "" { + rng, err := httprange.ParseRange(rangeHeader, l) + if err != nil { + slog.Error("failed parsing range header", "error", err) + w.WriteHeader(http.StatusLengthRequired) + return + } + if len(rng) > 1 { + slog.Error("no support for multiple read ranges") + w.WriteHeader(http.StatusNotImplemented) + return + } + if len(rng) > 0 { + w.Header().Set("content-range", rng[0].ContentRange(l)) + start = rng[0].Start + end = start + rng[0].Length - 1 + w.Header().Set("content-length", strconv.FormatInt(rng[0].Length, 10)) + } + } + if w.Header().Get("content-range") != "" { + w.WriteHeader(http.StatusPartialContent) + } + + // Stream the asset + _, rerr = res.Stream(w, start, end) + if rerr != nil { + if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) { + // Ignore client errors + return + } + + slog.Error("failed streaming asset", "error", rerr.Error()) + } + +} diff --git a/cmd/rwp/cmd/serve/cache/local.go b/cmd/rwp/cmd/serve/cache/local.go new file mode 100644 index 00000000..0ddb8899 --- /dev/null +++ b/cmd/rwp/cmd/serve/cache/local.go @@ -0,0 +1,90 @@ +package cache + +// Originally from https://github.com/go-redis/cache/blob/v8.4.3/local.go +// Modified to store interface{} instead of []byte + +import ( + "sync" + "time" + + "github.com/vmihailenco/go-tinylfu" + "golang.org/x/exp/rand" +) + +type Evictable interface { + OnEvict() +} + +type LocalCache interface { + Set(key string, data Evictable) + Get(key string) (Evictable, bool) + Del(key string) +} + +type TinyLFU struct { + mu sync.Mutex + rand *rand.Rand + lfu *tinylfu.T + ttl time.Duration + offset time.Duration +} + +var _ LocalCache = (*TinyLFU)(nil) + +func NewTinyLFU(size int, ttl time.Duration) *TinyLFU { + const maxOffset = 10 * time.Second + + offset := ttl / 10 + if offset > maxOffset { + offset = maxOffset + } + + return &TinyLFU{ + rand: rand.New(rand.NewSource(uint64(time.Now().UnixNano()))), + lfu: tinylfu.New(size, 100000), + ttl: ttl, + offset: offset, + } +} + +func (c *TinyLFU) UseRandomizedTTL(offset time.Duration) { + c.offset = offset +} + +func (c *TinyLFU) Set(key string, b Evictable) { + c.mu.Lock() + defer c.mu.Unlock() + + ttl := c.ttl + if c.offset > 0 { + ttl += time.Duration(c.rand.Int63n(int64(c.offset))) + } + + c.lfu.Set(&tinylfu.Item{ + Key: key, + Value: b, + ExpireAt: time.Now().Add(ttl), + OnEvict: func() { + b.OnEvict() + }, + }) +} + +func (c *TinyLFU) Get(key string) (Evictable, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + val, ok := c.lfu.Get(key) + if !ok { + return nil, false + } + + return val.(Evictable), true +} + +func (c *TinyLFU) Del(key string) { + c.mu.Lock() + defer c.mu.Unlock() + + c.lfu.Del(key) +} diff --git a/cmd/rwp/cmd/serve/cache/pubcache.go b/cmd/rwp/cmd/serve/cache/pubcache.go new file mode 100644 index 00000000..52a63f5a --- /dev/null +++ b/cmd/rwp/cmd/serve/cache/pubcache.go @@ -0,0 +1,22 @@ +package cache + +import ( + "github.com/readium/go-toolkit/pkg/pub" +) + +// CachedPublication implements Evictable +type CachedPublication struct { + *pub.Publication +} + +func EncapsulatePublication(pub *pub.Publication) *CachedPublication { + cp := &CachedPublication{pub} + return cp +} + +func (cp *CachedPublication) OnEvict() { + // Cleanup + if cp.Publication != nil { + cp.Publication.Close() + } +} diff --git a/cmd/rwp/cmd/serve/helpers.go b/cmd/rwp/cmd/serve/helpers.go new file mode 100644 index 00000000..6a25921a --- /dev/null +++ b/cmd/rwp/cmd/serve/helpers.go @@ -0,0 +1,64 @@ +package serve + +import ( + "strings" + + "github.com/readium/go-toolkit/pkg/manifest" +) + +var mimeSubstitutions = map[string]string{ + "application/vnd.ms-opentype": "font/otf", // Not just because it's sane, but because CF will compress it! + "application/vnd.readium.content+json": "application/vnd.readium.content+json; charset=utf-8", // Need utf-8 encoding +} + +var compressableMimes = []string{ + "application/javascript", + "application/x-javascript", + "image/x-icon", + "text/css", + "text/html", + "application/xhtml+xml", + "application/webpub+json", + "application/divina+json", + "application/vnd.readium.position-list+json", + "application/vnd.readium.content+json", + "application/audiobook+json", + "font/ttf", + "application/ttf", + "application/x-ttf", + "application/x-font-ttf", + "font/otf", + "application/otf", + "application/x-otf", + "application/vnd.ms-opentype", + "font/opentype", + "application/opentype", + "application/x-opentype", + "application/truetype", + "application/font-woff", + "font/x-woff", + "application/vnd.ms-fontobject", +} + +func makeRelative(link manifest.Link) manifest.Link { + link.Href = strings.TrimPrefix(link.Href, "/") + for i, alt := range link.Alternates { + link.Alternates[i].Href = strings.TrimPrefix(alt.Href, "/") + } + return link +} + +func conformsToAsMimetype(conformsTo manifest.Profiles) string { + mime := "application/webpub+json" + for _, profile := range conformsTo { + if profile == manifest.ProfileDivina { + mime = "application/divina+json" + } else if profile == manifest.ProfileAudiobook { + mime = "application/audiobook+json" + } else { + continue + } + break + } + return mime +} diff --git a/cmd/rwp/cmd/serve/router.go b/cmd/rwp/cmd/serve/router.go new file mode 100644 index 00000000..e3b5fd19 --- /dev/null +++ b/cmd/rwp/cmd/serve/router.go @@ -0,0 +1,47 @@ +package serve + +import ( + "net/http" + "net/http/pprof" + + "github.com/CAFxX/httpcompression" + "github.com/gorilla/mux" +) + +func (s *Server) Routes() *mux.Router { + r := mux.NewRouter() + + r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + if s.config.Debug { + r.HandleFunc("/debug/pprof/", pprof.Index) + r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + r.HandleFunc("/debug/pprof/profile", pprof.Profile) + r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + r.HandleFunc("/debug/pprof/trace", pprof.Trace) + + r.Handle("/debug/pprof/allocs", pprof.Handler("allocs")) + r.Handle("/debug/pprof/block", pprof.Handler("block")) + r.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) + r.Handle("/debug/pprof/heap", pprof.Handler("heap")) + r.Handle("/debug/pprof/mutex", pprof.Handler("mutex")) + r.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) + } + + r.HandleFunc("/list.json", s.demoList).Name("demo_list") + + pub := r.PathPrefix("/{path}").Subrouter() + // TODO: publication loading middleware with pub.Use() + pub.Use(func(h http.Handler) http.Handler { + adapter, _ := httpcompression.DefaultAdapter(httpcompression.ContentTypes(compressableMimes, false)) + return adapter(h) + }) + pub.HandleFunc("/manifest.json", s.getManifest).Name("manifest") + pub.HandleFunc("/{asset:.*}", s.getAsset).Name("asset") + + s.router = r + return r +} diff --git a/cmd/rwp/cmd/serve/server.go b/cmd/rwp/cmd/serve/server.go new file mode 100644 index 00000000..20f1b5df --- /dev/null +++ b/cmd/rwp/cmd/serve/server.go @@ -0,0 +1,32 @@ +package serve + +import ( + "time" + + "github.com/gorilla/mux" + "github.com/readium/go-toolkit/cmd/rwp/cmd/serve/cache" + "github.com/readium/go-toolkit/pkg/streamer" +) + +type ServerConfig struct { + Debug bool + BaseDirectory string + JSONIndent string + InferA11yMetadata streamer.InferA11yMetadata +} + +type Server struct { + config ServerConfig + router *mux.Router + lfu *cache.TinyLFU +} + +const MaxCachedPublicationAmount = 10 +const MaxCachedPublicationTTL = time.Second * time.Duration(600) + +func NewServer(config ServerConfig) *Server { + return &Server{ + config: config, + lfu: cache.NewTinyLFU(MaxCachedPublicationAmount, MaxCachedPublicationTTL), + } +} diff --git a/go.mod b/go.mod index a8797d41..660112ce 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module github.com/readium/go-toolkit go 1.21 require ( + github.com/CAFxX/httpcompression v0.0.9 github.com/agext/regexp v1.3.0 github.com/andybalholm/cascadia v1.3.2 github.com/deckarep/golang-set v1.7.1 github.com/gorilla/mux v1.7.4 + github.com/gotd/contrib v0.20.0 github.com/opds-community/libopds2-go v0.0.0-20170628075933-9c163cf60f6e github.com/pdfcpu/pdfcpu v0.5.0 github.com/pkg/errors v0.9.1 @@ -16,25 +18,31 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.8.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.9.0 github.com/trimmer-io/go-xmp v1.0.0 github.com/urfave/negroni v1.0.0 - golang.org/x/net v0.10.0 - golang.org/x/text v0.12.0 + github.com/vmihailenco/go-tinylfu v0.2.2 + github.com/zeebo/xxh3 v1.0.2 + golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 + golang.org/x/net v0.23.0 + golang.org/x/text v0.14.0 ) require ( + github.com/andybalholm/brotli v1.0.5 // indirect github.com/antchfx/xpath v1.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hhrutter/lzw v1.0.0 // indirect github.com/hhrutter/tiff v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/magiconair/properties v1.8.5 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/afero v1.6.0 // indirect @@ -42,8 +50,8 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect golang.org/x/image v0.11.0 // indirect - golang.org/x/sys v0.8.0 // indirect - gopkg.in/ini.v1 v1.62.0 // indirect + golang.org/x/sys v0.19.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3e78ec78..345a44cc 100644 --- a/go.sum +++ b/go.sum @@ -39,8 +39,12 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg= +github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM= github.com/agext/regexp v1.3.0 h1:6+9tp+S41TU48gFNV47bX+pp1q7WahGofw6JccmsCDs= github.com/agext/regexp v1.3.0/go.mod h1:6phv1gViOJXWcTfpxOi9VMS+MaSAo+SUDf7do3ur1HA= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/antchfx/xpath v1.2.1 h1:qhp4EW6aCOVr5XIkT+l6LJ9ck/JsUH/yyauNgTQkBF8= @@ -52,6 +56,9 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -114,6 +121,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k= +github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -147,10 +156,10 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= -github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gotd/contrib v0.20.0 h1:1Wc4+HMQiIKYQuGHVwVksIx152HFTP6B5n88dDe0ZYw= +github.com/gotd/contrib v0.20.0/go.mod h1:P6o8W4niqhDPHLA0U+SA/L7l3BQHYLULpeHfRSePn9o= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -184,16 +193,23 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -206,8 +222,9 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -218,6 +235,8 @@ github.com/pdfcpu/pdfcpu v0.5.0 h1:F3wC4bwPbaJM+RPgm1D0Q4SAUwxElw7BhwNvL3iPgDo= github.com/pdfcpu/pdfcpu v0.5.0/go.mod h1:UPcHdWcMw1V6Bo5tcWHd3jZfkG8cwUwrJkQOlB6o+7g= github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -232,14 +251,14 @@ github.com/relvacode/iso8601 v1.1.0 h1:2nV8sp0eOjpoKQ2vD3xSDygsjAx37NHG2UlZiCkDH github.com/relvacode/iso8601 v1.1.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= @@ -254,25 +273,40 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/trimmer-io/go-xmp v1.0.0 h1:zY8bolSga5kOjBAaHS6hrdxLgEoYuT875xTy0QDwZWs= github.com/trimmer-io/go-xmp v1.0.0/go.mod h1:Aaptr9sp1lLv7UnCAdQ+gSHZyY2miYaKmcNVj7HRBwA= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= +github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= +github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= +github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= @@ -304,6 +338,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= @@ -373,8 +409,8 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -448,8 +484,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -464,8 +500,9 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -632,8 +669,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=