From 5c66ab857f70e6d6d37166f12546969ef55e278f Mon Sep 17 00:00:00 2001 From: Storm Timmermans Date: Wed, 2 Mar 2022 12:11:13 +0100 Subject: [PATCH] feat(triggers): a-train (#115) Co-authored-by: l3uddz --- .github/workflows/build.yml | 7 +- .goreleaser.yml | 11 ++ README.md | 236 ++++++++++++------------ cmd/autoscan/health.go | 9 - cmd/autoscan/main.go | 65 +------ cmd/autoscan/router.go | 114 ++++++++++++ go.mod | 40 ++-- go.sum | 205 ++++++++++++++++---- triggers/a_train/a_train.go | 123 ++++++++++++ triggers/a_train/a_train_test.go | 155 ++++++++++++++++ triggers/a_train/testdata/full.json | 10 + triggers/a_train/testdata/modified.json | 8 + triggers/middleware.go | 77 -------- 13 files changed, 752 insertions(+), 308 deletions(-) delete mode 100644 cmd/autoscan/health.go create mode 100644 cmd/autoscan/router.go create mode 100644 triggers/a_train/a_train.go create mode 100644 triggers/a_train/a_train_test.go create mode 100644 triggers/a_train/testdata/full.json create mode 100644 triggers/a_train/testdata/modified.json delete mode 100644 triggers/middleware.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f18acc3..cef6427a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,8 +13,9 @@ jobs: steps: # dependencies - name: goreleaser - run: | - curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sudo sh -s -- -b /usr/local/bin + uses: goreleaser/goreleaser-action@v2 + with: + install-only: true - name: qemu uses: docker/setup-qemu-action@v1 @@ -32,7 +33,7 @@ jobs: - name: go uses: actions/setup-go@v1 with: - go-version: 1.16 + go-version: 1.17 - name: go info run: | diff --git a/.goreleaser.yml b/.goreleaser.yml index 9c766f1e..ae045d32 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -9,6 +9,7 @@ builds: goos: - linux - darwin + - freebsd main: ./cmd/autoscan goarch: - amd64 @@ -23,6 +24,16 @@ builds: - -X "main.Timestamp={{ .Timestamp }}" flags: - -trimpath + ignore: + - goos: freebsd + goarch: arm64 + - goos: freebsd + goarch: arm + +# MacOS Universal Binaries +universal_binaries: + - + replace: true # Archive archives: diff --git a/README.md b/README.md index 6562c5c6..2624afb0 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ Wait, what happened to [Plex Autoscan](https://github.com/l3uddz/plex_autoscan)? Well, Autoscan is a rewrite of the original Plex Autoscan written in the Go language. In addition, this rewrite introduces a more modular approach and should be easy to extend in the future. -## Table of contents - -- [Installing autoscan](#installing-autoscan) -- [Introduction](#introduction) - - [Rewriting paths](#rewriting-paths) - - [Triggers](#triggers) - - [Processor](#processor) - - [Targets](#targets) - - [Full config file](#full-config-file) -- [Other installation options](#other-installation-options) - - [Docker](#docker) +## Comparison to Plex Autoscan + +- [A-Train](https://github.com/m-rots/a-train/pkgs/container/a-train), Autoscan's Google Drive integration, only supports Shared Drives and requires Service Account authentication. +- A-Train does not support RClone Crypt remotes. +- Autoscan does not rely on manual trash deletion when connected to Plex. Therefore, you should re-enable the `Empty trash automatically after every scan` setting in Plex. + +Autoscan also improves upon [Plex Autoscan](https://github.com/l3uddz/plex_autoscan) by adding the following features: + +- Autoscan supports Plex music libraries. +- Autoscan adds additional support for Emby and Jellyfin. +- Autoscan can send _scans_ to multiple Plex, Emby and Jellyfin servers. ## Installing autoscan @@ -36,7 +36,7 @@ To start autoscan, simply run `./autoscan`. If you want autoscan to be globally If you need to debug certain Autoscan behaviour, either add the `-v` flag for debug mode or the `-vv` flag for trace mode to get even more details about internal behaviour. -## Introduction +## Overview Autoscan is split into three distinct modules: @@ -101,24 +101,26 @@ Let's take a look at the journey of the path `/tv/Westworld/Season 1/s01e01.mkv` This should be all that's needed to get you going. Good luck! -### Triggers +## Triggers Triggers are the 'input' of Autoscan. They translate incoming data into a common data format called the Scan. -Autoscan supports two kinds of triggers: +Autoscan currently supports the following triggers: + +- [A-Train](https://github.com/m-rots/a-train/pkgs/container/a-train): The official Google Drive trigger for Autoscan. \ + _A-Train is [available separately](https://github.com/m-rots/a-train/pkgs/container/a-train)._ -- Daemon processes. - These triggers run in the background and fetch resources based on a cron schedule or in real-time. \ +- Inotify: Listens for changes on the file system. \ + **This should not be used on top of RClone mounts.** \ *Bugs may still exist.* -- Webhooks. - These triggers expose HTTP handlers which can be added to the trigger's software. +- Manual: When you want to scan a path manually. -Each trigger consists of at least: +- The -arrs: Lidarr, Sonarr and Radarr. \ + Webhook support for Lidarr, Sonarrr and Radarr. -- A unique identifier: think of Drive IDs and HTTP routes. \ - *Webhooks use /triggers/ + their name to uniquely identify themselves.* +All triggers support: - Trigger-wide priority: higher priorities are processed sooner. \ *Defaults to 0.* @@ -126,32 +128,34 @@ Each trigger consists of at least: - RegExp-based rewriting rules: translate a path given by the trigger to a path on the local file system. \ *If the paths are identical between the trigger and the local file system, then the `rewrite` field should be ignored.* -#### Daemons - -Daemons run in the background and continuously fetch new changes based on a [cron expression](https://crontab.guru). - -The following daemons are currently provided by Autoscan: - -- Google Drive (Bernard) -- Inotify - -#### Webhooks +### A-Train -Webhooks, also known as HTTPTriggers internally, process HTTP requests on their exposed endpoints. -They should be tailor-made for the software they plan to support. +Autoscan can monitor Google Drive through [A-Train](https://github.com/m-rots/a-train/pkgs/container/a-train). A-Train is a stand-alone tool created by the Autoscan developers and is officially part of the Autoscan project. -Each instance of a webhook exposes a route which is added to Autoscan's main router. +The A-Train trigger configuration is not required, as Autoscan automatically listens for A-Train requests. However, to configure global and drive-specific rewrite rules, you could add A-Train to your config: -If one wants to configure a HTTPTrigger with multiple distinct configurations, then these configurations MUST provide a field called `Name` which uniquely identifies the trigger. -The name field is then used to create the route: `/triggers/:name`. - -The following webhooks are currently provided by Autoscan: +```yaml +triggers: + a-train: + priority: 5 + rewrite: # Global rewrites + - from: ^/Media/ + to: /mnt/unionfs/Media/ + # Drives only need to be given when Drive-specific rewrites are used + drives: + - id: 0A1xxxxxxxxxUk9PVA # The ID of Shared Drive #1 + rewrite: # Drive-specific rewrite (has priority over global rewrite) + - from: ^/TV/ + to: /mnt/unionfs/TV/ + - id: 0A2xxxxxxxxxUk9PVA # The ID of Shared Drive #2 + rewrite: # Drive-specific rewrite (has priority over global rewrite) + - from: ^/Movies/ + to: /mnt/unionfs/Movies/ +``` -- Sonarr -- Radarr -- Lidarr +### Manual -#### Manual Webhook +**Note: You can visit `/triggers/manual` within a browser to manually submit requests** Autoscan also supports a `manual` webhook for custom scripts or for software which is not supported by Autoscan directly. The manual endpoint is available at `/triggers/manual`. @@ -167,9 +171,41 @@ curl --request POST \ --header 'Authorization: Basic aGVsbG8gdGhlcmU6Z2VuZXJhbCBrZW5vYmk=' ``` -**Note: You can visit `/triggers/manual` within a browser to manually submit requests** +### The -arrs + +If one wants to configure a HTTPTrigger with multiple distinct configurations, then these configurations MUST provide a field called `Name` which uniquely identifies the trigger. +The name field is then used to create the route: `/triggers/:name`. + +The following -arrs are currently provided by Autoscan: + +- Lidarr +- Radarr +- Sonarr + +#### Connecting the -arrs + +To add your webhook to Sonarr, Radarr or Lidarr, do: + +1. Open the `settings` page in Sonarr/Radarr/Lidarr +2. Select the tab `connect` +3. Click on the big plus sign +4. Select `webhook` +5. Use `Autoscan` as name (or whatever you prefer) +6. Select `On Import` and `On Upgrade` +7. Set the URL to Autoscan's URL and add `/triggers/:name` where name is the name set in the trigger's config. +8. Optional: set username and password. -#### Configuration +#### The latest events + +Autoscan also supports the following events in the latest versions of Radarr and Sonarr: +- `Rename` +- `On Movie Delete` and `On Series Delete` +- `On Movie File Delete` and `On Episode File Delete` + +We are not 100% sure whether these three events cover all the possible file system interactions. +So for now, please do keep using Bernard or the Inotify trigger to fetch all scans. + +### Configuration A snippet of the `config.yml` file showcasing what is possible. You can mix and match exactly the way you like: @@ -191,24 +227,17 @@ triggers: - from: ^/Media/ to: /mnt/unionfs/Media/ - bernard: - - account: service-account.json - cron: "*/5 * * * *" # every five minutes (the "" are important) - priority: 0 - drives: - - id: Shared Drive 1 - - id: Shared Drive 2 - - # rewrite drive to the local filesystem - rewrite: - - from: ^/Media/ - to: /mnt/unionfs/Media/ - - # filter with regular expressions - include: - - ^/mnt/unionfs/Media/ - exclude: - - '\.srt$' + a-train: + priority: 5 + rewrite: # Global rewrites + - from: ^/Media/ + to: /mnt/unionfs/Media/ + # Drives only need to be given when Drive-specific rewrites are used + drives: + - id: 0A1xxxxxxxxxUk9PVA # The ID of Shared Drive #1 + rewrite: # Drive-specific rewrite (has priority over global rewrite) + - from: ^/TV/ + to: /mnt/unionfs/TV/ inotify: - priority: 0 @@ -228,6 +257,16 @@ triggers: paths: - path: /mnt/local/Media + lidarr: + - name: lidarr # /triggers/lidarr + priority: 1 + + radarr: + - name: radarr # /triggers/radarr + priority: 2 + - name: radarr4k # /triggers/radarr4k + priority: 5 + sonarr: - name: sonarr-docker # /triggers/sonarr-docker priority: 2 @@ -237,41 +276,9 @@ triggers: rewrite: - from: /tv/ to: /mnt/unionfs/Media/TV/ - - radarr: - - name: radarr # /triggers/radarr - priority: 2 - - name: radarr4k # /triggers/radarr4k - priority: 5 - lidarr: - - name: lidarr # /triggers/lidarr - priority: 1 ``` -#### Connecting the -arrs - -To add your webhook to Sonarr, Radarr or Lidarr, do: - -1. Open the `settings` page in Sonarr/Radarr/Lidarr -2. Select the tab `connect` -3. Click on the big plus sign -4. Select `webhook` -5. Use `Autoscan` as name (or whatever you prefer) -6. Select `On Import` and `On Upgrade` -7. Set the URL to Autoscan's URL and add `/triggers/:name` where name is the name set in the trigger's config. -8. Optional: set username and password. - -##### Experimental support for more events - -Autoscan also supports the following events in the latest versions of Radarr and Sonarr: -- `Rename` -- `On Movie Delete` and `On Series Delete` -- `On Movie File Delete` and `On Episode File Delete` - -We are not 100% sure whether these three events cover all the possible file system interactions. -So for now, please do keep using Bernard or the Inotify trigger to fetch all scans. - -### Processor +## Processor Triggers pass the Scans they receive to the processor. The processor then saves the Scans to its datastore. @@ -283,7 +290,7 @@ It will always group files belonging to the same folder together and it waits un When all files are older than the minimum age, then the processor will call all the configured targets in parallel to request a folder scan. -#### Anchor files +### Anchor files To prevent the processor from calling targets when a remote mount is offline, you can define a list of so called `anchor files`. These anchor files do not have any special properties and often have no content. @@ -297,14 +304,14 @@ Each remote mount MUST have its own anchor file and its own name for that anchor In addition, make sure to define the 'merged' path to the file and not the remote mount path. This helps check whether the union-software is working correctly as well. -#### Minimum age +### Minimum age Autoscan does not check whether scan requests received by triggers exist on the file system. Therefore, to make sure a file exists before it reaches the targets, you should set a minimum age. The minimum age delays the scan from being send to the targets after it has been added to the queue by a trigger. The default minimum age is set at 10 minutes to prevent common synchronisation issues. -#### Customising the processor +### Customising the processor The processor allows you to set the minimum age of a Scan. In addition, you can also define a list of anchor files. @@ -343,7 +350,7 @@ Scan stats will print the following information at a configured interval: - Scans processed - Scans remaining -### Targets +## Targets While collecting Scans is fun and all, they need to have a final destination. Targets are these final destinations and are given Scans from the processor, one batch at a time. @@ -355,7 +362,7 @@ Autoscan currently supports the following targets: - Jellyfin - Autoscan -#### Plex +### Plex Autoscan replaces Plex's default behaviour of updating the Plex library automatically. Therefore, it is advised to turn off Plex's `Update my library automatically` feature. @@ -378,7 +385,7 @@ There are a couple of things to take note of in the config: - Token. We need a Plex API Token to make requests on your behalf. [This article](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/) should help you out. - Rewrite. If Plex is not running on the host OS, but in a Docker container (or Autoscan is running in a Docker container), then you need to rewrite paths accordingly. Check out our [rewriting section](#rewriting-paths) for more info. -#### Emby +### Emby While Emby provides much better behaviour out of the box than Plex, it still might be useful to use Autoscan for even better performance. @@ -399,7 +406,7 @@ targets: *It's a bit out of date, but I'm sure you will manage!* - Rewrite. If Emby is not running on the host OS, but in a Docker container (or Autoscan is running in a Docker container), then you need to rewrite paths accordingly. Check out our [rewriting section](#rewriting-paths) for more info. -#### Jellyfin +### Jellyfin While Jellyfin provides much better behaviour out of the box than Plex, it still might be useful to use Autoscan for even better performance. @@ -420,7 +427,7 @@ targets: *It's a bit out of date, but I'm sure you will manage!* - Rewrite. If Jellyfin is not running on the host OS, but in a Docker container (or Autoscan is running in a Docker container), then you need to rewrite paths accordingly. Check out our [rewriting section](#rewriting-paths) for more info. -#### Autoscan +### Autoscan You can also send scan requests to other instances of autoscan! @@ -435,7 +442,7 @@ targets: to: /mnt/nfs/Media/ # path accessible by the remote autoscan instance (if applicable) ``` -### Full config file +## Full config file With the examples given in the [triggers](#triggers), [processor](#processor) and [targets](#targets) sections, here is what your full config file *could* look like: @@ -452,7 +459,7 @@ anchors: # <- triggers -> -# Optionally, protect your webhooks with authentication +# Protect your webhooks with authentication authentication: username: hello there password: general kenobi @@ -461,6 +468,16 @@ authentication: port: 3030 triggers: + lidarr: + - name: lidarr # /triggers/lidarr + priority: 1 + + radarr: + - name: radarr # /triggers/radarr + priority: 2 + - name: radarr4k # /triggers/radarr4k + priority: 5 + sonarr: - name: sonarr-docker # /triggers/sonarr-docker priority: 2 @@ -471,15 +488,6 @@ triggers: - from: /tv/ to: /mnt/unionfs/Media/TV/ - radarr: - - name: radarr # /triggers/radarr - priority: 2 - - name: radarr4k # /triggers/radarr4k - priority: 5 - lidarr: - - name: lidarr # /triggers/lidarr - priority: 1 - # <- targets -> targets: diff --git a/cmd/autoscan/health.go b/cmd/autoscan/health.go deleted file mode 100644 index 098c526b..00000000 --- a/cmd/autoscan/health.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "net/http" -) - -func healthHandler(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(http.StatusOK) -} diff --git a/cmd/autoscan/main.go b/cmd/autoscan/main.go index 7733c838..e97793b0 100644 --- a/cmd/autoscan/main.go +++ b/cmd/autoscan/main.go @@ -23,7 +23,7 @@ import ( "github.com/cloudbox/autoscan/targets/emby" "github.com/cloudbox/autoscan/targets/jellyfin" "github.com/cloudbox/autoscan/targets/plex" - "github.com/cloudbox/autoscan/triggers" + "github.com/cloudbox/autoscan/triggers/a_train" "github.com/cloudbox/autoscan/triggers/bernard" "github.com/cloudbox/autoscan/triggers/inotify" "github.com/cloudbox/autoscan/triggers/lidarr" @@ -52,6 +52,7 @@ type config struct { // autoscan.HTTPTrigger Triggers struct { Manual manual.Config `yaml:"manual"` + ATrain a_train.Config `yaml:"a-train"` Bernard []bernard.Config `yaml:"bernard"` Inotify []inotify.Config `yaml:"inotify"` Lidarr []lidarr.Config `yaml:"lidarr"` @@ -209,8 +210,7 @@ func main() { Strs("anchors", c.Anchors). Msg("Initialised processor") - // Set authentication. If none and running at least one webhook -> warn user. - authHandler := triggers.WithAuth(c.Auth.Username, c.Auth.Password) + // Check authentication. If no auth -> warn user. if c.Auth.Username == "" || c.Auth.Password == "" { log.Warn().Msg("Webhooks running without authentication") } @@ -241,65 +241,12 @@ func main() { } // http triggers - mux := http.NewServeMux() - mux.HandleFunc("/health", healthHandler) - - manualTrigger, err := manual.New(c.Triggers.Manual) - if err != nil { - log.Fatal(). - Err(err). - Str("trigger", "manual"). - Msg("Failed initialising trigger") - } - - logHandler := triggers.WithLogger(autoscan.GetLogger(c.Triggers.Manual.Verbosity)) - mux.Handle("/triggers/manual", logHandler(authHandler(manualTrigger(proc.Add)))) - - for _, t := range c.Triggers.Lidarr { - trigger, err := lidarr.New(t) - if err != nil { - log.Fatal(). - Err(err). - Str("trigger", t.Name). - Msg("Failed initialising trigger") - } - - logHandler := triggers.WithLogger(autoscan.GetLogger(t.Verbosity)) - mux.Handle("/triggers/"+t.Name, logHandler(authHandler(trigger(proc.Add)))) - } - - for _, t := range c.Triggers.Radarr { - trigger, err := radarr.New(t) - if err != nil { - log.Fatal(). - Err(err). - Str("trigger", t.Name). - Msg("Failed initialising trigger") - } - - logHandler := triggers.WithLogger(autoscan.GetLogger(t.Verbosity)) - mux.Handle("/triggers/"+t.Name, logHandler(authHandler(trigger(proc.Add)))) - } - - for _, t := range c.Triggers.Sonarr { - trigger, err := sonarr.New(t) - if err != nil { - log.Fatal(). - Err(err). - Str("trigger", t.Name). - Msg("Failed initialising trigger") - } - - logHandler := triggers.WithLogger(autoscan.GetLogger(t.Verbosity)) - mux.Handle("/triggers/"+t.Name, logHandler(authHandler(trigger(proc.Add)))) - } + router := getRouter(c, proc) go func() { log.Info().Msgf("Starting server on port %d", c.Port) - if err := http.ListenAndServe(fmt.Sprintf(":%d", c.Port), mux); err != nil { - log.Fatal(). - Err(err). - Msg("Failed starting web server") + if err := http.ListenAndServe(fmt.Sprintf(":%d", c.Port), router); err != nil { + log.Fatal().Err(err).Msg("Failed starting web server") } }() diff --git a/cmd/autoscan/router.go b/cmd/autoscan/router.go new file mode 100644 index 00000000..72d7f78d --- /dev/null +++ b/cmd/autoscan/router.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/rs/zerolog/hlog" + "github.com/rs/zerolog/log" + + "github.com/cloudbox/autoscan/processor" + "github.com/cloudbox/autoscan/triggers/a_train" + "github.com/cloudbox/autoscan/triggers/lidarr" + "github.com/cloudbox/autoscan/triggers/manual" + "github.com/cloudbox/autoscan/triggers/radarr" + "github.com/cloudbox/autoscan/triggers/sonarr" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +func pattern(name string) string { + return fmt.Sprintf("/%s", name) +} + +func createCredentials(c config) map[string]string { + creds := make(map[string]string) + creds[c.Auth.Username] = c.Auth.Password + return creds +} + +func getRouter(c config, proc *processor.Processor) chi.Router { + r := chi.NewRouter() + + // Middleware + r.Use(middleware.Recoverer) + + // Logging-related middleware + r.Use(hlog.NewHandler(log.Logger)) + r.Use(hlog.RequestIDHandler("id", "request-id")) + r.Use(hlog.URLHandler("url")) + r.Use(hlog.MethodHandler("method")) + r.Use(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { + hlog.FromRequest(r).Debug(). + Int("status", status). + Dur("duration", duration). + Msg("Request processed") + })) + + // Health check + r.Get("/health", healthHandler) + + // HTTP-Triggers + r.Route("/triggers", func(r chi.Router) { + // Use Basic Auth middleware if username and password are set. + if c.Auth.Username != "" && c.Auth.Password != "" { + r.Use(middleware.BasicAuth("Autoscan 1.x", createCredentials(c))) + } + + // A-Train HTTP-trigger + r.Route("/a-train", func(r chi.Router) { + trigger, err := a_train.New(c.Triggers.ATrain) + if err != nil { + log.Fatal().Err(err).Str("trigger", "a-train").Msg("Failed initialising trigger") + } + + r.Post("/{drive}", trigger(proc.Add).ServeHTTP) + }) + + // Mixed-style Manual HTTP-trigger + r.Route("/manual", func(r chi.Router) { + trigger, err := manual.New(c.Triggers.Manual) + if err != nil { + log.Fatal().Err(err).Str("trigger", "manual").Msg("Failed initialising trigger") + } + + r.HandleFunc("/", trigger(proc.Add).ServeHTTP) + }) + + // OLD-style HTTP-triggers. Can be converted to the /{trigger}/{id} format in a 2.0 release. + for _, t := range c.Triggers.Lidarr { + trigger, err := lidarr.New(t) + if err != nil { + log.Fatal().Err(err).Str("trigger", t.Name).Msg("Failed initialising trigger") + } + + r.Post(pattern(t.Name), trigger(proc.Add).ServeHTTP) + } + + for _, t := range c.Triggers.Radarr { + trigger, err := radarr.New(t) + if err != nil { + log.Fatal().Err(err).Str("trigger", t.Name).Msg("Failed initialising trigger") + } + + r.Post(pattern(t.Name), trigger(proc.Add).ServeHTTP) + } + + for _, t := range c.Triggers.Sonarr { + trigger, err := sonarr.New(t) + if err != nil { + log.Fatal().Err(err).Str("trigger", t.Name).Msg("Failed initialising trigger") + } + + r.Post(pattern(t.Name), trigger(proc.Add).ServeHTTP) + } + }) + + return r +} + +// Other Handlers +func healthHandler(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) +} diff --git a/go.mod b/go.mod index a728eb93..f758cd01 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,44 @@ module github.com/cloudbox/autoscan -go 1.16 +go 1.17 require ( github.com/BurntSushi/toml v0.3.1 // indirect - github.com/alecthomas/kong v0.2.16 - github.com/fsnotify/fsnotify v1.4.9 - github.com/justinas/alice v1.2.0 + github.com/alecthomas/kong v0.4.1 + github.com/fsnotify/fsnotify v1.5.1 + github.com/go-chi/chi/v5 v5.0.7 github.com/l3uddz/bernard v0.5.1 github.com/m-rots/stubbs v1.1.0 github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/natefinch/lumberjack v2.0.0+incompatible - github.com/oriser/regroup v0.0.0-20201024192559-010c434ff8f3 + github.com/oriser/regroup v0.0.0-20210730155327-fca8d7531263 github.com/pkg/errors v0.9.1 // indirect github.com/robfig/cron/v3 v3.0.1 - github.com/rs/zerolog v1.20.0 - golang.org/x/mod v0.4.2 // indirect + github.com/rs/zerolog v1.26.1 + golang.org/x/mod v0.5.1 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18 // indirect - golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba - golang.org/x/tools v0.1.0 // indirect + golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect + golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 + golang.org/x/tools v0.1.9 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 - modernc.org/cc/v3 v3.32.0 // indirect - modernc.org/sqlite v1.10.0 + modernc.org/cc/v3 v3.35.22 // indirect + modernc.org/sqlite v1.14.7 modernc.org/strutil v1.1.1 // indirect ) + +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/rs/xid v1.3.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/ccgo/v3 v3.15.13 // indirect + modernc.org/libc v1.14.5 // indirect + modernc.org/mathutil v1.4.1 // indirect + modernc.org/memory v1.0.5 // indirect + modernc.org/opt v0.1.1 // indirect + modernc.org/token v1.0.0 // indirect +) diff --git a/go.sum b/go.sum index 668c4a60..83b9519a 100644 --- a/go.sum +++ b/go.sum @@ -2,20 +2,25 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/alecthomas/kong v0.2.9/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= github.com/alecthomas/kong v0.2.11/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ= -github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/alecthomas/kong v0.4.1 h1:0sFnMts+ijOiFuSHsMB9MlDi3NGINBkx9KIw1/gcuDw= +github.com/alecthomas/kong v0.4.1/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.5.3 h1:x95R7cp+rSeeqAMI2knLtQ0DKlaBhv2NrtrOvafPHRo= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= -github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/l3uddz/bernard v0.5.1 h1:PdkmJn44q4dmix1riBkrvnpb2LVvhyRLwIRhWoc05bo= @@ -23,15 +28,18 @@ github.com/l3uddz/bernard v0.5.1/go.mod h1:J2ad7LeQl+6Nxc8sGJ8HtQIsv9c9bk2jHxsBt github.com/m-rots/stubbs v1.0.0/go.mod h1:iDS6z2oonw2UMo2l0S1WTPJ9git7FWU4YEo6fq7F2WU= github.com/m-rots/stubbs v1.1.0 h1:QR1LHxFYPasju/sEO0KLmI5/RADF70CW3ZtisCs7XrQ= github.com/m-rots/stubbs v1.1.0/go.mod h1:Ive+DY/P1EikQ644M3tuyvsO/7ohPLnmEru2L+6hbVw= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= -github.com/oriser/regroup v0.0.0-20201024192559-010c434ff8f3 h1:SIHa1eb8CJDqFu8potgn0n7grPG2cf2WEKekk/nSz5g= github.com/oriser/regroup v0.0.0-20201024192559-010c434ff8f3/go.mod h1:odkMeLkWS8G6+WP2z3Pn2vkzhPSvBtFhAUYTKXAtZMQ= +github.com/oriser/regroup v0.0.0-20210730155327-fca8d7531263 h1:Qd1Ml+uEhpesT8Og0ysEhu5+DGhbhW+qxjapH8t1Kvs= +github.com/oriser/regroup v0.0.0-20210730155327-fca8d7531263/go.mod h1:odkMeLkWS8G6+WP2z3Pn2vkzhPSvBtFhAUYTKXAtZMQ= 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= @@ -41,48 +49,64 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6O github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= -github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc= +github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 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/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18 h1:jxr7/dEo+rR29uEBoLSWJ1tRHCFAMwFbGUU9nRqzpds= -golang.org/x/sys v0.0.0-20210313110737-8e9fff1a3a18/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -94,34 +118,147 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXL gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.31.5-0.20210308123301-7a3e9dab9009/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= -modernc.org/cc/v3 v3.32.0 h1:f7CzEixf9/9Rv7KksLyFHgOT1BfpbPLpwD/SPAHu3pg= -modernc.org/cc/v3 v3.32.0/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= -modernc.org/ccgo/v3 v3.9.0 h1:JbcEIqjw4Agf+0g3Tc85YvfYqkkFOv6xBwS4zkfqSoA= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.20/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.22 h1:BzShpwCAP7TWzFppM4k2t03RhXhgYqaibROWkrWq7lE= +modernc.org/cc/v3 v3.35.22/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= modernc.org/ccgo/v3 v3.9.0/go.mod h1:nQbgkn8mwzPdp4mm6BT6+p85ugQ7FrGgIcYaE7nSrpY= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU= +modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko= +modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA= +modernc.org/ccgo/v3 v3.13.1/go.mod h1:aBYVOUfIlcSnrsRVU8VRS35y2DIfpgkmVkYZ0tpIXi4= +modernc.org/ccgo/v3 v3.15.1/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0= +modernc.org/ccgo/v3 v3.15.9/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0= +modernc.org/ccgo/v3 v3.15.10/go.mod h1:wQKxoFn0ynxMuCLfFD09c8XPUCc8obfchoVR9Cn0fI8= +modernc.org/ccgo/v3 v3.15.12/go.mod h1:VFePOWoCd8uDGRJpq/zfJ29D0EVzMSyID8LCMWYbX6I= +modernc.org/ccgo/v3 v3.15.13 h1:hqlCzNJTXLrhS70y1PqWckrF9x1btSQRC7JFuQcBg5c= +modernc.org/ccgo/v3 v3.15.13/go.mod h1:QHtvdpeODlXjdK3tsbpyK+7U9JV4PQsrPGIbtmc0KfY= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/ccorpus v1.11.4 h1:YOmQBBzE8GC/puUx76D5j/gJYIZQsydrh6VMJVfXF0M= +modernc.org/ccorpus v1.11.4/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/libc v1.8.0 h1:Pp4uv9g0csgBMpGPABKtkieF6O5MGhfGo6ZiOdlYfR8= modernc.org/libc v1.8.0/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ= +modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.12.0/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ= +modernc.org/libc v1.14.1/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk= +modernc.org/libc v1.14.2/go.mod h1:MX1GBLnRLNdvmK9azU9LCxZ5lMyhrbEMK8rG3X/Fe34= +modernc.org/libc v1.14.3/go.mod h1:GPIvQVOVPizzlqyRX3l756/3ppsAgg1QgPxjr5Q4agQ= +modernc.org/libc v1.14.5 h1:DAHvwGoVRDZs5iJXnX9RJrgXSsorupCWmJ2ac964Owk= +modernc.org/libc v1.14.5/go.mod h1:2PJHINagVxO4QW/5OQdRrvMYo+bm5ClpUFfyXCYl9ak= modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.2.2 h1:+yFk8hBprV+4c0U9GjFtL+dV3N8hOJ8JCituQcMShFY= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.0.4 h1:utMBrFcpnQDdNsmM6asmyH/FM9TqLPS7XF7otpJmrwM= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.10.0 h1:0QNqx4EzfZzNEG13sFbS/L+egh0X5WXSckHrxHkySX8= modernc.org/sqlite v1.10.0/go.mod h1:PGzq6qlhyYjL6uVbSgS6WoF7ZopTW/sI7+7p+mb4ZVU= +modernc.org/sqlite v1.14.7 h1:A+6rGjtRQbt9SORXfV+hUyXOP3mDf7J5uz+EES/CNPE= +modernc.org/sqlite v1.14.7/go.mod h1:yiCvMv3HblGmzENNIaNtFhfaNIwcla4u2JQEwJPzfEc= modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/tcl v1.5.0 h1:euZSUNfE0Fd4W8VqXI1Ly1v7fqDJoBuAV88Ea+SnaSs= modernc.org/tcl v1.5.0/go.mod h1:gb57hj4pO8fRrK54zveIfFXBaMHK3SKJNWcmRw1cRzc= +modernc.org/tcl v1.11.0 h1:B/zzEYjINeaki38KcIqdQRQx7W3WE7TkrlTwGnbm2II= +modernc.org/tcl v1.11.0/go.mod h1:zsTUpbQ+NxQEjOjCUlImDLPv1sG8Ww0qp66ZvyOxCgw= modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= -modernc.org/z v1.0.1 h1:WyIDpEpAIx4Hel6q/Pcgj/VhaQV5XPJ2I6ryIYbjnpc= modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= +modernc.org/z v1.3.0 h1:4RWULo1Nvaq5ZBhbLe74u8p6tV4Mmm0ZrPBXYPm/xjM= +modernc.org/z v1.3.0/go.mod h1:+mvgLH814oDjtATDdT3rs84JnUIpkvAF5B8AVkNlE2g= diff --git a/triggers/a_train/a_train.go b/triggers/a_train/a_train.go new file mode 100644 index 00000000..2ddc283c --- /dev/null +++ b/triggers/a_train/a_train.go @@ -0,0 +1,123 @@ +package a_train + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/cloudbox/autoscan" + "github.com/go-chi/chi/v5" + "github.com/rs/zerolog/hlog" +) + +type Drive struct { + ID string `yaml:"id"` + Rewrite []autoscan.Rewrite `yaml:"rewrite"` +} + +type Config struct { + Drives []Drive `yaml:"drives"` + Priority int `yaml:"priority"` + Rewrite []autoscan.Rewrite `yaml:"rewrite"` + Verbosity string `yaml:"verbosity"` +} + +type ATrainRewriter = func(drive string, input string) string + +// // New creates an autoscan-compatible HTTP Trigger for A-Train webhooks. +func New(c Config) (autoscan.HTTPTrigger, error) { + rewrites := make(map[string]autoscan.Rewriter) + for _, drive := range c.Drives { + rewriter, err := autoscan.NewRewriter(append(drive.Rewrite, c.Rewrite...)) + if err != nil { + return nil, err + } + + rewrites[drive.ID] = rewriter + } + + globalRewriter, err := autoscan.NewRewriter(c.Rewrite) + if err != nil { + return nil, err + } + + rewriter := func(drive string, input string) string { + driveRewriter, ok := rewrites[drive] + if !ok { + return globalRewriter(input) + } + + return driveRewriter(input) + } + + trigger := func(callback autoscan.ProcessorFunc) http.Handler { + return handler{ + callback: callback, + priority: c.Priority, + rewrite: rewriter, + } + } + + return trigger, nil +} + +type handler struct { + priority int + rewrite ATrainRewriter + callback autoscan.ProcessorFunc +} + +type atrainEvent struct { + Created []string + Deleted []string +} + +func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + var err error + rlog := hlog.FromRequest(r) + + drive := chi.URLParam(r, "drive") + + event := new(atrainEvent) + err = json.NewDecoder(r.Body).Decode(event) + if err != nil { + rlog.Error().Err(err).Msg("Failed decoding request") + rw.WriteHeader(http.StatusBadRequest) + return + } + + rlog.Trace().Interface("event", event).Msg("Received JSON body") + + scans := make([]autoscan.Scan, 0) + + for _, path := range event.Created { + scans = append(scans, autoscan.Scan{ + Folder: h.rewrite(drive, path), + Priority: h.priority, + Time: now(), + }) + } + + for _, path := range event.Deleted { + scans = append(scans, autoscan.Scan{ + Folder: h.rewrite(drive, path), + Priority: h.priority, + Time: now(), + }) + } + + err = h.callback(scans...) + if err != nil { + rlog.Error().Err(err).Msg("Processor could not process scans") + rw.WriteHeader(http.StatusInternalServerError) + return + } + + for _, scan := range scans { + rlog.Info().Str("path", scan.Folder).Msg("Scan moved to processor") + } + + rw.WriteHeader(http.StatusOK) +} + +var now = time.Now diff --git a/triggers/a_train/a_train_test.go b/triggers/a_train/a_train_test.go new file mode 100644 index 00000000..0f2a3d5b --- /dev/null +++ b/triggers/a_train/a_train_test.go @@ -0,0 +1,155 @@ +package a_train + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "os" + "reflect" + "testing" + "time" + + "github.com/cloudbox/autoscan" + "github.com/go-chi/chi/v5" +) + +func TestHandler(t *testing.T) { + type Given struct { + Config Config + ID string + Fixture string + } + + type Expected struct { + Scans []autoscan.Scan + StatusCode int + } + + type Test struct { + Name string + Given Given + Expected Expected + } + + standardConfig := Config{ + Drives: []Drive{{ + ID: "1234VA", + Rewrite: []autoscan.Rewrite{{ + From: "^/TV/*", + To: "/mnt/unionfs/Media/TV/$1", + }}, + }}, + Priority: 5, + Rewrite: []autoscan.Rewrite{{ + From: "^/Movies/*", + To: "/mnt/unionfs/Media/Movies/$1", + }}, + } + + currentTime := time.Now() + now = func() time.Time { + return currentTime + } + + var testCases = []Test{ + { + "Multiple created and deleted", + Given{ + Config: standardConfig, + ID: "1234VA", + Fixture: "testdata/full.json", + }, + Expected{ + StatusCode: 200, + Scans: []autoscan.Scan{ + { + Folder: "/mnt/unionfs/Media/Movies/Interstellar (2014)", + Priority: 5, + Time: currentTime, + }, + { + Folder: "/mnt/unionfs/Media/TV/Legion/Season 1", + Priority: 5, + Time: currentTime, + }, + { + Folder: "/mnt/unionfs/Media/Movies/Wonder Woman 1984 (2020)", + Priority: 5, + Time: currentTime, + }, + { + Folder: "/mnt/unionfs/Media/Movies/Mortal Kombat (2021)", + Priority: 5, + Time: currentTime, + }, + }, + }, + }, + { + "Duplicate (parent is given in both create and delete)", + Given{ + Config: standardConfig, + ID: "anotherVA", + Fixture: "testdata/modified.json", + }, + Expected{ + StatusCode: 200, + Scans: []autoscan.Scan{ + { + Folder: "/TV/Legion/Season 1", + Priority: 5, + Time: currentTime, + }, + { + Folder: "/TV/Legion/Season 1", + Priority: 5, + Time: currentTime, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + callback := func(scans ...autoscan.Scan) error { + if !reflect.DeepEqual(tc.Expected.Scans, scans) { + t.Log(scans) + t.Log(tc.Expected.Scans) + t.Errorf("Scans do not equal") + return errors.New("Scans do not equal") + } + + return nil + } + + trigger, err := New(tc.Given.Config) + if err != nil { + t.Fatalf("Could not create A-Train Trigger: %v", err) + } + + r := chi.NewRouter() + r.Post("/triggers/a-train/{drive}", trigger(callback).ServeHTTP) + + server := httptest.NewServer(r) + defer server.Close() + + request, err := os.Open(tc.Given.Fixture) + if err != nil { + t.Fatalf("Could not open the fixture: %s", tc.Given.Fixture) + } + + url := fmt.Sprintf("%s/triggers/a-train/%s", server.URL, tc.Given.ID) + res, err := http.Post(url, "application/json", request) + if err != nil { + t.Fatalf("Request failed: %v", err) + } + + defer res.Body.Close() + if res.StatusCode != tc.Expected.StatusCode { + t.Errorf("Status codes do not match: %d vs %d", res.StatusCode, tc.Expected.StatusCode) + } + }) + } +} diff --git a/triggers/a_train/testdata/full.json b/triggers/a_train/testdata/full.json new file mode 100644 index 00000000..4f80c93f --- /dev/null +++ b/triggers/a_train/testdata/full.json @@ -0,0 +1,10 @@ +{ + "created": [ + "/Movies/Interstellar (2014)", + "/TV/Legion/Season 1" + ], + "deleted": [ + "/Movies/Wonder Woman 1984 (2020)", + "/Movies/Mortal Kombat (2021)" + ] +} \ No newline at end of file diff --git a/triggers/a_train/testdata/modified.json b/triggers/a_train/testdata/modified.json new file mode 100644 index 00000000..d7c5fe27 --- /dev/null +++ b/triggers/a_train/testdata/modified.json @@ -0,0 +1,8 @@ +{ + "created": [ + "/TV/Legion/Season 1" + ], + "deleted": [ + "/TV/Legion/Season 1" + ] +} \ No newline at end of file diff --git a/triggers/middleware.go b/triggers/middleware.go deleted file mode 100644 index 6beb17f5..00000000 --- a/triggers/middleware.go +++ /dev/null @@ -1,77 +0,0 @@ -package triggers - -import ( - "crypto/subtle" - "net/http" - "time" - - "github.com/justinas/alice" - "github.com/rs/zerolog" - "github.com/rs/zerolog/hlog" -) - -func detailHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger := zerolog.Ctx(r.Context()) - logger.UpdateContext(func(c zerolog.Context) zerolog.Context { - return c. - Str("method", r.Method). - Str("url", r.URL.Path) - }) - - next.ServeHTTP(w, r) - }) -} - -func WithLogger(logger zerolog.Logger) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - c := alice.New() - c = c.Append(hlog.NewHandler(logger)) - c = c.Append(hlog.RequestIDHandler("request_id", "Request-Id")) - c = c.Append(detailHandler) - - c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { - hlog.FromRequest(r).Debug(). - Int("status", status). - Dur("duration", duration). - Msg("Request processed") - })) - - return c.Then(next) - } -} - -func WithAuth(username, password string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - // Don't check for auth if username or password is missing. - if username == "" || password == "" { - return next - } - - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - l := hlog.FromRequest(r) - - user, pass, ok := r.BasicAuth() - if !ok || user == "" || pass == "" { - // prompt auth dialog - rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - rw.WriteHeader(http.StatusUnauthorized) - return - } - - // validate credentials - if ok && - subtle.ConstantTimeCompare([]byte(user), []byte(username)) == 1 && - subtle.ConstantTimeCompare([]byte(pass), []byte(password)) == 1 { - l.Trace().Msg("Successful authentication") - next.ServeHTTP(rw, r) - return - } - - // invalid credentials - l.Warn().Msg("Invalid authentication") - rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) - rw.WriteHeader(http.StatusUnauthorized) - }) - } -}