Compare commits

...

10 Commits

Author SHA1 Message Date
wpetit 1029cc8f0f feat: load zip bundle from io.ReaderAt
arcad/edge/pipeline/head This commit looks good Details
2024-05-02 09:43:22 +02:00
wpetit 0f673671b8 feat: generalize defaut user middleware 2024-05-02 09:42:56 +02:00
wpetit 6aec6da078 feat: use github.com/wlynxg/anet instead of net
See https://github.com/golang/go/issues/40569
2024-05-02 09:40:55 +02:00
wpetit 2fbc7186c0 Merge pull request 'Intégration du support `arcast` dans le module `cast`' (#25) from arcast into master
arcad/edge/pipeline/head This commit looks good Details
Reviewed-on: #25
2024-01-12 14:04:29 +01:00
wpetit 7633ae0419 feat(module): arcast integration
arcad/edge/pipeline/head Build started... Details
arcad/edge/pipeline/pr-master Build started... Details
2024-01-12 14:02:53 +01:00
wpetit 335b34625b feat(module): generalizing casting process
arcad/edge/pipeline/head This commit looks good Details
2024-01-12 12:11:41 +01:00
wpetit 776dbba5b0 Merge pull request 'feat(storage): add revision number to documents' (#24) from document-revision into master
arcad/edge/pipeline/head This commit looks good Details
Reviewed-on: #24
2024-01-12 10:07:46 +01:00
wpetit 8f9428b3f3 feat(storage): add revision number to documents
arcad/edge/pipeline/head Build started... Details
arcad/edge/pipeline/pr-master Build started... Details
ref #13
2024-01-12 10:05:47 +01:00
wpetit a268759d33 Merge pull request 'Implémentation d'un système de cache type LFU pour le `BlobStore`' (#23) from lfu-cache into master
arcad/edge/pipeline/head This commit looks good Details
Reviewed-on: #23
2024-01-10 13:22:51 +01:00
wpetit a276b92a03 feat: implement lfu based cache strategy
arcad/edge/pipeline/head This commit looks good Details
arcad/edge/pipeline/pr-master This commit looks good Details
2024-01-10 13:16:52 +01:00
67 changed files with 3251 additions and 1081 deletions

View File

@ -1,4 +1,4 @@
RUN_APP_ARGS=""
#EDGE_DOCUMENTSTORE_DSN="rpc://localhost:3001/documentstore?tenant=local&appId=%APPID%"
#EDGE_BLOBSTORE_DSN="cache://localhost:3001/blobstore?driver=rpc&tenant=local&appId=%APPID%"
#EDGE_BLOBSTORE_DSN="cache://localhost:3001/blobstore?driver=rpc&tenant=local&appId=%APPID%&blobCacheStoreType=fs&blobCacheStoreBaseDir=data/cache/%APPID%&blobCacheSize=64MB"
#EDGE_SHARESTORE_DSN="rpc://localhost:3001/sharestore?tenant=local"

View File

@ -39,6 +39,7 @@ import (
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"github.com/wlynxg/anet"
_ "embed"
@ -52,6 +53,10 @@ import (
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
// Register casting device supported types
_ "forge.cadoles.com/arcad/edge/pkg/module/cast/arcast"
_ "forge.cadoles.com/arcad/edge/pkg/module/cast/chromecast"
"forge.cadoles.com/arcad/edge/pkg/storage/share"
)
@ -247,7 +252,7 @@ func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN,
fetchModule.Mount(),
),
appHTTP.WithHTTPMiddlewares(
authModuleMiddleware.AnonymousUser(key, jwa.HS256),
authModuleMiddleware.DefaultUser(key, jwa.HS256, authModuleMiddleware.WithAnonymousUser()),
),
)
if err := handler.Load(ctx, bundle); err != nil {
@ -356,13 +361,13 @@ func findMatchingDeviceAddress(ctx context.Context, from string, defaultAddr str
return defaultAddr, nil
}
ifaces, err := net.Interfaces()
ifaces, err := anet.Interfaces()
if err != nil {
return "", errors.WithStack(err)
}
for _, ifa := range ifaces {
addrs, err := ifa.Addrs()
addrs, err := anet.InterfaceAddrsByInterface(&ifa)
if err != nil {
logger.Error(
ctx, "could not retrieve iface adresses",

View File

@ -1,6 +1,9 @@
package cast
import (
"context"
"time"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
@ -21,16 +24,25 @@ func LoadURLCommand() *cli.Command {
Aliases: []string{"u"},
Required: true,
},
&cli.DurationFlag{
Name: "timeout",
Aliases: []string{"t"},
Value: 10 * time.Second,
},
},
Action: func(ctx *cli.Context) error {
device := ctx.String("device")
url := ctx.String("url")
timeout := ctx.Duration("timeout")
if err := cast.StopCast(ctx.Context, device); err != nil {
timeoutCtx, cancel := context.WithTimeout(ctx.Context, timeout)
defer cancel()
if err := cast.StopCast(timeoutCtx, device); err != nil {
return errors.WithStack(err)
}
if err := cast.LoadURL(ctx.Context, device, url); err != nil {
if err := cast.LoadURL(timeoutCtx, device, url); err != nil {
return errors.WithStack(err)
}

View File

@ -2,6 +2,10 @@ package cast
import (
"github.com/urfave/cli/v2"
// Register casting device supported types
_ "forge.cadoles.com/arcad/edge/pkg/module/cast/arcast"
_ "forge.cadoles.com/arcad/edge/pkg/module/cast/chromecast"
)
func Root() *cli.Command {
@ -11,6 +15,8 @@ func Root() *cli.Command {
Subcommands: []*cli.Command{
ScanCommand(),
LoadURLCommand(),
StatusCommand(),
StopCastCommand(),
},
}
}

View File

@ -18,7 +18,7 @@ func ScanCommand() *cli.Command {
&cli.DurationFlag{
Name: "timeout",
Aliases: []string{"t"},
Value: 30 * time.Second,
Value: 10 * time.Second,
},
},
Action: func(ctx *cli.Context) error {
@ -32,8 +32,8 @@ func ScanCommand() *cli.Command {
log.Fatalf("%+v", errors.WithStack(err))
}
for dev := range devices {
log.Printf("[DEVICE] %s %s %s:%d", dev.UUID, dev.Name, dev.Host.String(), dev.Port)
for _, dev := range devices {
log.Printf("[DEVICE] %s %s %s %s:%d", dev.DeviceID(), dev.DeviceType(), dev.DeviceName(), dev.DeviceHost().String(), dev.DevicePort())
}
return nil

View File

@ -0,0 +1,46 @@
package cast
import (
"context"
"log"
"time"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func StatusCommand() *cli.Command {
return &cli.Command{
Name: "status",
Usage: "Retrieve casting device status",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "device",
Aliases: []string{"d"},
Required: true,
},
&cli.DurationFlag{
Name: "timeout",
Aliases: []string{"t"},
Value: 10 * time.Second,
},
},
Action: func(ctx *cli.Context) error {
device := ctx.String("device")
timeout := ctx.Duration("timeout")
getStatusCtx, cancel := context.WithTimeout(ctx.Context, timeout)
defer cancel()
status, err := cast.GetStatus(getStatusCtx, device)
if err != nil {
return errors.WithStack(err)
}
log.Printf("[STATUS] %s %s", status.Title(), status.State())
return nil
},
}
}

View File

@ -0,0 +1,42 @@
package cast
import (
"context"
"time"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func StopCastCommand() *cli.Command {
return &cli.Command{
Name: "stop-cast",
Usage: "Stop casting process on device",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "device",
Aliases: []string{"d"},
Required: true,
},
&cli.DurationFlag{
Name: "timeout",
Aliases: []string{"t"},
Value: 10 * time.Second,
},
},
Action: func(ctx *cli.Context) error {
device := ctx.String("device")
timeout := ctx.Duration("timeout")
timeoutCtx, cancel := context.WithTimeout(ctx.Context, timeout)
defer cancel()
if err := cast.StopCast(timeoutCtx, device); err != nil {
return errors.WithStack(err)
}
return nil
},
}
}

60
go.mod
View File

@ -1,81 +1,89 @@
module forge.cadoles.com/arcad/edge
go 1.21
go 1.21.4
toolchain go1.21.5
require (
github.com/allegro/bigcache/v3 v3.1.0
forge.cadoles.com/arcad/arcast v0.0.0-20231220090835-5d0311b7315d
github.com/getsentry/sentry-go v0.25.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/hashicorp/mdns v1.0.5
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf
github.com/jackc/puddle/v2 v2.2.1
github.com/keegancsmith/rpc v1.3.0
github.com/klauspost/compress v1.16.6
github.com/lestrrat-go/jwx/v2 v2.0.8
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/ulikunitz/xz v0.5.11
github.com/wlynxg/anet v0.0.1
go.uber.org/goleak v1.3.0
modernc.org/sqlite v1.20.4
)
require (
cloud.google.com/go v0.75.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/charmbracelet/lipgloss v0.7.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1 // indirect
github.com/jaevor/go-nanoid v1.3.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.0 // indirect
github.com/miekg/dns v1.1.53 // indirect
golang.org/x/sync v0.1.0 // indirect
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 // indirect
google.golang.org/grpc v1.35.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
golang.org/x/sync v0.5.0 // indirect
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
)
require (
cdr.dev/slog v1.4.0
github.com/alecthomas/chroma v0.7.0 // indirect
cdr.dev/slog v1.6.1
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/davecgh/go-spew v1.1.1
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.1
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/chi/v5 v5.0.10
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.2 // indirect
github.com/igm/sockjs-go/v3 v3.0.2
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0
github.com/oklog/ulid/v2 v2.1.0
github.com/orcaman/concurrent-map v1.0.0
github.com/pkg/errors v0.9.1
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/urfave/cli/v2 v2.24.3
github.com/urfave/cli/v2 v2.26.0
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
gitlab.com/wpetit/goweb v0.0.0-20231019192040-4c72331a7648
go.opencensus.io v0.22.5 // indirect
golang.org/x/crypto v0.10.0
golang.org/x/mod v0.10.0
golang.org/x/net v0.11.0
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.8.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07
golang.org/x/crypto v0.16.0
golang.org/x/mod v0.14.0
golang.org/x/net v0.19.0
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.16.1 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gopkg.in/yaml.v2 v2.4.0
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect

598
go.sum
View File

@ -1,76 +1,26 @@
cdr.dev/slog v1.4.0 h1:tLXQJ/hZ5Q051h0MBHSd2Ha8xzdXj7CjtzmG/8dUvUk=
cdr.dev/slog v1.4.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Bornholm/bigcache v0.0.0-20231201111725-1ddf51584cad h1:PTOf0L/YjiVis5LYzJmi7WqttJ/h/DU6h06aJ24Kpbg=
github.com/Bornholm/bigcache v0.0.0-20231201111725-1ddf51584cad/go.mod h1:+q+mA6jGsjfsZ2HzhVSk38qDbX2/ZBJ7Yyciv75Ruo0=
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/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw=
github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
cdr.dev/slog v1.6.1 h1:IQjWZD0x6//sfv5n+qEhbu3wBkmtBQY5DILXNvMaIv4=
cdr.dev/slog v1.6.1/go.mod h1:eHEYQLaZvxnIAXC+XdTSNLb/kgA/X2RVSF72v5wsxEI=
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
forge.cadoles.com/arcad/arcast v0.0.0-20231220090835-5d0311b7315d h1:SPDaDDF5StoprDqop8j8zozs8xK32EEWnUHLccWplKM=
forge.cadoles.com/arcad/arcast v0.0.0-20231220090835-5d0311b7315d/go.mod h1:QR8p4kUScWBcTQ0dE/gR+2ndpKOx77mIDSYGqSF1gms=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 h1:JW4WZlqyaNWUUahfr7MigeDW6jmtam5cTzzo1lwsFhE=
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692/go.mod h1:Au0ipPuCBA7zsOC61SnyrYetm8VT3vo1UJtwHeYke44=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.0.1-0.20160907170601-6d212800a42e/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
@ -78,8 +28,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
@ -93,26 +41,18 @@ github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097 h1:WsLyDk8yHsVT
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097/go.mod h1:0tlktQL7yHfYEtjcRGi/eiOkbDR5XF7gyFFvbC5//E0=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
@ -123,101 +63,36 @@ github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY=
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1 h1:cNb52t5fkWv8ZiicKWnc2eZnhsCCoH7WmRBMIbMp04Q=
github.com/grandcat/zeroconf v1.0.1-0.20230119201135-e4f60f8407b1/go.mod h1:I6CSXU4zCGL08JOk9NbcT0ofAgnIkS/fVXbYzfSoDic=
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE=
github.com/igm/sockjs-go/v3 v3.0.2/go.mod h1:UqchsOjeagIBFHvd+RZpLaVRbCwGilEC08EDHsD1jYE=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
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/jaevor/go-nanoid v1.3.0 h1:nD+iepesZS6pr3uOVf20vR9GdGgJW1HPaR46gtrxzkg=
github.com/jaevor/go-nanoid v1.3.0/go.mod h1:SI+jFaPuddYkqkVQoNGHs81navCtH388TcrH0RqFKgY=
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/keegancsmith/rpc v1.3.0 h1:wGWOpjcNrZaY8GDYZJfvyxmlLljm3YQWF+p918DXtDk=
github.com/keegancsmith/rpc v1.3.0/go.mod h1:6O2xnOGjPyvIPbvp0MdrOe5r6cu1GZ4JoTzpzDhWeo0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -242,412 +117,149 @@ github.com/lestrrat-go/jwx/v2 v2.0.8 h1:jCFT8oc0hEDVjgUgsBy1F9cbjsjAVZSXNi7JaU9H
github.com/lestrrat-go/jwx/v2 v2.0.8/go.mod h1:zLxnyv9rTlEvOUHbc48FAfIL8iYu2hHvIRaTFGc8mT0=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
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/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
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.1.5-0.20160925220609-976c720a22c8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/wlynxg/anet v0.0.1 h1:VbkEEgHxPSrRQSiyRd0pmrbcEQAEU2TTb8fb4DmSYoQ=
github.com/wlynxg/anet v0.0.1/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/wpetit/goweb v0.0.0-20231019192040-4c72331a7648 h1:t2UQmCmUoElIBBuVTqxqo8DcTJA/exQ/Q7XycfLqCZo=
gitlab.com/wpetit/goweb v0.0.0-20231019192040-4c72331a7648/go.mod h1:WdxGjM3HJWgBkUa4TwaTXUqY2BnRKlNSyUIv1aF4jxk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07 h1:0V95X1cBpdj5zyOe6oGtn/BQHlRpV8WlL3eTs3jaxiA=
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07/go.mod h1:Nfr7aZPiSN6biFumhiHbh9k8A3rKQRzR+o0bVtv78UY=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
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/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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20161013035702-8b4af36cd21a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
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=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 h1:PYBmACG+YEv8uQPW0r1kJj8tR+gkF0UWq7iFdUezwEw=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -663,13 +275,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
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.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
@ -698,6 +303,3 @@ modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -16,7 +16,7 @@ func TestBundle(t *testing.T) {
bundles := []Bundle{
NewDirectoryBundle("testdata/bundle"),
NewTarBundle("testdata/bundle.tar.gz"),
NewZipBundle("testdata/bundle.zip"),
Must(NewZipBundleFromPath("testdata/bundle.zip")),
}
for _, b := range bundles {

View File

@ -54,7 +54,7 @@ func matchArchivePattern(archivePath string) (Bundle, error) {
}
if matches {
return NewZipBundle(archivePath), nil
return NewZipBundleFromPath(archivePath)
}
matches, err = filepath.Match(fmt.Sprintf("*.%s", ExtZim), base)

View File

@ -13,15 +13,10 @@ import (
)
type ZipBundle struct {
archivePath string
reader *zip.Reader
}
func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
reader, err := b.openArchive()
if err != nil {
return nil, nil, err
}
ctx := logger.With(
context.Background(),
logger.F("filename", filename),
@ -29,7 +24,7 @@ func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
logger.Debug(ctx, "opening file")
f, err := reader.Open(filename)
f, err := b.reader.Open(filename)
if err != nil {
return nil, nil, errors.WithStack(err)
}
@ -43,21 +38,10 @@ func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
}
func (b *ZipBundle) Dir(dirname string) ([]os.FileInfo, error) {
reader, err := b.openArchive()
if err != nil {
return nil, err
}
defer func() {
if err := reader.Close(); err != nil {
panic(errors.WithStack(err))
}
}()
files := make([]os.FileInfo, 0)
ctx := context.Background()
for _, f := range reader.File {
for _, f := range b.reader.File {
if !strings.HasPrefix(f.Name, dirname) {
continue
}
@ -82,17 +66,35 @@ func (b *ZipBundle) Dir(dirname string) ([]os.FileInfo, error) {
return files, nil
}
func (b *ZipBundle) openArchive() (*zip.ReadCloser, error) {
zr, err := zip.OpenReader(b.archivePath)
func NewZipBundleFromReader(reader io.ReaderAt, size int64) (*ZipBundle, error) {
zipReader, err := zip.NewReader(reader, size)
if err != nil {
return nil, errors.Wrapf(err, "could not decompress '%v'", b.archivePath)
return nil, errors.WithStack(err)
}
return zr, nil
}
func NewZipBundle(archivePath string) *ZipBundle {
return &ZipBundle{
archivePath: archivePath,
}
reader: zipReader,
}, nil
}
func NewZipBundleFromPath(filename string) (*ZipBundle, error) {
file, err := os.Open(filename)
if err != nil {
return nil, errors.WithStack(err)
}
stat, err := file.Stat()
if err != nil {
return nil, errors.WithStack(err)
}
return NewZipBundleFromReader(file, stat.Size())
}
func Must(bundle Bundle, err error) Bundle {
if err != nil {
panic(errors.WithStack(err))
}
return bundle
}

View File

@ -5,101 +5,45 @@ import (
"fmt"
"math/big"
"net/http"
"time"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const AnonIssuer = "anon"
func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler {
opts := defaultAnonymousUserOptions()
for _, fn := range funcs {
fn(opts)
}
func WithAnonymousUser(funcs ...DefaultUserOptionFunc) DefaultUserOptionFunc {
return func(opts *DefaultUserOptions) {
opts.GetSubject = getAnonymousSubject
opts.GetPreferredUsername = getAnonymousPreferredUsername
opts.Issuer = AnonIssuer
return func(next http.Handler) http.Handler {
handler := func(w http.ResponseWriter, r *http.Request) {
rawToken, err := jwtutil.FindRawToken(r, jwtutil.WithFinders(
jwtutil.FindTokenFromAuthorizationHeader,
jwtutil.FindTokenFromQueryString(auth.CookieName),
jwtutil.FindTokenFromCookie(auth.CookieName),
))
// If request already has a raw token, we do nothing
if rawToken != "" && err == nil {
next.ServeHTTP(w, r)
return
}
ctx := r.Context()
uuid, err := uuid.NewUUID()
if err != nil {
logger.Error(ctx, "could not generate uuid for anonymous user", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String())
preferredUsername, err := generateRandomPreferredUsername(8)
if err != nil {
logger.Error(ctx, "could not generate preferred username for anonymous user", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
claims := map[string]any{
auth.ClaimSubject: subject,
auth.ClaimIssuer: AnonIssuer,
auth.ClaimPreferredUsername: preferredUsername,
auth.ClaimEdgeRole: opts.Role,
auth.ClaimEdgeEntrypoint: opts.Entrypoint,
auth.ClaimEdgeTenant: opts.Tenant,
}
token, err := jwtutil.SignedToken(key, signingAlgorithm, claims)
if err != nil {
logger.Error(ctx, "could not generate signed token", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookieDomain, err := opts.GetCookieDomain(r)
if err != nil {
logger.Error(ctx, "could not retrieve cookie domain", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookie := http.Cookie{
Name: auth.CookieName,
Value: string(token),
Domain: cookieDomain,
HttpOnly: false,
Expires: time.Now().Add(opts.CookieDuration),
Path: "/",
}
http.SetCookie(w, &cookie)
next.ServeHTTP(w, r)
for _, fn := range funcs {
fn(opts)
}
return http.HandlerFunc(handler)
}
}
func getAnonymousSubject(r *http.Request) (string, error) {
uuid, err := uuid.NewUUID()
if err != nil {
return "", errors.Wrap(err, "could not generate uuid for anonymous user")
}
subject := fmt.Sprintf("%s-%s", AnonIssuer, uuid.String())
return subject, nil
}
func getAnonymousPreferredUsername(r *http.Request) (string, error) {
preferredUsername, err := generateRandomPreferredUsername(8)
if err != nil {
return "", errors.Wrap(err, "could not generate preferred username for anonymous user")
}
return preferredUsername, nil
}
func generateRandomPreferredUsername(size int) (string, error) {
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
max := big.NewInt(int64(len(letters)))

View File

@ -0,0 +1,94 @@
package middleware
import (
"net/http"
"time"
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func DefaultUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...DefaultUserOptionFunc) func(next http.Handler) http.Handler {
opts := defaultUserOptions()
for _, fn := range funcs {
fn(opts)
}
return func(next http.Handler) http.Handler {
handler := func(w http.ResponseWriter, r *http.Request) {
rawToken, err := jwtutil.FindRawToken(r, jwtutil.WithFinders(
jwtutil.FindTokenFromAuthorizationHeader,
jwtutil.FindTokenFromQueryString(auth.CookieName),
jwtutil.FindTokenFromCookie(auth.CookieName),
))
// If request already has a raw token, we do nothing
if rawToken != "" && err == nil {
next.ServeHTTP(w, r)
return
}
ctx := r.Context()
subject, err := opts.GetSubject(r)
if err != nil {
logger.Error(ctx, "could not retrieve user subject", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
preferredUsername, err := opts.GetPreferredUsername(r)
if err != nil {
logger.Error(ctx, "could not retrieve user preferred username", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
claims := map[string]any{
auth.ClaimSubject: subject,
auth.ClaimIssuer: opts.Issuer,
auth.ClaimPreferredUsername: preferredUsername,
auth.ClaimEdgeRole: opts.Role,
auth.ClaimEdgeEntrypoint: opts.Entrypoint,
auth.ClaimEdgeTenant: opts.Tenant,
}
token, err := jwtutil.SignedToken(key, signingAlgorithm, claims)
if err != nil {
logger.Error(ctx, "could not generate signed token", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookieDomain, err := opts.GetCookieDomain(r)
if err != nil {
logger.Error(ctx, "could not retrieve cookie domain", logger.CapturedE(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookie := http.Cookie{
Name: auth.CookieName,
Value: string(token),
Domain: cookieDomain,
HttpOnly: false,
Expires: time.Now().Add(opts.CookieDuration),
Path: "/",
}
http.SetCookie(w, &cookie)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(handler)
}
}

View File

@ -11,47 +11,52 @@ func defaultGetCookieDomain(r *http.Request) (string, error) {
return "", nil
}
type AnonymousUserOptions struct {
GetCookieDomain GetCookieDomainFunc
CookieDuration time.Duration
Tenant string
Entrypoint string
Role string
type DefaultUserOptions struct {
GetCookieDomain GetCookieDomainFunc
CookieDuration time.Duration
Tenant string
Entrypoint string
Role string
Issuer string
GetPreferredUsername func(r *http.Request) (string, error)
GetSubject func(r *http.Request) (string, error)
}
type AnonymousUserOptionFunc func(*AnonymousUserOptions)
type DefaultUserOptionFunc func(opts *DefaultUserOptions)
func defaultAnonymousUserOptions() *AnonymousUserOptions {
return &AnonymousUserOptions{
GetCookieDomain: defaultGetCookieDomain,
CookieDuration: 24 * time.Hour,
Tenant: "",
Entrypoint: "",
Role: "",
func defaultUserOptions() *DefaultUserOptions {
return &DefaultUserOptions{
GetCookieDomain: defaultGetCookieDomain,
CookieDuration: 24 * time.Hour,
Tenant: "",
Entrypoint: "",
Role: "",
GetSubject: getAnonymousSubject,
GetPreferredUsername: getAnonymousPreferredUsername,
}
}
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) AnonymousUserOptionFunc {
return func(opts *AnonymousUserOptions) {
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) DefaultUserOptionFunc {
return func(opts *DefaultUserOptions) {
opts.GetCookieDomain = getCookieDomain
opts.CookieDuration = duration
}
}
func WithTenant(tenant string) AnonymousUserOptionFunc {
return func(opts *AnonymousUserOptions) {
func WithTenant(tenant string) DefaultUserOptionFunc {
return func(opts *DefaultUserOptions) {
opts.Tenant = tenant
}
}
func WithEntrypoint(entrypoint string) AnonymousUserOptionFunc {
return func(opts *AnonymousUserOptions) {
func WithEntrypoint(entrypoint string) DefaultUserOptionFunc {
return func(opts *DefaultUserOptions) {
opts.Entrypoint = entrypoint
}
}
func WithRole(role string) AnonymousUserOptionFunc {
return func(opts *AnonymousUserOptions) {
func WithRole(role string) DefaultUserOptionFunc {
return func(opts *DefaultUserOptions) {
opts.Role = role
}
}

View File

@ -0,0 +1,49 @@
package arcast
import (
"context"
"forge.cadoles.com/arcad/arcast/pkg/client"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
)
type Client struct {
client *client.Client
addr string
}
// Close implements cast.Client.
func (c *Client) Close() error {
return nil
}
// Load implements cast.Client.
func (c *Client) Load(ctx context.Context, url string) error {
if _, err := c.client.Cast(ctx, c.addr, url); err != nil {
return errors.WithStack(err)
}
return nil
}
// Status implements cast.Client.
func (c *Client) Status(ctx context.Context) (cast.DeviceStatus, error) {
status, err := c.client.Status(ctx, c.addr)
if err != nil {
return nil, errors.WithStack(err)
}
return &DeviceStatus{status}, nil
}
// Unload implements cast.Client.
func (c *Client) Unload(ctx context.Context) error {
if _, err := c.client.Reset(ctx, c.addr); err != nil {
return errors.WithStack(err)
}
return nil
}
var _ cast.Client = &Client{}

View File

@ -0,0 +1,57 @@
package arcast
import (
"context"
"net"
"forge.cadoles.com/arcad/arcast/pkg/client"
"forge.cadoles.com/arcad/arcast/pkg/server"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const DeviceTypeArcast cast.DeviceType = "arcast"
type Device struct {
player *client.Player
}
// DeviceHost implements cast.Device.
func (d *Device) DeviceHost() net.IP {
rawPreferredIP, err := server.FindPreferredLocalAddress(d.player.IPs...)
if err != nil {
logger.Error(context.Background(), "could not find preferred local address", logger.CapturedE(errors.WithStack(err)))
return d.player.IPs[0]
}
preferredIP := net.ParseIP(rawPreferredIP)
if preferredIP == nil {
logger.Error(context.Background(), "could not parse device preferred ip", logger.F("rawPreferredIP", rawPreferredIP))
return d.player.IPs[0]
}
return preferredIP
}
// DeviceID implements cast.Device.
func (d *Device) DeviceID() string {
return d.player.ID
}
// DeviceName implements cast.Device.
func (d *Device) DeviceName() string {
return d.player.ID
}
// DevicePort implements cast.Device.
func (d *Device) DevicePort() int {
return d.player.Port
}
// DeviceType implements cast.Device.
func (d *Device) DeviceType() cast.DeviceType {
return DeviceTypeArcast
}
var _ cast.Device = &Device{}

View File

@ -0,0 +1,60 @@
package arcast
import (
"context"
"fmt"
"forge.cadoles.com/arcad/arcast/pkg/client"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/pkg/errors"
)
func init() {
cast.Register(DeviceTypeArcast, &Service{
client: client.New(),
})
}
type Service struct {
client *client.Client
}
// Find implements cast.Service.
func (s *Service) Find(ctx context.Context, deviceID string) (cast.Device, error) {
players, err := s.client.Scan(ctx, client.WithPlayerIDs(deviceID))
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
return nil, errors.WithStack(err)
}
if len(players) == 0 {
return nil, errors.WithStack(cast.ErrDeviceNotFound)
}
return &Device{players[0]}, nil
}
// NewClient implements cast.Service.
func (s *Service) NewClient(ctx context.Context, device cast.Device) (cast.Client, error) {
return &Client{
client: s.client,
addr: fmt.Sprintf("%s:%d", device.DeviceHost(), device.DevicePort()),
}, nil
}
// Scan implements cast.Service.
func (s *Service) Scan(ctx context.Context) ([]cast.Device, error) {
players, err := s.client.Scan(ctx)
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
return nil, errors.WithStack(err)
}
devices := make([]cast.Device, len(players))
for i, p := range players {
devices[i] = &Device{p}
}
return devices, nil
}
var _ cast.Service = &Service{}

View File

@ -0,0 +1,22 @@
package arcast
import (
"forge.cadoles.com/arcad/arcast/pkg/server"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
)
type DeviceStatus struct {
status *server.StatusResponse
}
// State implements cast.DeviceStatus.
func (s *DeviceStatus) State() string {
return s.status.Status
}
// Title implements cast.DeviceStatus.
func (s *DeviceStatus) Title() string {
return s.status.Title
}
var _ cast.DeviceStatus = &DeviceStatus{}

View File

@ -2,22 +2,13 @@ package cast
import (
"context"
"net"
"sync"
"time"
"github.com/barnybug/go-cast"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Device struct {
UUID string `goja:"uuid" json:"uuid"`
Host net.IP `goja:"host" json:"host"`
Port int `goja:"port" json:"port"`
Name string `goja:"name" json:"name"`
}
type CachedDevice struct {
Device
UpdatedAt time.Time
@ -27,66 +18,49 @@ func (d CachedDevice) Expired() bool {
return d.UpdatedAt.Add(30 * time.Minute).Before(time.Now())
}
type DeviceStatus struct {
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
}
type DeviceStatusCurrentApp struct {
ID string `goja:"id" json:"id"`
DisplayName string `goja:"displayName" json:"displayName"`
StatusText string `goja:"statusText" json:"statusText"`
}
type DeviceStatusVolume struct {
Level float64 `goja:"level" json:"level"`
Muted bool `goja:"muted" json:"muted"`
}
const (
serviceDiscoveryPollingInterval time.Duration = 500 * time.Millisecond
)
var cache sync.Map
func getCachedDevice(uuid string) (Device, bool) {
value, exists := cache.Load(uuid)
if !exists {
return Device{}, false
return nil, false
}
cachedDevice, ok := value.(CachedDevice)
if !ok {
return Device{}, false
return nil, false
}
if cachedDevice.Expired() {
return Device{}, false
return nil, false
}
return cachedDevice.Device, true
}
func cacheDevice(dev Device) {
cache.Store(dev.UUID, CachedDevice{
cache.Store(dev.DeviceID(), CachedDevice{
Device: dev,
UpdatedAt: time.Now(),
})
}
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
device, err := FindDeviceByUUID(ctx, uuid)
func getDeviceClientByID(ctx context.Context, deviceID string) (Client, error) {
device, err := findCachedDeviceByID(ctx, deviceID)
if err != nil {
return nil, errors.WithStack(err)
}
client := cast.NewClient(device.Host, device.Port)
client, err := NewClient(ctx, device)
if err != nil {
return nil, errors.WithStack(err)
}
return client, nil
}
func FindDeviceByUUID(ctx context.Context, uuid string) (Device, error) {
device, exists := getCachedDevice(uuid)
func findCachedDeviceByID(ctx context.Context, deviceID string) (Device, error) {
device, exists := getCachedDevice(deviceID)
if exists {
return device, nil
}
@ -94,18 +68,14 @@ func FindDeviceByUUID(ctx context.Context, uuid string) (Device, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
devices, err := SearchDevices(ctx)
device, err := Find(ctx, deviceID)
if err != nil {
return Device{}, nil
return nil, errors.WithStack(err)
}
for dev := range devices {
if dev.UUID == uuid {
return dev, nil
}
}
cacheDevice(device)
return Device{}, errors.Errorf("could not find device '%s'", uuid)
return device, nil
}
func ListDevices(ctx context.Context, refresh bool) ([]Device, error) {
@ -125,119 +95,74 @@ func ListDevices(ctx context.Context, refresh bool) ([]Device, error) {
return devices, nil
}
ch, err := SearchDevices(ctx)
devices, err := SearchDevices(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
for dev := range ch {
devices = append(devices, dev)
}
return devices, nil
}
var searchDevicesMutex sync.Mutex
func SearchDevices(ctx context.Context) (chan Device, error) {
service := NewService(ctx)
defer service.Stop()
func SearchDevices(ctx context.Context) ([]Device, error) {
searchDevicesMutex.Lock()
defer searchDevicesMutex.Unlock()
go func() {
searchDevicesMutex.Lock()
defer searchDevicesMutex.Unlock()
devices, err := Scan(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
logger.Error(ctx, "error while running cast service discovery", logger.CapturedE(errors.WithStack(err)))
}
}()
devices := make(chan Device)
go func() {
defer close(devices)
found := make(map[string]struct{})
LOOP:
for {
select {
case c := <-service.Found():
dev := Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
}
if _, exists := found[dev.UUID]; !exists {
devices <- dev
found[dev.UUID] = struct{}{}
}
cacheDevice(dev)
case <-ctx.Done():
break LOOP
}
}
}()
for _, device := range devices {
cacheDevice(device)
}
return devices, nil
}
var loadURLMutex sync.Mutex
func LoadURL(ctx context.Context, deviceUUID string, url string) error {
func LoadURL(ctx context.Context, deviceID string, url string) error {
loadURLMutex.Lock()
defer loadURLMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID)
client, err := getDeviceClientByID(ctx, deviceID)
if err != nil {
return errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return errors.WithStack(err)
}
defer client.Close()
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
}
}()
controller, err := client.URL(ctx)
if err != nil {
return errors.WithStack(err)
}
// Ignore context.DeadlineExceeded errors. github.com/barnybug/go-cast bug ?
if _, err := controller.LoadURL(ctx, url); err != nil && !isLoadURLContextExceeded(err) {
if err := client.Load(ctx, url); err != nil {
return errors.WithStack(err)
}
return nil
}
// False positive workaround.
func isLoadURLContextExceeded(err error) bool {
return err.Error() == "Failed to send load command: context deadline exceeded"
}
var stopCastMutex sync.Mutex
func StopCast(ctx context.Context, deviceUUID string) error {
func StopCast(ctx context.Context, deviceID string) error {
stopCastMutex.Lock()
defer stopCastMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID)
client, err := getDeviceClientByID(ctx, deviceID)
if err != nil {
return errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return errors.WithStack(err)
}
defer client.Close()
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
}
}()
if _, err := client.Receiver().QuitApp(ctx); err != nil {
if err := client.Unload(ctx); err != nil {
return errors.WithStack(err)
}
@ -246,42 +171,25 @@ func StopCast(ctx context.Context, deviceUUID string) error {
var getStatusMutex sync.Mutex
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
func GetStatus(ctx context.Context, deviceID string) (DeviceStatus, error) {
getStatusMutex.Lock()
defer getStatusMutex.Unlock()
client, err := getDeviceClientByUUID(ctx, deviceUUID)
client, err := getDeviceClientByID(ctx, deviceID)
if err != nil {
return nil, errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return nil, errors.WithStack(err)
}
defer client.Close()
defer func() {
if err := client.Close(); err != nil {
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
}
}()
ctrlStatus, err := client.Receiver().GetStatus(ctx)
status, err := client.Status(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
status := &DeviceStatus{
CurrentApp: DeviceStatusCurrentApp{
ID: "",
DisplayName: "",
StatusText: "",
},
Volume: DeviceStatusVolume{
Level: *ctrlStatus.Volume.Level,
Muted: *ctrlStatus.Volume.Muted,
},
}
if len(ctrlStatus.Applications) > 0 {
status.CurrentApp.ID = *ctrlStatus.Applications[0].AppID
status.CurrentApp.DisplayName = *ctrlStatus.Applications[0].DisplayName
status.CurrentApp.StatusText = *ctrlStatus.Applications[0].StatusText
}
return status, nil
}

View File

@ -1,15 +1,21 @@
package cast
package cast_test
import (
"context"
"fmt"
"os"
"testing"
"time"
"cdr.dev/slog"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
// Register casting device supported types
_ "forge.cadoles.com/arcad/edge/pkg/module/cast/arcast"
_ "forge.cadoles.com/arcad/edge/pkg/module/cast/chromecast"
)
func TestCastLoadURL(t *testing.T) {
@ -28,51 +34,58 @@ func TestCastLoadURL(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
devices, err := ListDevices(ctx, true)
devices, err := cast.ListDevices(ctx, true)
if err != nil {
t.Error(errors.WithStack(err))
}
t.Logf("DEVICES: %s", spew.Sdump(devices))
if e, g := 1, len(devices); e != g {
t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
if e, g := 1, len(devices); e > g {
t.Fatalf("len(devices): expected 'value >= %v', got '%v'", e, g)
}
devices, err = ListDevices(ctx, false)
devices, err = cast.ListDevices(ctx, false)
if err != nil {
t.Error(errors.WithStack(err))
}
t.Logf("CACHED DEVICES: %s", spew.Sdump(devices))
if e, g := 1, len(devices); e != g {
t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
if e, g := 1, len(devices); e > g {
t.Fatalf("len(devices): expected 'value >= %v', got '%v'", e, g)
}
dev := devices[0]
for _, device := range devices {
testName := fmt.Sprintf("%s(%s)", device.DeviceType(), device.DeviceID())
func(device cast.Device) {
t.Run(testName, func(t *testing.T) {
t.Parallel()
ctx, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel2()
ctx, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel2()
if err := LoadURL(ctx, dev.UUID, "https://go.dev"); err != nil {
t.Error(errors.WithStack(err))
}
if err := cast.LoadURL(ctx, device.DeviceID(), "https://go.dev"); err != nil {
t.Error(errors.WithStack(err))
}
ctx, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel3()
ctx, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel3()
status, err := getStatus(ctx, dev.UUID)
if err != nil {
t.Error(errors.WithStack(err))
}
status, err := cast.GetStatus(ctx, device.DeviceID())
if err != nil {
t.Error(errors.WithStack(err))
}
t.Logf("DEVICE STATUS: %s", spew.Sdump(status))
t.Logf("DEVICE STATUS: %s", spew.Sdump(status))
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel4()
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel4()
if err := StopCast(ctx, dev.UUID); err != nil {
t.Error(errors.WithStack(err))
if err := cast.StopCast(ctx, device.DeviceID()); err != nil {
t.Error(errors.WithStack(err))
}
})
}(device)
}
}

View File

@ -0,0 +1,78 @@
package chromecast
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
gcast "github.com/barnybug/go-cast"
"github.com/pkg/errors"
)
type Client struct {
client *gcast.Client
}
// Close implements cast.Client.
func (c *Client) Close() error {
c.client.Close()
return nil
}
// Load implements cast.Client.
func (c *Client) Load(ctx context.Context, url string) error {
controller, err := c.client.URL(ctx)
if err != nil {
return errors.WithStack(err)
}
// Ignore context.DeadlineExceeded errors. github.com/barnybug/go-cast bug ?
if _, err := controller.LoadURL(ctx, url); err != nil && !isLoadURLContextExceeded(err) {
return errors.WithStack(err)
}
return nil
}
// Status implements cast.Client.
func (c *Client) Status(ctx context.Context) (cast.DeviceStatus, error) {
ctrlStatus, err := c.client.Receiver().GetStatus(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
status := &DeviceStatus{
CurrentApp: DeviceStatusCurrentApp{
ID: "",
DisplayName: "",
StatusText: "",
},
Volume: DeviceStatusVolume{
Level: *ctrlStatus.Volume.Level,
Muted: *ctrlStatus.Volume.Muted,
},
}
if len(ctrlStatus.Applications) > 0 {
status.CurrentApp.ID = *ctrlStatus.Applications[0].AppID
status.CurrentApp.DisplayName = *ctrlStatus.Applications[0].DisplayName
status.CurrentApp.StatusText = *ctrlStatus.Applications[0].StatusText
}
return status, nil
}
// Unload implements cast.Client.
func (c *Client) Unload(ctx context.Context) error {
if _, err := c.client.Receiver().QuitApp(ctx); err != nil {
return errors.WithStack(err)
}
return nil
}
var _ cast.Client = &Client{}
// False positive workaround.
func isLoadURLContextExceeded(err error) bool {
return err.Error() == "Failed to send load command: context deadline exceeded"
}

View File

@ -0,0 +1,43 @@
package chromecast
import (
"net"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
)
const DeviceTypeChromecast cast.DeviceType = "chromecast"
type Device struct {
UUID string `goja:"uuid" json:"uuid"`
Host net.IP `goja:"host" json:"host"`
Port int `goja:"port" json:"port"`
Name string `goja:"name" json:"name"`
}
// DeviceHost implements cast.Device.
func (d *Device) DeviceHost() net.IP {
return d.Host
}
// DeviceID implements cast.Device.
func (d *Device) DeviceID() string {
return d.UUID
}
// DeviceName implements cast.Device.
func (d *Device) DeviceName() string {
return d.Name
}
// DevicePort implements cast.Device.
func (d *Device) DevicePort() int {
return d.Port
}
// DeviceType implements cast.Device.
func (d *Device) DeviceType() cast.DeviceType {
return DeviceTypeChromecast
}
var _ cast.Device = &Device{}

View File

@ -1,10 +1,8 @@
package cast
package chromecast
import (
"context"
"net"
"regexp"
"strconv"
"strings"
"sync"
"time"
@ -15,26 +13,31 @@ import (
"github.com/barnybug/go-cast/log"
"github.com/hashicorp/mdns"
"github.com/pkg/errors"
"github.com/wlynxg/anet"
)
type Service struct {
const (
serviceDiscoveryPollingInterval time.Duration = 500 * time.Millisecond
)
type Discovery struct {
found chan *cast.Client
entriesCh chan *mdns.ServiceEntry
stopPeriodic chan struct{}
}
func NewService(ctx context.Context) *Service {
s := &Service{
func NewDiscovery(ctx context.Context) *Discovery {
d := &Discovery{
found: make(chan *cast.Client),
entriesCh: make(chan *mdns.ServiceEntry, 10),
}
go s.listener(ctx)
return s
go d.listener(ctx)
return d
}
func (d *Service) Run(ctx context.Context, interval time.Duration) error {
func (d *Discovery) Run(ctx context.Context, interval time.Duration) error {
ifaces, err := findMulticastInterfaces(ctx)
if err != nil {
return errors.WithStack(err)
@ -82,7 +85,7 @@ func (d *Service) Run(ctx context.Context, interval time.Duration) error {
return nil
}
func (d *Service) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool) error {
func (d *Discovery) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool) error {
err := mdns.Query(&mdns.QueryParam{
Service: "_googlecast._tcp",
Domain: "local",
@ -99,7 +102,7 @@ func (d *Service) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool)
return nil
}
func (d *Service) pollInterface(ctx context.Context, iface net.Interface, interval time.Duration, disableIPv4, disableIPv6 bool) error {
func (d *Discovery) pollInterface(ctx context.Context, iface net.Interface, interval time.Duration, disableIPv4, disableIPv6 bool) error {
ticker := time.NewTicker(interval)
for {
select {
@ -118,18 +121,18 @@ func (d *Service) pollInterface(ctx context.Context, iface net.Interface, interv
}
}
func (d *Service) Stop() {
func (d *Discovery) Stop() {
if d.stopPeriodic != nil {
close(d.stopPeriodic)
d.stopPeriodic = nil
}
}
func (d *Service) Found() chan *cast.Client {
func (d *Discovery) Found() chan *cast.Client {
return d.found
}
func (d *Service) listener(ctx context.Context) {
func (d *Discovery) listener(ctx context.Context) {
for entry := range d.entriesCh {
name := strings.Split(entry.Name, "._googlecast")
// Skip everything that doesn't have googlecast in the fdqn
@ -147,28 +150,11 @@ func (d *Service) listener(ctx context.Context) {
case d.found <- client:
case <-time.After(time.Second):
case <-ctx.Done():
break
return
}
}
}
func decodeDnsEntry(text string) string {
text = strings.Replace(text, `\.`, ".", -1)
text = strings.Replace(text, `\ `, " ", -1)
re := regexp.MustCompile(`([\\][0-9][0-9][0-9])`)
text = re.ReplaceAllStringFunc(text, func(source string) string {
i, err := strconv.Atoi(source[1:])
if err != nil {
return ""
}
return string([]byte{byte(i)})
})
return text
}
func decodeTxtRecord(txt string) map[string]string {
m := make(map[string]string)
@ -192,7 +178,7 @@ func isIPv6(ip net.IP) bool {
}
func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
ifaces, err := net.Interfaces()
ifaces, err := anet.Interfaces()
if err != nil {
return nil, nil
}
@ -219,7 +205,7 @@ func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
}
func retrieveSupportedProtocols(iface net.Interface) (bool, bool, error) {
adresses, err := iface.Addrs()
adresses, err := anet.InterfaceAddrsByInterface(&iface)
if err != nil {
return false, false, errors.WithStack(err)
}

View File

@ -0,0 +1,110 @@
package chromecast
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
gcast "github.com/barnybug/go-cast"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func init() {
cast.Register(DeviceTypeChromecast, &Service{})
}
type Service struct {
}
// Find implements cast.Service.
func (s *Service) Find(ctx context.Context, deviceID string) (cast.Device, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
results, err := s.scan(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
for dev := range results {
if dev.DeviceID() != deviceID {
continue
}
return dev, nil
}
return nil, errors.WithStack(cast.ErrDeviceNotFound)
}
// NewClient implements cast.Service.
func (*Service) NewClient(ctx context.Context, device cast.Device) (cast.Client, error) {
client := gcast.NewClient(device.DeviceHost(), device.DevicePort())
if err := client.Connect(ctx); err != nil {
return nil, errors.WithStack(err)
}
return &Client{client}, nil
}
// Scan implements cast.Service.
func (s *Service) Scan(ctx context.Context) ([]cast.Device, error) {
results, err := s.scan(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
devices := make([]cast.Device, 0)
for dev := range results {
devices = append(devices, dev)
}
return devices, nil
}
func (*Service) scan(ctx context.Context) (chan cast.Device, error) {
discovery := NewDiscovery(ctx)
defer discovery.Stop()
go func() {
if err := discovery.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
logger.Error(ctx, "could not run chromecast discovery", logger.CapturedE(errors.WithStack(err)))
}
}()
results := make(chan cast.Device)
go func() {
defer close(results)
found := make(map[string]struct{})
LOOP:
for {
select {
case c := <-discovery.Found():
dev := &Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
}
if _, exists := found[dev.UUID]; !exists {
results <- dev
found[dev.UUID] = struct{}{}
}
case <-ctx.Done():
break LOOP
}
}
}()
return results, nil
}
var _ cast.Service = &Service{}

View File

@ -0,0 +1,31 @@
package chromecast
import "forge.cadoles.com/arcad/edge/pkg/module/cast"
type DeviceStatus struct {
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
}
// State implements cast.DeviceStatus.
func (s *DeviceStatus) State() string {
return s.CurrentApp.StatusText
}
// Title implements cast.DeviceStatus.
func (s *DeviceStatus) Title() string {
return s.CurrentApp.DisplayName
}
var _ cast.DeviceStatus = &DeviceStatus{}
type DeviceStatusCurrentApp struct {
ID string `goja:"id" json:"id"`
DisplayName string `goja:"displayName" json:"displayName"`
StatusText string `goja:"statusText" json:"statusText"`
}
type DeviceStatusVolume struct {
Level float64 `goja:"level" json:"level"`
Muted bool `goja:"muted" json:"muted"`
}

10
pkg/module/cast/client.go Normal file
View File

@ -0,0 +1,10 @@
package cast
import "context"
type Client interface {
Load(ctx context.Context, url string) error
Unload(ctx context.Context) error
Status(ctx context.Context) (DeviceStatus, error)
Close() error
}

41
pkg/module/cast/device.go Normal file
View File

@ -0,0 +1,41 @@
package cast
import "net"
type DeviceType string
type Device interface {
DeviceType() DeviceType
DeviceHost() net.IP
DevicePort() int
DeviceName() string
DeviceID() string
}
type legacyDevice struct {
Device `json:"-"`
UUID string `goja:"uuid" json:"uuid"`
Host string `goja:"host" json:"host"`
Port int `goja:"port" json:"port"`
Name string `goja:"name" json:"name"`
Type string `goja:"type" json:"type"`
}
func toLegacyDevice(device Device) *legacyDevice {
return &legacyDevice{
Device: device,
UUID: device.DeviceID(),
Host: device.DeviceHost().String(),
Port: device.DevicePort(),
Name: device.DeviceName(),
Type: string(device.DeviceType()),
}
}
func toLegacyDevices(devices ...Device) []*legacyDevice {
legacyDevices := make([]*legacyDevice, len(devices))
for i, d := range devices {
legacyDevices[i] = toLegacyDevice(d)
}
return legacyDevices
}

View File

@ -2,4 +2,7 @@ package cast
import "errors"
var ErrDeviceNotFound = errors.New("device not found")
var (
ErrDeviceNotFound = errors.New("device not found")
ErrUnknownDeviceType = errors.New("unknown device type")
)

View File

@ -67,7 +67,7 @@ func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.V
return
}
promise.Resolve(devices)
promise.Resolve(toLegacyDevices(devices...))
}()
return rt.ToValue(promise)
@ -81,7 +81,7 @@ func (m *Module) getDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(devices)
return rt.ToValue(toLegacyDevices(devices...))
}
func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
@ -175,7 +175,7 @@ func (m *Module) getStatus(call goja.FunctionCall, rt *goja.Runtime) goja.Value
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
status, err := getStatus(ctx, deviceUUID)
status, err := GetStatus(ctx, deviceUUID)
if err != nil {
err = errors.WithStack(err)
logger.Error(ctx, "error while getting casting device status", logger.CapturedE(err))

133
pkg/module/cast/registry.go Normal file
View File

@ -0,0 +1,133 @@
package cast
import (
"context"
"sync"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Service interface {
Scan(ctx context.Context) ([]Device, error)
Find(ctx context.Context, deviceID string) (Device, error)
NewClient(ctx context.Context, device Device) (Client, error)
}
type Registry struct {
index map[DeviceType]Service
}
func (r *Registry) NewClient(ctx context.Context, device Device) (Client, error) {
deviceType := device.DeviceType()
srv, exists := r.index[deviceType]
if !exists {
return nil, errors.Wrapf(ErrUnknownDeviceType, "device type '%s' is not registered", deviceType)
}
client, err := srv.NewClient(ctx, device)
if err != nil {
return nil, errors.WithStack(err)
}
return client, nil
}
func (r *Registry) Find(ctx context.Context, deviceID string) (Device, error) {
for _, srv := range r.index {
device, err := srv.Find(ctx, deviceID)
if err != nil {
logger.Error(ctx, "could not get device", logger.CapturedE(errors.WithStack(err)))
continue
}
if device != nil {
return device, nil
}
}
return nil, errors.WithStack(ErrDeviceNotFound)
}
func (r *Registry) Scan(ctx context.Context) ([]Device, error) {
results := make([]Device, 0)
errs := make([]error, 0)
var (
lock sync.Mutex
wg sync.WaitGroup
)
wg.Add(len(r.index))
for _, srv := range r.index {
func(srv Service) {
go func() {
defer wg.Done()
devices, err := srv.Scan(ctx)
if err != nil {
lock.Lock()
errs = append(errs, errors.WithStack(err))
lock.Unlock()
}
lock.Lock()
results = append(results, devices...)
lock.Unlock()
}()
}(srv)
}
wg.Wait()
for _, err := range errs {
logger.Error(ctx, "error occured while scanning", logger.CapturedE(errors.WithStack(err)))
}
return results, nil
}
func (r *Registry) Register(deviceType DeviceType, service Service) {
r.index[deviceType] = service
}
func NewRegistry() *Registry {
return &Registry{
index: make(map[DeviceType]Service),
}
}
var defaultRegistry = NewRegistry()
func NewClient(ctx context.Context, device Device) (Client, error) {
client, err := defaultRegistry.NewClient(ctx, device)
if err != nil {
return nil, errors.WithStack(err)
}
return client, nil
}
func Scan(ctx context.Context) ([]Device, error) {
devices, err := defaultRegistry.Scan(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
return devices, nil
}
func Find(ctx context.Context, deviceID string) (Device, error) {
device, err := defaultRegistry.Find(ctx, deviceID)
if err != nil {
return nil, errors.WithStack(err)
}
return device, nil
}
func Register(deviceType DeviceType, service Service) {
defaultRegistry.Register(deviceType, service)
}

View File

@ -0,0 +1,6 @@
package cast
type DeviceStatus interface {
Title() string
State() string
}

View File

@ -21,7 +21,7 @@ func (m *ConsoleModule) Name() string {
func (m *ConsoleModule) log(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
var sb strings.Builder
fields := make([]logger.Field, 0)
fields := make([]any, 0)
stack := rt.CaptureCallStack(0, nil)
if len(stack) > 1 {

View File

@ -9,12 +9,16 @@ import (
"github.com/oklog/ulid/v2"
)
var ErrDocumentNotFound = errors.New("document not found")
var (
ErrDocumentNotFound = errors.New("document not found")
ErrDocumentRevisionConflict = errors.New("document revision conflict")
)
type DocumentID string
const (
DocumentAttrID = "_id"
DocumentAttrRevision = "_revision"
DocumentAttrCreatedAt = "_createdAt"
DocumentAttrUpdatedAt = "_updatedAt"
)
@ -44,6 +48,20 @@ func (d Document) ID() (DocumentID, bool) {
return "", false
}
func (d Document) Revision() (int, bool) {
rawRevision, exists := d[DocumentAttrRevision]
if !exists {
return 0, false
}
revision, ok := rawRevision.(int)
if ok {
return revision, true
}
return 0, false
}
func (d Document) CreatedAt() (time.Time, bool) {
return d.timeAttr(DocumentAttrCreatedAt)
}

View File

@ -6,17 +6,16 @@ import (
"io"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/allegro/bigcache/v3"
"github.com/hashicorp/golang-lru/v2/expirable"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type BlobBucket struct {
bucket storage.BlobBucket
contentCache *bigcache.BigCache
blobInfoCache *expirable.LRU[string, storage.BlobInfo]
bucketCache *expirable.LRU[string, storage.BlobBucket]
blobCache *lfu.Cache[string, []byte]
bucketCache *lfu.Cache[string, storage.BlobBucket]
blobInfoCache *lfu.Cache[string, storage.BlobInfo]
}
// Close implements storage.BlobBucket.
@ -39,7 +38,16 @@ func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error {
// Get implements storage.BlobBucket.
func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobInfo, error) {
key := b.getCacheKey(id)
if blobInfo, ok := b.blobInfoCache.Get(key); ok {
blobInfo, err := b.blobInfoCache.Get(key)
if err != nil && !errors.Is(err, lfu.ErrNotFound) {
logger.Error(
ctx, "could not retrieve blob info from cache",
logger.F("cacheKey", key),
logger.CapturedE(errors.WithStack(err)),
)
}
if blobInfo != nil {
logger.Debug(
ctx, "found blob info in cache",
logger.F("cacheKey", key),
@ -52,13 +60,25 @@ func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobIn
if err != nil {
if errors.Is(err, storage.ErrBucketClosed) {
b.clearCache(ctx, id)
b.bucketCache.Remove(b.Name())
if err := b.bucketCache.Delete(b.Name()); err != nil && !errors.Is(err, lfu.ErrNotFound) {
logger.Error(
ctx, "could not delete bucket from cache",
logger.F("cacheKey", b.Name()),
logger.CapturedE(errors.WithStack(err)),
)
}
}
return nil, errors.WithStack(err)
}
b.blobInfoCache.Add(key, info)
if err := b.blobInfoCache.Set(key, info); err != nil {
logger.Error(
ctx, "could not set blob info in cache",
logger.F("cacheKey", key),
logger.CapturedE(errors.WithStack(err)),
)
}
return info, nil
}
@ -68,7 +88,13 @@ func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) {
infos, err := b.bucket.List(ctx)
if err != nil {
if errors.Is(err, storage.ErrBucketClosed) {
b.bucketCache.Remove(b.Name())
if err := b.bucketCache.Delete(b.Name()); err != nil && !errors.Is(err, lfu.ErrNotFound) {
logger.Error(
ctx, "could not delete bucket from cache",
logger.F("cacheKey", b.Name()),
logger.CapturedE(errors.WithStack(err)),
)
}
}
return nil, errors.WithStack(err)
@ -76,7 +102,13 @@ func (b *BlobBucket) List(ctx context.Context) ([]storage.BlobInfo, error) {
for _, ifo := range infos {
key := b.getCacheKey(ifo.ID())
b.blobInfoCache.Add(key, ifo)
if err := b.blobInfoCache.Set(key, ifo); err != nil {
logger.Error(
ctx, "could not set blob info in cache",
logger.F("cacheKey", key),
logger.CapturedE(errors.WithStack(err)),
)
}
}
return infos, nil
@ -93,7 +125,6 @@ func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadS
logger.Debug(
ctx, "found blob content in cache",
logger.F("cacheKey", b.getCacheKey(id)),
logger.F("cacheStats", b.contentCache.Stats()),
)
return cached, nil
}
@ -102,7 +133,13 @@ func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadS
if err != nil {
if errors.Is(err, storage.ErrBucketClosed) {
b.clearCache(ctx, id)
b.bucketCache.Remove(b.Name())
if err := b.bucketCache.Delete(b.Name()); err != nil && !errors.Is(err, lfu.ErrNotFound) {
logger.Error(
ctx, "could not delete bucket from cache",
logger.F("cacheKey", b.Name()),
logger.CapturedE(errors.WithStack(err)),
)
}
}
return nil, errors.WithStack(err)
@ -110,7 +147,7 @@ func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadS
return &readCacher{
reader: reader,
cache: b.contentCache,
cache: b.blobCache,
key: b.getCacheKey(id),
}, nil
}
@ -121,13 +158,13 @@ func (b *BlobBucket) getCacheKey(id storage.BlobID) string {
func (b *BlobBucket) inContentCache(id storage.BlobID) (io.ReadSeekCloser, bool) {
key := b.getCacheKey(id)
data, err := b.contentCache.Get(key)
data, err := b.blobCache.Get(key)
if err != nil {
if errors.Is(err, bigcache.ErrEntryNotFound) {
if errors.Is(err, lfu.ErrNotFound) {
return nil, false
}
logger.Error(context.Background(), "could not retrieve cache value", logger.CapturedE(errors.WithStack(err)))
logger.Error(context.Background(), "could not retrieve cached value", logger.CapturedE(errors.WithStack(err)))
return nil, false
}
@ -140,11 +177,17 @@ func (b *BlobBucket) clearCache(ctx context.Context, id storage.BlobID) {
logger.Debug(ctx, "clearing cache", logger.F("cacheKey", key))
if err := b.contentCache.Delete(key); err != nil && !errors.Is(err, bigcache.ErrEntryNotFound) {
logger.Error(ctx, "could not clear cache", logger.CapturedE(errors.WithStack(err)))
if err := b.blobCache.Delete(key); err != nil && !errors.Is(err, lfu.ErrNotFound) {
logger.Error(ctx, "could not clear cache", logger.F("cacheKey", key), logger.CapturedE(errors.WithStack(err)))
}
b.blobInfoCache.Remove(key)
if err := b.blobInfoCache.Delete(key); err != nil {
logger.Error(
ctx, "could not delete blob info from cache",
logger.F("cacheKey", key),
logger.CapturedE(errors.WithStack(err)),
)
}
}
// NewWriter implements storage.BlobBucket.
@ -154,7 +197,13 @@ func (b *BlobBucket) NewWriter(ctx context.Context, id storage.BlobID) (io.Write
writer, err := b.bucket.NewWriter(ctx, id)
if err != nil {
if errors.Is(err, storage.ErrBucketClosed) {
b.bucketCache.Remove(b.Name())
if err := b.bucketCache.Delete(b.Name()); err != nil && !errors.Is(err, lfu.ErrNotFound) {
logger.Error(
ctx, "could not delete bucket from cache",
logger.F("cacheKey", b.Name()),
logger.CapturedE(errors.WithStack(err)),
)
}
}
return nil, errors.WithStack(err)
@ -168,7 +217,13 @@ func (b *BlobBucket) Size(ctx context.Context) (int64, error) {
size, err := b.bucket.Size(ctx)
if err != nil {
if errors.Is(err, storage.ErrBucketClosed) {
b.bucketCache.Remove(b.Name())
if err := b.bucketCache.Delete(b.Name()); err != nil && !errors.Is(err, lfu.ErrNotFound) {
logger.Error(
ctx, "could not delete bucket from cache",
logger.F("cacheKey", b.Name()),
logger.CapturedE(errors.WithStack(err)),
)
}
}
return 0, errors.WithStack(err)

View File

@ -4,17 +4,16 @@ import (
"context"
"forge.cadoles.com/arcad/edge/pkg/storage"
"github.com/allegro/bigcache/v3"
"github.com/hashicorp/golang-lru/v2/expirable"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type BlobStore struct {
store storage.BlobStore
contentCache *bigcache.BigCache
bucketCache *expirable.LRU[string, storage.BlobBucket]
blobInfoCache *expirable.LRU[string, storage.BlobInfo]
blobCache *lfu.Cache[string, []byte]
bucketCache *lfu.Cache[string, storage.BlobBucket]
blobInfoCache *lfu.Cache[string, storage.BlobInfo]
}
// DeleteBucket implements storage.BlobStore.
@ -23,7 +22,7 @@ func (s *BlobStore) DeleteBucket(ctx context.Context, name string) error {
return errors.WithStack(err)
}
s.bucketCache.Remove(name)
s.bucketCache.Delete(name)
return nil
}
@ -40,28 +39,40 @@ func (s *BlobStore) ListBuckets(ctx context.Context) ([]string, error) {
// OpenBucket implements storage.BlobStore.
func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBucket, error) {
bucket, ok := s.bucketCache.Get(name)
if ok {
bucket, err := s.bucketCache.Get(name)
if err == nil {
logger.Debug(ctx, "found bucket in cache", logger.F("name", name))
return &BlobBucket{
bucket: bucket,
contentCache: s.contentCache,
blobCache: s.blobCache,
blobInfoCache: s.blobInfoCache,
bucketCache: s.bucketCache,
}, nil
}
bucket, err := s.store.OpenBucket(ctx, name)
if err != nil && !errors.Is(err, lfu.ErrNotFound) {
logger.Error(ctx, "could not retrieve bucket from cache",
logger.F("cacheKey", name),
logger.CapturedE(errors.WithStack(err)),
)
}
bucket, err = s.store.OpenBucket(ctx, name)
if err != nil {
return nil, errors.WithStack(err)
}
s.bucketCache.Add(name, bucket)
if err := s.bucketCache.Set(name, bucket); err != nil {
logger.Error(ctx, "could not set bucket in cache",
logger.F("cacheKey", name),
logger.CapturedE(errors.WithStack(err)),
)
}
return &BlobBucket{
bucket: bucket,
contentCache: s.contentCache,
blobCache: s.blobCache,
blobInfoCache: s.blobInfoCache,
bucketCache: s.bucketCache,
}, nil
@ -70,27 +81,36 @@ func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBu
func NewBlobStore(store storage.BlobStore, funcs ...OptionFunc) (*BlobStore, error) {
options := NewOptions(funcs...)
contentCache, err := bigcache.New(context.Background(), options.BigCache)
if err != nil {
return nil, errors.WithStack(err)
}
blobCache := lfu.NewCache[string, []byte](
options.BlobCacheStore,
lfu.WithTTL[string, []byte](options.CacheTTL),
lfu.WithCapacity[string, []byte](options.BlobCacheSize),
lfu.WithGetValueSize[string, []byte](func(value []byte) (int, error) {
return len(value), nil
}),
)
onBlobBucketEvict := func(key string, bucket storage.BlobBucket) {
ctx := context.Background()
logger.Debug(ctx, "evicting blob bucket from cache", logger.F("cacheKey", key))
blobBucketCache := lfu.NewCache[string, storage.BlobBucket](
options.BlobBucketCacheStore,
lfu.WithCapacity[string, storage.BlobBucket](options.BlobBucketCacheSize),
lfu.WithGetValueSize[string, storage.BlobBucket](func(value storage.BlobBucket) (int, error) {
return 1, nil
}),
)
if err := bucket.Close(); err != nil {
logger.Error(ctx, "could not close bucket", logger.E(errors.WithStack(err)))
}
}
bucketCache := expirable.NewLRU[string, storage.BlobBucket](options.BucketCacheSize, onBlobBucketEvict, options.CacheTTL)
blobInfoCache := expirable.NewLRU[string, storage.BlobInfo](options.BlobInfoCacheSize, nil, options.CacheTTL)
blobInfoCache := lfu.NewCache[string, storage.BlobInfo](
options.BlobInfoCacheStore,
lfu.WithTTL[string, storage.BlobInfo](options.CacheTTL),
lfu.WithCapacity[string, storage.BlobInfo](options.BlobInfoCacheSize),
lfu.WithGetValueSize[string, storage.BlobInfo](func(value storage.BlobInfo) (int, error) {
return 1, nil
}),
)
return &BlobStore{
store: store,
contentCache: contentCache,
bucketCache: bucketCache,
blobCache: blobCache,
bucketCache: blobBucketCache,
blobInfoCache: blobInfoCache,
}, nil
}

View File

@ -1,17 +1,19 @@
package cache
import (
"context"
"fmt"
"bytes"
"io"
"net/url"
"strconv"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
"github.com/allegro/bigcache/v3"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu/fs"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu/memory"
"github.com/inhies/go-bytesize"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func init() {
@ -41,13 +43,6 @@ func blobStoreFactory(dsn *url.URL) (storage.BlobStore, error) {
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithCacheTTL(cacheTTL))
cacheConfig, err := parseBigCacheConfig(&query, cacheTTL)
if err != nil {
return nil, errors.Wrap(err, "could not parse big cache config")
}
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithBigCacheConfig(*cacheConfig))
blobBucketCacheSize, err := parseInt(&query, "blobBucketCacheSize")
if err != nil {
if !errors.Is(err, errNotFound) {
@ -57,7 +52,15 @@ func blobStoreFactory(dsn *url.URL) (storage.BlobStore, error) {
blobBucketCacheSize = 16
}
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithBucketCacheSize(int(blobBucketCacheSize)))
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithBlobBucketCacheSize(int(blobBucketCacheSize)))
blobBucketCacheStorePrefix := "blobBucketCacheStore"
blobBucketCacheStore, err := parseCacheStore[string, storage.BlobBucket](&query, blobBucketCacheStorePrefix)
if err != nil {
return nil, errors.WithStack(err)
}
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithBlobBucketCacheStore(blobBucketCacheStore))
bloInfoCacheSize, err := parseInt(&query, "bloInfoCacheSize")
if err != nil {
@ -70,6 +73,46 @@ func blobStoreFactory(dsn *url.URL) (storage.BlobStore, error) {
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithBlobInfoCacheSize(int(bloInfoCacheSize)))
blobInfoCacheStorePrefix := "blobInfoCacheStore"
blobInfoCacheStore, err := parseCacheStore[string, storage.BlobInfo](&query, blobInfoCacheStorePrefix)
if err != nil {
return nil, errors.WithStack(err)
}
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithBlobInfoCacheStore(blobInfoCacheStore))
blobCacheSize, err := parseByteSize(&query, "blobCacheSize")
if err != nil {
if !errors.Is(err, errNotFound) {
return nil, errors.WithStack(err)
}
blobCacheSize = 256e+6
}
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithBlobCacheSize(int(blobCacheSize)))
blobCacheStorePrefix := "blobCacheStore"
blobCacheStore, err := parseCacheStore[string, []byte](
&query, blobCacheStorePrefix,
fs.WithMarshalValue[string, []byte](func(value []byte) (io.Reader, error) {
return bytes.NewBuffer(value), nil
}),
fs.WithUnmarshalValue[string, []byte](func(r io.Reader) ([]byte, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, errors.WithStack(err)
}
return data, nil
}),
)
if err != nil {
return nil, errors.WithStack(err)
}
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithBlobCacheStore(blobCacheStore))
url := &url.URL{
Scheme: rawDriver,
Host: dsn.Host,
@ -90,89 +133,34 @@ func blobStoreFactory(dsn *url.URL) (storage.BlobStore, error) {
return store, nil
}
type cacheLogger struct{}
func (l *cacheLogger) Printf(format string, v ...interface{}) {
logger.Debug(context.Background(), fmt.Sprintf(format, v...))
}
var _ bigcache.Logger = &cacheLogger{}
func parseBigCacheConfig(query *url.Values, cacheTTL time.Duration) (*bigcache.Config, error) {
config := bigcache.DefaultConfig(cacheTTL)
config.Logger = &cacheLogger{}
hardMaxCacheSize, err := parseInt(query, "bigCacheHardMaxCacheSize")
if err != nil {
if !errors.Is(err, errNotFound) {
return nil, errors.WithStack(err)
}
hardMaxCacheSize = int64(config.HardMaxCacheSize)
}
config.HardMaxCacheSize = int(hardMaxCacheSize)
maxEntriesInWindow, err := parseInt(query, "bigCacheMaxEntriesInWindow")
if err != nil {
if !errors.Is(err, errNotFound) {
return nil, errors.WithStack(err)
}
maxEntriesInWindow = int64(config.MaxEntriesInWindow)
}
config.MaxEntriesInWindow = int(maxEntriesInWindow)
shards, err := parseInt(query, "bigCacheShards")
if err != nil {
if !errors.Is(err, errNotFound) {
return nil, errors.WithStack(err)
}
shards = int64(config.Shards)
}
config.Shards = int(shards)
maxEntrySize, err := parseInt(query, "bigCacheMaxEntrySize")
if err != nil {
if !errors.Is(err, errNotFound) {
return nil, errors.WithStack(err)
}
maxEntrySize = int64(config.MaxEntrySize)
}
config.MaxEntrySize = int(maxEntrySize)
cleanWindow, err := parseDuration(query, "bigCacheCleanWindow")
if err != nil {
if !errors.Is(err, errNotFound) {
return nil, errors.WithStack(err)
}
cleanWindow = config.CleanWindow
}
config.CleanWindow = cleanWindow
lifeWindow, err := parseDuration(query, "bigCacheLifeWindow")
if err != nil {
if !errors.Is(err, errNotFound) {
return nil, errors.WithStack(err)
}
lifeWindow = config.LifeWindow
}
config.LifeWindow = lifeWindow
return &config, nil
}
var errNotFound = errors.New("not found")
func parseString(query *url.Values, name string) (string, error) {
value := query.Get(name)
if value != "" {
query.Del(name)
return value, nil
}
return "", errors.WithStack(errNotFound)
}
func parseByteSize(query *url.Values, name string) (bytesize.ByteSize, error) {
rawValue := query.Get(name)
if rawValue != "" {
query.Del(name)
value, err := bytesize.Parse(rawValue)
if err != nil {
return 0, errors.Wrapf(err, "could not parse url parameter '%s'", name)
}
return value, nil
}
return 0, errors.WithStack(errNotFound)
}
func parseInt(query *url.Values, name string) (int64, error) {
rawValue := query.Get(name)
if rawValue != "" {
@ -204,3 +192,73 @@ func parseDuration(query *url.Values, name string) (time.Duration, error) {
return 0, errors.WithStack(errNotFound)
}
const (
storeTypeFS string = "fs"
storeTypeMemory string = "memory"
)
func parseCacheStore[K comparable, V any](query *url.Values, prefix string, optionFuncs ...any) (lfu.Store[K, V], error) {
storeTypeParam := prefix + "Type"
storeType, err := parseString(query, storeTypeParam)
if err != nil {
if errors.Is(err, errNotFound) {
storeType = storeTypeMemory
}
}
switch storeType {
case storeTypeFS:
store, err := parseFSCacheStore[K, V](query, prefix, optionFuncs...)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
case storeTypeMemory:
store, err := parseMemoryCacheStore[K, V](query, prefix, optionFuncs...)
if err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}
return nil, errors.Errorf("unexpected store type value '%s' for parameter '%s'", storeType, storeTypeParam)
}
func parseFSCacheStore[K comparable, V any](query *url.Values, prefix string, optionFuncs ...any) (*fs.Store[K, V], error) {
baseDirParam := prefix + "BaseDir"
baseDir, err := parseString(query, baseDirParam)
if err != nil {
if errors.Is(err, errNotFound) {
return nil, errors.Wrapf(err, "missing required url parameter '%s'", baseDirParam)
}
return nil, errors.WithStack(err)
}
funcs := make([]fs.OptionsFunc[K, V], 0)
for _, anyFn := range optionFuncs {
fn, ok := anyFn.(fs.OptionsFunc[K, V])
if !ok {
continue
}
funcs = append(funcs, fn)
}
store := fs.NewStore[K, V](baseDir, funcs...)
if err := store.Clear(); err != nil {
return nil, errors.WithStack(err)
}
return store, nil
}
func parseMemoryCacheStore[K comparable, V any](query *url.Values, prefix string, optionFuncs ...any) (*memory.Store[K, V], error) {
return memory.NewStore[K, V](), nil
}

349
pkg/storage/driver/cache/lfu/cache.go vendored Normal file
View File

@ -0,0 +1,349 @@
package lfu
import (
"slices"
"sync/atomic"
"time"
"github.com/pkg/errors"
)
var (
ErrNotFound = errors.New("not found")
ErrSizeExceedCapacity = errors.New("size exceed capacity")
errExpired = errors.New("expired")
)
type Cache[K comparable, V any] struct {
index *Map[K, *cacheItem[K, V]]
freqs *List[*frequencyItem[K, V]]
size atomic.Int32
capacity int
store Store[K, V]
getValueSize GetValueSizeFunc[V]
sync *Synchronizer[K]
log LogFunc
ttl time.Duration
}
type cacheItem[K any, V any] struct {
key K
size int
time atomic.Int64
frequencyParent *Element[*frequencyItem[K, V]]
}
func (i *cacheItem[K, V]) Expired(ttl time.Duration) bool {
if ttl == 0 {
return false
}
itemTime := time.Unix(i.time.Load(), 0)
// If item has expired, mark it as not found
return itemTime.Add(ttl).Before(time.Now())
}
func (i *cacheItem[K, V]) Refresh() {
i.time.Store(time.Now().Unix())
}
func newCacheItem[K any, V any](key K, size int) *cacheItem[K, V] {
item := &cacheItem[K, V]{
key: key,
size: size,
}
item.time.Store(time.Now().Unix())
return item
}
type frequencyItem[K any, V any] struct {
entries *Map[*cacheItem[K, V], struct{}]
freq int
}
func newFrequencyItem[K any, V any]() *frequencyItem[K, V] {
frequencyItem := &frequencyItem[K, V]{}
frequencyItem.entries = NewMap[*cacheItem[K, V], struct{}]()
return frequencyItem
}
func (c *Cache[K, V]) Set(key K, value V) error {
newItemSize, err := c.getValueSize(value)
if err != nil {
return errors.WithStack(err)
}
c.log("setting '%v' (size: %d)", key, newItemSize)
if newItemSize > int(c.capacity) {
return errors.Wrapf(ErrSizeExceedCapacity, "item size '%d' exceed cache total capacity of '%v'", newItemSize, c.capacity)
}
var sizeDelta int
err = c.sync.WriteTx(key, func() error {
if err := c.store.Set(key, value); err != nil {
return errors.WithStack(err)
}
item, ok := c.index.Get(key)
if ok {
oldItemSize := item.size
sizeDelta = -int(oldItemSize) + newItemSize
item.Refresh()
} else {
item = newCacheItem[K, V](key, newItemSize)
c.index.Set(key, item)
sizeDelta = newItemSize
}
c.size.Add(int32(sizeDelta))
c.increment(item)
return nil
})
if err != nil {
return errors.WithStack(err)
}
// Eviction, if needed
if err := c.Evict(key); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *Cache[K, V]) Get(key K) (V, error) {
var value V
err := c.sync.ReadTx(key, func(upgrade func(func())) error {
c.log("getting '%v'", key)
e, ok := c.index.Get(key)
if !ok {
return errors.WithStack(ErrNotFound)
}
if e.Expired(c.ttl) {
return errors.WithStack(errExpired)
}
v, err := c.store.Get(key)
if err != nil {
return errors.WithStack(err)
}
upgrade(func() {
c.increment(e)
})
value = v
return nil
})
if err != nil {
if errors.Is(err, errExpired) {
if err := c.Delete(key); err != nil {
return *new(V), errors.WithStack(err)
}
return *new(V), errors.WithStack(ErrNotFound)
}
return *new(V), errors.WithStack(err)
}
return value, nil
}
func (c *Cache[K, V]) Delete(key K) error {
err := c.sync.WriteTx(key, func() error {
c.log("deleting '%v'", key)
item, exists := c.index.Get(key)
if !exists {
return errors.WithStack(ErrNotFound)
}
if err := c.store.Delete(key); err != nil {
return errors.WithStack(err)
}
c.size.Add(-int32(item.size))
c.remove(item.frequencyParent, item)
c.index.Delete(key)
return nil
})
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *Cache[K, V]) Evict(skipped ...K) error {
exceed, delta := c.atCapacity()
if exceed && delta > 0 {
if err := c.evict(delta, skipped...); err != nil {
return errors.WithStack(err)
}
}
return nil
}
func (c *Cache[K, V]) Len() int {
return c.index.Len()
}
func (c *Cache[K, V]) Size() int {
return int(c.size.Load())
}
func (c *Cache[K, V]) Capacity() int {
return c.capacity
}
func (c *Cache[K, V]) increment(item *cacheItem[K, V]) {
currentFrequencyElement := item.frequencyParent
var nextFrequencyAmount int
var nextFrequencyElement *Element[*frequencyItem[K, V]]
if currentFrequencyElement == nil {
nextFrequencyAmount = 1
nextFrequencyElement = c.freqs.First()
} else {
atomicFrequencyItem := c.freqs.Value(currentFrequencyElement)
nextFrequencyAmount = atomicFrequencyItem.freq + 1
nextFrequencyElement = c.freqs.Next(currentFrequencyElement)
}
var nextFrequency *frequencyItem[K, V]
if nextFrequencyElement != nil {
nextFrequency = c.freqs.Value(nextFrequencyElement)
}
if nextFrequencyElement == nil || nextFrequency == nil || nextFrequency.freq != nextFrequencyAmount {
newFrequencyItem := newFrequencyItem[K, V]()
newFrequencyItem.freq = nextFrequencyAmount
if currentFrequencyElement == nil {
nextFrequencyElement = c.freqs.PushFront(newFrequencyItem)
} else {
nextFrequencyElement = c.freqs.InsertValueAfter(newFrequencyItem, currentFrequencyElement)
}
}
item.frequencyParent = nextFrequencyElement
nextFrequency = c.freqs.Value(nextFrequencyElement)
nextFrequency.entries.Set(item, struct{}{})
if currentFrequencyElement != nil {
c.remove(currentFrequencyElement, item)
}
}
func (c *Cache[K, V]) remove(listItem *Element[*frequencyItem[K, V]], item *cacheItem[K, V]) {
entries := c.freqs.Value(listItem).entries
entries.Delete(item)
}
func (c *Cache[K, V]) atCapacity() (bool, int) {
size, capacity := c.Size(), c.Capacity()
c.log("cache stats: %d/%d", size, capacity)
return size >= capacity, size - capacity
}
func (c *Cache[K, V]) evict(total int, skipped ...K) error {
if total == 0 {
return nil
}
frequencyElement := c.freqs.First()
if frequencyElement == nil {
c.log("no frequency element")
return nil
}
for evicted := 0; evicted < total; {
c.log("running eviction: [to_evict:%d, evicted: %d]", total, evicted)
c.log("first frequency element %p", frequencyElement)
frequencyItem := c.freqs.Value(frequencyElement)
if frequencyItem == nil {
return nil
}
entries := frequencyItem.entries
if entries.Len() == 0 {
c.log("no frequency entries")
frequencyElement = c.freqs.Next(frequencyElement)
continue
}
var rangeErr error
entries.Range(func(key, v any) bool {
if evicted >= total {
c.log("evicted enough (%d >= %d), stopping", evicted, total)
return false
}
entry, _ := key.(*cacheItem[K, V])
if slices.Contains(skipped, entry.key) {
c.log("skipping key '%v'", entry.key)
return true
}
if err := c.Delete(entry.key); err != nil {
if errors.Is(err, ErrNotFound) {
c.log("key '%s' not found", entry.key)
// Cleanup obsolete frequency
c.remove(frequencyElement, entry)
return true
}
rangeErr = errors.WithStack(err)
return false
}
c.log("evicted key '%v' (size: %d)", entry.key, entry.size)
evicted += int(entry.size)
return true
})
if rangeErr != nil {
return errors.WithStack(rangeErr)
}
}
return nil
}
func NewCache[K comparable, V any](store Store[K, V], funcs ...OptionsFunc[K, V]) *Cache[K, V] {
opts := DefaultOptions[K, V](funcs...)
cache := &Cache[K, V]{
index: NewMap[K, *cacheItem[K, V]](),
freqs: NewList[*frequencyItem[K, V]](),
capacity: opts.Capacity,
store: store,
getValueSize: opts.GetValueSize,
sync: NewSynchronizer[K](),
log: opts.Log,
ttl: opts.TTL,
}
return cache
}

View File

@ -0,0 +1,37 @@
package fs
import (
"bytes"
"io"
"path/filepath"
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu/testsuite"
"github.com/pkg/errors"
)
func TestCacheWithFSStore(t *testing.T) {
testsuite.TestCacheWithStore(t, func(testName string) lfu.Store[string, string] {
dir := filepath.Join("testdata", "testsuite", testName)
store := NewStore[string, string](dir,
WithMarshalValue[string, string](func(value string) (io.Reader, error) {
return bytes.NewBuffer([]byte(value)), nil
}),
WithUnmarshalValue[string, string](func(r io.Reader) (string, error) {
data, err := io.ReadAll(r)
if err != nil {
return "", errors.WithStack(err)
}
return string(data), nil
}),
)
if err := store.Clear(); err != nil {
panic(errors.WithStack(err))
}
return store
})
}

19
pkg/storage/driver/cache/lfu/fs/hash.go vendored Normal file
View File

@ -0,0 +1,19 @@
package fs
import (
"strconv"
"github.com/mitchellh/hashstructure/v2"
"github.com/pkg/errors"
)
func DefaultGetPath[K comparable](key K) ([]string, error) {
uintHash, err := hashstructure.Hash(key, hashstructure.FormatV2, nil)
if err != nil {
return nil, errors.WithStack(err)
}
hash := strconv.FormatUint(uintHash, 16)
return []string{hash}, nil
}

View File

@ -0,0 +1,31 @@
package fs
import (
"bytes"
"encoding/gob"
"io"
"github.com/pkg/errors"
)
func DefaultMarshalValue[V any](value V) (io.Reader, error) {
var buf bytes.Buffer
encoder := gob.NewEncoder(&buf)
if err := encoder.Encode(value); err != nil {
return nil, errors.WithStack(err)
}
return &buf, nil
}
func DefaultUnmarshalValue[V any](d io.Reader) (V, error) {
var value V
encoder := gob.NewDecoder(d)
if err := encoder.Decode(&value); err != nil {
return *new(V), errors.WithStack(err)
}
return value, nil
}

View File

@ -0,0 +1,45 @@
package fs
import "io"
type GetPathFunc[K comparable] func(key K) ([]string, error)
type MarshalValueFunc[V any] func(value V) (io.Reader, error)
type UnmarshalValueFunc[V any] func(r io.Reader) (V, error)
type Options[K comparable, V any] struct {
GetPath GetPathFunc[K]
MarshalValue MarshalValueFunc[V]
UnmarshalValue UnmarshalValueFunc[V]
}
type OptionsFunc[K comparable, V any] func(opts *Options[K, V])
func DefaultOptions[K comparable, V any](funcs ...OptionsFunc[K, V]) *Options[K, V] {
opts := &Options[K, V]{
GetPath: DefaultGetPath[K],
MarshalValue: DefaultMarshalValue[V],
UnmarshalValue: DefaultUnmarshalValue[V],
}
for _, fn := range funcs {
fn(opts)
}
return opts
}
func WithGetPath[K comparable, V any](getKeyHash GetPathFunc[K]) OptionsFunc[K, V] {
return func(opts *Options[K, V]) {
opts.GetPath = getKeyHash
}
}
func WithMarshalValue[K comparable, V any](marshalValue MarshalValueFunc[V]) OptionsFunc[K, V] {
return func(opts *Options[K, V]) {
opts.MarshalValue = marshalValue
}
}
func WithUnmarshalValue[K comparable, V any](unmarshalValue UnmarshalValueFunc[V]) OptionsFunc[K, V] {
return func(opts *Options[K, V]) {
opts.UnmarshalValue = unmarshalValue
}
}

165
pkg/storage/driver/cache/lfu/fs/store.go vendored Normal file
View File

@ -0,0 +1,165 @@
package fs
import (
"fmt"
"io"
"os"
"path/filepath"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
)
type Store[K comparable, V any] struct {
baseDir string
getPath GetPathFunc[K]
marshalValue MarshalValueFunc[V]
unmarshalValue UnmarshalValueFunc[V]
}
// Delete implements Store.
func (s *Store[K, V]) Delete(key K) error {
path, err := s.getEntryPath(key)
if err != nil {
return errors.WithStack(err)
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return errors.WithStack(err)
}
return nil
}
// Get implements Store.
func (s *Store[K, V]) Get(key K) (V, error) {
path, err := s.getEntryPath(key)
if err != nil {
return *new(V), errors.WithStack(err)
}
value, err := s.readValue(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return *new(V), errors.WithStack(lfu.ErrNotFound)
}
return *new(V), errors.WithStack(err)
}
return value, nil
}
// Set implements Store.
func (s *Store[K, V]) Set(key K, value V) error {
path, err := s.getEntryPath(key)
if err != nil {
return errors.WithStack(err)
}
if err := s.writeValue(path, value); err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *Store[K, V]) Clear() error {
if err := os.RemoveAll(s.baseDir); err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *Store[K, V]) getEntryPath(k K) (string, error) {
path, err := s.getPath(k)
if err != nil {
return "", errors.WithStack(err)
}
path = append([]string{s.baseDir}, path...)
return filepath.Join(path...), nil
}
func (s *Store[K, V]) writeValue(path string, value V) error {
fi, err := os.Stat(path)
if err == nil && !fi.Mode().IsRegular() {
return fmt.Errorf("%s already exists and is not a regular file", path)
}
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil {
return errors.WithStack(err)
}
f, err := os.CreateTemp(dir, filepath.Base(path)+".tmp")
if err != nil {
return errors.WithStack(err)
}
tmpName := f.Name()
defer func() {
if err != nil {
f.Close()
os.Remove(tmpName)
}
}()
reader, err := s.marshalValue(value)
if err != nil {
return errors.WithStack(err)
}
if _, err := io.Copy(f, reader); err != nil {
return errors.WithStack(err)
}
if err := f.Sync(); err != nil {
return errors.WithStack(err)
}
if err := f.Close(); err != nil {
return errors.WithStack(err)
}
if err := os.Rename(tmpName, path); err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *Store[K, V]) readValue(path string) (V, error) {
file, err := os.Open(path)
if err != nil {
return *new(V), errors.WithStack(err)
}
defer func() {
if err := file.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
panic(errors.WithStack(err))
}
}()
value, err := s.unmarshalValue(file)
if err != nil {
return *new(V), errors.WithStack(err)
}
return value, nil
}
func NewStore[K comparable, V any](baseDir string, funcs ...OptionsFunc[K, V]) *Store[K, V] {
opts := DefaultOptions[K, V](funcs...)
return &Store[K, V]{
baseDir: baseDir,
getPath: opts.GetPath,
unmarshalValue: opts.UnmarshalValue,
marshalValue: opts.MarshalValue,
}
}
var _ lfu.Store[string, int] = &Store[string, int]{}

View File

@ -0,0 +1,2 @@
*
!.gitignore

203
pkg/storage/driver/cache/lfu/list.go vendored Normal file
View File

@ -0,0 +1,203 @@
package lfu
import (
"sync/atomic"
)
type List[T any] struct {
root *Element[T]
len atomic.Int32
sync *Synchronizer[*Element[T]]
}
func (l *List[T]) First() *Element[T] {
if l.Len() == 0 {
return nil
}
var next *Element[T]
l.sync.ReadTx(l.root, func(upgrade func(func())) error {
next = l.root.next
return nil
})
return next
}
func (l *List[T]) Last() *Element[T] {
if l.Len() == 0 {
return nil
}
var prev *Element[T]
l.sync.ReadTx(l.root, func(upgrade func(func())) error {
prev = l.root.prev
return nil
})
return prev
}
func (l *List[T]) Prev(e *Element[T]) *Element[T] {
var prev *Element[T]
l.sync.ReadTx(e, func(upgrade func(func())) error {
prev = e.prev
return nil
})
return prev
}
func (l *List[T]) Next(e *Element[T]) *Element[T] {
var next *Element[T]
l.sync.ReadTx(e, func(upgrade func(func())) error {
next = e.next
return nil
})
return next
}
func (l *List[T]) Value(e *Element[T]) T {
var value T
l.sync.ReadTx(e, func(upgrade func(func())) error {
value = e.value
return nil
})
return value
}
func (l *List[T]) PushFront(v T) *Element[T] {
return l.InsertValueAfter(v, l.root)
}
func (l *List[T]) PushBack(v T) *Element[T] {
return l.InsertValueAfter(v, l.root)
}
func (l *List[T]) Remove(e *Element[T]) {
l.remove(e)
}
func (l *List[T]) Len() int {
return int(l.len.Load())
}
func (l *List[T]) insertAfter(e *Element[T], at *Element[T]) *Element[T] {
l.sync.ReadTx(e, func(upgrade func(fn func())) error {
var next *Element[T]
l.sync.ReadTx(at, func(upgrade func(func())) error {
next = at.next
return nil
})
upgrade(func() {
e.prev = at
e.next = next
e.list = l
})
if e.prev != nil {
l.sync.WriteTx(e.prev, func() error {
e.prev.next = e
return nil
})
}
if e.next != nil {
l.sync.WriteTx(e.next, func() error {
e.next.prev = e
return nil
})
}
return nil
})
l.len.Add(1)
return e
}
func (l *List[T]) InsertValueAfter(v T, at *Element[T]) *Element[T] {
e := NewElement[T](v)
return l.insertAfter(e, at)
}
func (l *List[T]) remove(e *Element[T]) {
if e == nil && e == l.root {
return
}
l.sync.ReadTx(e, func(upgrade func(fn func())) error {
if e.prev != nil {
if e.prev == e {
upgrade(func() {
e.prev.next = e.next
})
} else {
l.sync.WriteTx(e.prev, func() error {
e.prev.next = e.next
return nil
})
}
}
if e.next != nil {
if e.next == e {
upgrade(func() {
e.next.prev = e.prev
})
} else {
l.sync.WriteTx(e.next, func() error {
e.next.prev = e.prev
return nil
})
}
}
upgrade(func() {
e.next = nil
e.prev = nil
e.list = nil
})
return nil
})
l.sync.Remove(e)
l.len.Add(-1)
}
func NewList[T any]() *List[T] {
root := NewElement(*new(T))
root.next = root
root.prev = root
list := &List[T]{
sync: NewSynchronizer[*Element[T]](),
}
root.list = list
list.root = root
return list
}
type Element[T any] struct {
prev *Element[T]
next *Element[T]
list *List[T]
value T
}
func NewElement[T any](v T) *Element[T] {
element := &Element[T]{
prev: nil,
next: nil,
list: nil,
value: v,
}
return element
}

67
pkg/storage/driver/cache/lfu/map.go vendored Normal file
View File

@ -0,0 +1,67 @@
package lfu
import (
"sync"
"sync/atomic"
)
type Map[K comparable, V any] struct {
size atomic.Int32
inner sync.Map
}
func (m *Map[K, V]) Get(key K) (V, bool) {
raw, exists := m.inner.Load(key)
if !exists {
return *new(V), false
}
value, ok := raw.(V)
if !ok {
return *new(V), false
}
return value, true
}
func (m *Map[K, V]) GetOrSet(key K, defaultValue V) (V, bool) {
raw, loaded := m.inner.LoadOrStore(key, defaultValue)
if !loaded {
m.size.Add(1)
}
value, ok := raw.(V)
if !ok {
return *new(V), loaded
}
return value, loaded
}
func (m *Map[K, V]) Set(key K, value V) {
_, loaded := m.inner.Swap(key, value)
if !loaded {
m.size.Add(1)
}
}
func (m *Map[K, V]) Delete(key K) {
_, existed := m.inner.LoadAndDelete(key)
if existed {
m.size.Add(-1)
}
}
func (m *Map[K, V]) Range(fn func(key, value any) bool) {
m.inner.Range(fn)
}
func (m *Map[K, V]) Len() int {
return int(m.size.Load())
}
func NewMap[K comparable, V any]() *Map[K, V] {
return &Map[K, V]{
inner: sync.Map{},
}
}

View File

@ -0,0 +1,14 @@
package memory
import (
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu/testsuite"
)
func TestCacheWithMemoryStore(t *testing.T) {
testsuite.TestCacheWithStore(t, func(testName string) lfu.Store[string, string] {
return NewStore[string, string]()
})
}

View File

@ -0,0 +1,40 @@
package memory
import (
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
)
type Store[K comparable, V any] struct {
index *lfu.Map[K, V]
}
// Delete implements Store.
func (s *Store[K, V]) Delete(key K) error {
s.index.Delete(key)
return nil
}
// Get implements Store.
func (s *Store[K, V]) Get(key K) (V, error) {
value, exists := s.index.Get(key)
if !exists {
return *new(V), errors.WithStack(lfu.ErrNotFound)
}
return value, nil
}
// Set implements Store.
func (s *Store[K, V]) Set(key K, value V) error {
s.index.Set(key, value)
return nil
}
func NewStore[K comparable, V any]() *Store[K, V] {
return &Store[K, V]{
index: lfu.NewMap[K, V](),
}
}
var _ lfu.Store[string, int] = &Store[string, int]{}

57
pkg/storage/driver/cache/lfu/options.go vendored Normal file
View File

@ -0,0 +1,57 @@
package lfu
import "time"
type GetValueSizeFunc[V any] func(value V) (int, error)
type LogFunc func(format string, values ...any)
func DefaultLogFunc(format string, values ...any) {
}
type Options[K comparable, V any] struct {
GetValueSize GetValueSizeFunc[V]
Capacity int
Log LogFunc
TTL time.Duration
}
type OptionsFunc[K comparable, V any] func(opts *Options[K, V])
func DefaultOptions[K comparable, V any](funcs ...OptionsFunc[K, V]) *Options[K, V] {
opts := &Options[K, V]{
GetValueSize: DefaultGetValueSize[V],
Capacity: 100,
Log: DefaultLogFunc,
TTL: 0,
}
for _, fn := range funcs {
fn(opts)
}
return opts
}
func WithCapacity[K comparable, V any](capacity int) OptionsFunc[K, V] {
return func(opts *Options[K, V]) {
opts.Capacity = capacity
}
}
func WithGetValueSize[K comparable, V any](getValueSize GetValueSizeFunc[V]) OptionsFunc[K, V] {
return func(opts *Options[K, V]) {
opts.GetValueSize = getValueSize
}
}
func WithLog[K comparable, V any](fn LogFunc) OptionsFunc[K, V] {
return func(opts *Options[K, V]) {
opts.Log = fn
}
}
func WithTTL[K comparable, V any](ttl time.Duration) OptionsFunc[K, V] {
return func(opts *Options[K, V]) {
opts.TTL = ttl
}
}

41
pkg/storage/driver/cache/lfu/size.go vendored Normal file
View File

@ -0,0 +1,41 @@
package lfu
import (
"github.com/pkg/errors"
)
type Measurable interface {
Size() (int, error)
}
func DefaultGetValueSize[V any](value V) (int, error) {
switch v := any(value).(type) {
case int:
return v, nil
case int8:
return int(v), nil
case int32:
return int(v), nil
case int64:
return int(v), nil
case float32:
return int(v), nil
case float64:
return int(v), nil
case []byte:
return len(v), nil
case string:
return len(v), nil
}
if measurable, ok := any(value).(Measurable); ok {
size, err := measurable.Size()
if err != nil {
return 0, errors.WithStack(err)
}
return size, nil
}
return 0, errors.Errorf("could not retrieve size of type '%T'", value)
}

7
pkg/storage/driver/cache/lfu/store.go vendored Normal file
View File

@ -0,0 +1,7 @@
package lfu
type Store[K comparable, V any] interface {
Delete(key K) error
Set(key K, value V) error
Get(key K) (V, error)
}

View File

@ -0,0 +1,56 @@
package lfu
import (
"sync"
"github.com/pkg/errors"
)
type Synchronizer[K comparable] struct {
index *Map[K, *sync.RWMutex]
}
func (s *Synchronizer[K]) Remove(key K) {
s.index.Delete(key)
}
func (s *Synchronizer[K]) ReadTx(key K, fn func(upgrade func(fn func())) error) error {
mutex, _ := s.index.GetOrSet(key, &sync.RWMutex{})
mutex.RLock()
defer mutex.RUnlock()
upgrade := func(fn func()) {
mutex.RUnlock()
mutex.Lock()
defer func() {
mutex.Unlock()
mutex.RLock()
}()
fn()
}
if err := fn(upgrade); err != nil {
return errors.WithStack(err)
}
return nil
}
func (s *Synchronizer[K]) WriteTx(key K, fn func() error) error {
mutex, _ := s.index.GetOrSet(key, &sync.RWMutex{})
mutex.Lock()
defer mutex.Unlock()
if err := fn(); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewSynchronizer[K comparable]() *Synchronizer[K] {
return &Synchronizer[K]{
index: NewMap[K, *sync.RWMutex](),
}
}

View File

@ -0,0 +1,41 @@
package testsuite
import (
"reflect"
"runtime"
"strings"
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
)
type StoreFactory func(testName string) lfu.Store[string, string]
type testCase func(t *testing.T, store lfu.Store[string, string]) error
var testCases = []testCase{
testSetGetDelete,
testEviction,
testConcurrent,
testMultipleSet,
testTTL,
}
func TestCacheWithStore(t *testing.T, factory StoreFactory) {
for _, tc := range testCases {
funcName := runtime.FuncForPC(reflect.ValueOf(tc).Pointer()).Name()
funcNameParts := strings.Split(funcName, "/")
testName := funcNameParts[len(funcNameParts)-1]
func(tc testCase) {
t.Run(testName, func(t *testing.T) {
t.Parallel()
store := factory(testName)
if err := tc(t, store); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
})
}(tc)
}
}

View File

@ -0,0 +1,67 @@
package testsuite
import (
"fmt"
"sync"
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
)
func testConcurrent(t *testing.T, store lfu.Store[string, string]) error {
const value = "foobar"
totalKeys := 25
totalSize := len(value) * totalKeys
capacity := totalSize / 2
cache := lfu.NewCache[string, string](store,
lfu.WithCapacity[string, string](capacity),
lfu.WithLog[string, string](t.Logf),
)
var wg sync.WaitGroup
wg.Add(totalKeys)
loops := totalKeys * 10
for i := 0; i < totalKeys; i++ {
key := fmt.Sprintf("key%d", i)
func(key string) {
go func() {
defer wg.Done()
for i := 0; i < loops; i++ {
if err := cache.Set(key, value); err != nil {
t.Errorf("%+v", errors.WithStack(err))
}
}
}()
}(key)
}
wg.Wait()
t.Logf("cache before final evict [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
if err := cache.Evict(); err != nil {
t.Errorf("%+v", errors.WithStack(err))
}
t.Logf("cache after final evict [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
expectedLength := capacity / len(value)
if e, g := expectedLength, cache.Len(); e < g {
t.Errorf("cache.Len(): expected <= %d, got %d", e, g)
}
if cache.Size() > capacity {
t.Errorf("cache.Size(): expected <= %d, got %d", capacity, cache.Size())
}
if e, g := expectedLength*len(value), cache.Size(); e < g {
t.Errorf("cache.Size(): expected <= %d, got %d", e, g)
}
return nil
}

View File

@ -0,0 +1,70 @@
package testsuite
import (
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
)
func testEviction(t *testing.T, store lfu.Store[string, string]) error {
cache := lfu.NewCache[string, string](store,
lfu.WithCapacity[string, string](10),
lfu.WithLog[string, string](t.Logf),
)
if err := cache.Set("key1", "key1"); err != nil {
return errors.WithStack(err)
}
if err := cache.Set("key2", "key2"); err != nil {
return errors.WithStack(err)
}
// Increment frequency of key2
if _, err := cache.Get("key2"); err != nil {
return errors.WithStack(err)
}
if e, g := 8, cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
if err := cache.Set("key3", "key3"); err != nil {
return errors.WithStack(err)
}
t.Logf("cache [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
_, err := cache.Get("key1")
if err == nil {
t.Errorf("expected 'key1' to be evicted")
}
if !errors.Is(err, lfu.ErrNotFound) {
t.Errorf("expected err to be 'ErrNotFound'")
}
value, err := cache.Get("key2")
if err != nil {
return errors.WithStack(err)
}
if e, g := "key2", value; e < g {
t.Errorf("cache.Get(\"key2\"): expected %v, got %v", e, g)
}
if e, g := cache.Capacity(), cache.Size(); e < g {
t.Errorf("cache.Size(): expected <= %d, got %d", e, g)
}
if e, g := 2, cache.Len(); e != g {
t.Errorf("cache.Len(): expected %d, got %d", e, g)
}
if cache.Size() < 0 {
t.Errorf("cache.Size(): expected value >= 0, got %d", cache.Size())
}
return nil
}

View File

@ -0,0 +1,80 @@
package testsuite
import (
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
)
func testMultipleSet(t *testing.T, store lfu.Store[string, string]) error {
const (
key = "mykey"
firstValue = "foo"
secondValue = "bar"
thirdValue = "foobar"
)
cache := lfu.NewCache[string, string](store)
if e, g := 0, cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
if err := cache.Set(key, firstValue); err != nil {
return errors.WithStack(err)
}
if e, g := len(firstValue), cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
retrieved, err := cache.Get(key)
if err != nil {
return errors.WithStack(err)
}
if e, g := firstValue, retrieved; e != g {
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
}
if err := cache.Set(key, secondValue); err != nil {
return errors.WithStack(err)
}
if e, g := len(secondValue), cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
retrieved, err = cache.Get(key)
if err != nil {
return errors.WithStack(err)
}
if e, g := secondValue, retrieved; e != g {
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
}
if err := cache.Set(key, thirdValue); err != nil {
return errors.WithStack(err)
}
if e, g := len(thirdValue), cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
retrieved, err = cache.Get(key)
if err != nil {
return errors.WithStack(err)
}
if e, g := thirdValue, retrieved; e != g {
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
}
if e, g := len(thirdValue), cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
return nil
}

View File

@ -0,0 +1,66 @@
package testsuite
import (
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
)
func testSetGetDelete(t *testing.T, store lfu.Store[string, string]) error {
const (
key = "mykey"
value = "foobar"
)
cache := lfu.NewCache[string, string](store, lfu.WithCapacity[string, string](10))
if e, g := 0, cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
if err := cache.Set(key, value); err != nil {
return errors.WithStack(err)
}
if e, g := len(value), cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
if e, g := 1, cache.Len(); e != g {
t.Errorf("cache.Len(): expected '%v', got '%v'", e, g)
}
retrieved, err := cache.Get(key)
if err != nil {
return errors.WithStack(err)
}
if e, g := value, retrieved; e != g {
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
}
if err := cache.Delete(key); err != nil {
return errors.WithStack(err)
}
if _, err := cache.Get(key); err == nil || !errors.Is(err, lfu.ErrNotFound) {
t.Errorf("cache.Get(key): err should be lfu.ErrNotFound, got '%v'", errors.WithStack(err))
}
if e, g := value, retrieved; e != g {
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
}
if e, g := 0, cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
if e, g := 0, cache.Len(); e != g {
t.Errorf("cache.Len(): expected '%v', got '%v'", e, g)
}
t.Logf("cache [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
return nil
}

View File

@ -0,0 +1,54 @@
package testsuite
import (
"testing"
"time"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
)
func testTTL(t *testing.T, store lfu.Store[string, string]) error {
const (
key = "mykey"
value = "foobar"
)
ttl := time.Second
cache := lfu.NewCache[string, string](store,
lfu.WithTTL[string, string](ttl),
lfu.WithCapacity[string, string](10),
)
if err := cache.Set(key, value); err != nil {
return errors.WithStack(err)
}
retrieved, err := cache.Get(key)
if err != nil {
return errors.WithStack(err)
}
if e, g := value, retrieved; e != g {
t.Errorf("cache.Get(key): expected '%v', got '%v'", e, g)
}
time.Sleep(ttl * 2)
if _, err := cache.Get(key); !errors.Is(err, lfu.ErrNotFound) {
t.Errorf("cache.Get(key): expected err == lfu.ErrNotFound, got '%v'", err)
}
t.Logf("cache [capacity: %d, size: %d, len: %d]", cache.Capacity(), cache.Size(), cache.Len())
if e, g := 0, cache.Size(); e != g {
t.Errorf("cache.Size(): expected '%v', got '%v'", e, g)
}
if e, g := 0, cache.Len(); e != g {
t.Errorf("cache.Len(): expected '%v', got '%v'", e, g)
}
return nil
}

View File

@ -3,14 +3,25 @@ package cache
import (
"time"
"github.com/allegro/bigcache/v3"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu/memory"
)
type Options struct {
CacheTTL time.Duration
BigCache bigcache.Config
BucketCacheSize int
CacheTTL time.Duration
BlobCacheStore lfu.Store[string, []byte]
// Maximum total size of cached data
BlobCacheSize int
BlobInfoCacheStore lfu.Store[string, storage.BlobInfo]
// Maximum number of blob infos
BlobInfoCacheSize int
BlobBucketCacheStore lfu.Store[string, storage.BlobBucket]
// Maximum number of blob bucket
BlobBucketCacheSize int
}
type OptionFunc func(opts *Options)
@ -18,10 +29,13 @@ type OptionFunc func(opts *Options)
func NewOptions(funcs ...OptionFunc) *Options {
defaultTTL := 60 * time.Minute
opts := &Options{
CacheTTL: defaultTTL,
BigCache: bigcache.DefaultConfig(defaultTTL),
BucketCacheSize: 16,
BlobInfoCacheSize: 256,
CacheTTL: defaultTTL,
BlobCacheStore: memory.NewStore[string, []byte](),
BlobCacheSize: 1e+9, // 1Gb
BlobInfoCacheStore: memory.NewStore[string, storage.BlobInfo](),
BlobInfoCacheSize: 256,
BlobBucketCacheStore: memory.NewStore[string, storage.BlobBucket](),
BlobBucketCacheSize: 16,
}
for _, fn := range funcs {
@ -37,15 +51,15 @@ func WithCacheTTL(ttl time.Duration) OptionFunc {
}
}
func WithBigCacheConfig(config bigcache.Config) OptionFunc {
func WithBlobBucketCacheSize(size int) OptionFunc {
return func(opts *Options) {
opts.BigCache = config
opts.BlobBucketCacheSize = size
}
}
func WithBucketCacheSize(size int) OptionFunc {
func WithBlobBucketCacheStore(store lfu.Store[string, storage.BlobBucket]) OptionFunc {
return func(opts *Options) {
opts.BucketCacheSize = size
opts.BlobBucketCacheStore = store
}
}
@ -54,3 +68,21 @@ func WithBlobInfoCacheSize(size int) OptionFunc {
opts.BlobInfoCacheSize = size
}
}
func WithBlobInfoCacheStore(store lfu.Store[string, storage.BlobInfo]) OptionFunc {
return func(opts *Options) {
opts.BlobInfoCacheStore = store
}
}
func WithBlobCacheSize(size int) OptionFunc {
return func(opts *Options) {
opts.BlobCacheSize = size
}
}
func WithBlobCacheStore(store lfu.Store[string, []byte]) OptionFunc {
return func(opts *Options) {
opts.BlobCacheStore = store
}
}

View File

@ -1,18 +1,21 @@
package cache
import (
"bytes"
"context"
"fmt"
"io"
"github.com/allegro/bigcache/v3"
"cdr.dev/slog"
"forge.cadoles.com/arcad/edge/pkg/storage/driver/cache/lfu"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type readCacher struct {
reader io.ReadSeekCloser
cache *bigcache.BigCache
cache *lfu.Cache[string, []byte]
buf bytes.Buffer
key string
}
@ -22,6 +25,21 @@ func (r *readCacher) Close() error {
return errors.WithStack(err)
}
if err := r.cache.Set(r.key, r.buf.Bytes()); err != nil {
var logErr slog.Field
if errors.Is(err, lfu.ErrSizeExceedCapacity) {
logErr = logger.E(errors.WithStack(err))
} else {
logErr = logger.CapturedE(errors.WithStack(err))
}
logger.Error(context.Background(), "could not cache buffered data",
logger.F("cacheKey", r.key),
logErr,
)
}
r.buf.Reset()
return nil
}
@ -37,13 +55,9 @@ func (r *readCacher) Read(p []byte) (n int, err error) {
}
if length > 0 {
if err := r.cache.Append(r.key, p[:length]); err != nil {
if _, err := r.buf.Write(p[:length]); err != nil {
ctx := logger.With(context.Background(), logger.F("cacheKey", r.key))
logger.Error(ctx, "could not write to buffer", logger.CapturedE(errors.WithStack(err)))
if err := r.cache.Delete(r.key); err != nil {
logger.Error(ctx, "could not delete cache key", logger.CapturedE(errors.WithStack(err)))
}
}
}

View File

@ -24,7 +24,7 @@ func TestBlobStore(t *testing.T) {
t.Fatalf("%+v", errors.WithStack(err))
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d&_pragma=journal_mode=wal", file, (60 * time.Second).Milliseconds())
store := NewBlobStore(dsn)
testsuite.TestBlobStore(context.Background(), t, store)

View File

@ -13,9 +13,14 @@ import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
_ "embed"
_ "modernc.org/sqlite"
)
//go:embed document_store.sql
var documentStoreSchema string
type DocumentStore struct {
getDB GetDBFunc
}
@ -48,7 +53,7 @@ func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.D
err := s.withTx(ctx, func(tx *sql.Tx) error {
query := `
SELECT id, data, created_at, updated_at
SELECT id, revision, data, created_at, updated_at
FROM documents
WHERE collection = $1 AND id = $2
`
@ -59,9 +64,10 @@ func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.D
createdAt time.Time
updatedAt time.Time
data JSONMap
revision int
)
err := row.Scan(&id, &data, &createdAt, &updatedAt)
err := row.Scan(&id, &revision, &data, &createdAt, &updatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errors.WithStack(storage.ErrDocumentNotFound)
@ -77,6 +83,7 @@ func (s *DocumentStore) Get(ctx context.Context, collection string, id storage.D
document = storage.Document(data)
document[storage.DocumentAttrID] = id
document[storage.DocumentAttrRevision] = revision
document[storage.DocumentAttrCreatedAt] = createdAt
document[storage.DocumentAttrUpdatedAt] = updatedAt
@ -119,7 +126,7 @@ func (s *DocumentStore) Query(ctx context.Context, collection string, filter *fi
}
query := `
SELECT id, data, created_at, updated_at
SELECT id, revision, data, created_at, updated_at
FROM documents
WHERE collection = $1 AND (` + criteria + `)
`
@ -171,17 +178,19 @@ func (s *DocumentStore) Query(ctx context.Context, collection string, filter *fi
for rows.Next() {
var (
id storage.DocumentID
revision int
createdAt time.Time
updatedAt time.Time
data JSONMap
)
if err := rows.Scan(&id, &data, &createdAt, &updatedAt); err != nil {
if err := rows.Scan(&id, &revision, &data, &createdAt, &updatedAt); err != nil {
return errors.WithStack(err)
}
document := storage.Document(data)
document[storage.DocumentAttrID] = id
document[storage.DocumentAttrRevision] = revision
document[storage.DocumentAttrCreatedAt] = createdAt
document[storage.DocumentAttrUpdatedAt] = updatedAt
@ -206,22 +215,16 @@ func (s *DocumentStore) Upsert(ctx context.Context, collection string, document
var upsertedDocument storage.Document
err := s.withTx(ctx, func(tx *sql.Tx) error {
query := `
INSERT INTO documents (id, collection, data, created_at, updated_at)
VALUES($1, $2, $3, $4, $4)
ON CONFLICT (id, collection) DO UPDATE SET
data = $3, updated_at = $4
RETURNING "id", "data", "created_at", "updated_at"
`
now := time.Now().UTC()
id, exists := document.ID()
if !exists || id == "" {
id = storage.NewDocumentID()
}
args := []any{id, collection, JSONMap(document), now, now}
query := `
SELECT revision FROM documents WHERE id = $1
`
args := []any{id}
logger.Debug(
ctx, "executing query",
@ -231,14 +234,44 @@ func (s *DocumentStore) Upsert(ctx context.Context, collection string, document
row := tx.QueryRowContext(ctx, query, args...)
var storedRevision int
if err := row.Scan(&storedRevision); err != nil && !errors.Is(err, sql.ErrNoRows) {
return errors.WithStack(err)
}
revision, found := document.Revision()
if found && storedRevision != revision {
return errors.Wrapf(storage.ErrDocumentRevisionConflict, "document revision '%d' does not match stored '%d'", revision, storedRevision)
}
query = `
INSERT INTO documents (id, collection, data, created_at, updated_at)
VALUES($1, $2, $3, $4, $4)
ON CONFLICT (id, collection) DO UPDATE SET
data = $3, updated_at = $4, revision = revision + 1
RETURNING "id", "revision", "data", "created_at", "updated_at"
`
now := time.Now().UTC()
args = []any{id, collection, JSONMap(document), now, now}
logger.Debug(
ctx, "executing query",
logger.F("query", query),
logger.F("args", args),
)
row = tx.QueryRowContext(ctx, query, args...)
var (
createdAt time.Time
updatedAt time.Time
data JSONMap
)
err := row.Scan(&id, &data, &createdAt, &updatedAt)
if err != nil {
if err := row.Scan(&id, &revision, &data, &createdAt, &updatedAt); err != nil {
return errors.WithStack(err)
}
@ -249,6 +282,7 @@ func (s *DocumentStore) Upsert(ctx context.Context, collection string, document
upsertedDocument = storage.Document(data)
upsertedDocument[storage.DocumentAttrID] = id
upsertedDocument[storage.DocumentAttrRevision] = revision
upsertedDocument[storage.DocumentAttrCreatedAt] = createdAt
upsertedDocument[storage.DocumentAttrUpdatedAt] = updatedAt
@ -276,29 +310,13 @@ func (s *DocumentStore) withTx(ctx context.Context, fn func(tx *sql.Tx) error) e
return nil
}
func ensureDocumentTables(ctx context.Context, db *sql.DB) error {
func migrateSchema(ctx context.Context, db *sql.DB) error {
err := WithTx(ctx, db, func(tx *sql.Tx) error {
query := `
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
collection TEXT NOT NULL,
data TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE(id, collection) ON CONFLICT REPLACE
);
`
if _, err := tx.ExecContext(ctx, query); err != nil {
return errors.WithStack(err)
for _, migr := range documentStoreMigrations {
if err := migr(ctx, tx); err != nil {
return errors.WithStack(err)
}
}
query = `
CREATE INDEX IF NOT EXISTS collection_idx ON documents (collection);
`
if _, err := tx.ExecContext(ctx, query); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
@ -344,7 +362,7 @@ func withLimitOffsetClause(query string, args []any, limit int, offset int) (str
}
func NewDocumentStore(path string) *DocumentStore {
getDB := NewGetDBFunc(path, ensureDocumentTables)
getDB := NewGetDBFunc(path, migrateSchema)
return &DocumentStore{
getDB: getDB,
@ -352,7 +370,7 @@ func NewDocumentStore(path string) *DocumentStore {
}
func NewDocumentStoreWithDB(db *sql.DB) *DocumentStore {
getDB := NewGetDBFuncFromDB(db, ensureDocumentTables)
getDB := NewGetDBFuncFromDB(db, migrateSchema)
return &DocumentStore{
getDB: getDB,

View File

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
collection TEXT NOT NULL,
data TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE(id, collection) ON CONFLICT REPLACE
);
---
CREATE INDEX IF NOT EXISTS collection_idx ON documents (collection);
---
ALTER TABLE documents ADD COLUMN revision INTEGER DEFAULT 0;

View File

@ -0,0 +1,98 @@
package sqlite
import (
"context"
"database/sql"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type MigrateFunc func(ctx context.Context, tx *sql.Tx) error
var documentStoreMigrations = []MigrateFunc{
documentStoreMigrationBaseSchema,
documentStoreMigrationAddIDIndex,
documentStoreMigrationAddRevisionColumn,
}
func documentStoreMigrationBaseSchema(ctx context.Context, tx *sql.Tx) error {
query := `
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
collection TEXT NOT NULL,
data TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE(id, collection) ON CONFLICT REPLACE
);
`
if _, err := tx.ExecContext(ctx, query); err != nil {
return errors.WithStack(err)
}
return nil
}
func documentStoreMigrationAddIDIndex(ctx context.Context, tx *sql.Tx) error {
query := `
CREATE INDEX IF NOT EXISTS collection_idx ON documents (collection);
`
if _, err := tx.ExecContext(ctx, query); err != nil {
return errors.WithStack(err)
}
return nil
}
func documentStoreMigrationAddRevisionColumn(ctx context.Context, tx *sql.Tx) error {
query := `PRAGMA table_info(documents)`
rows, err := tx.QueryContext(ctx, query)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := rows.Close(); err != nil {
logger.Error(ctx, "could not close rows", logger.CapturedE(errors.WithStack(err)))
}
}()
var hasRevisionColumn bool
for rows.Next() {
var (
id int
name string
dataType string
nullable int
defaultValue any
primaryKey int
)
if err := rows.Scan(&id, &name, &dataType, &nullable, &defaultValue, &primaryKey); err != nil {
return errors.WithStack(err)
}
if name == "revision" {
hasRevisionColumn = true
break
}
}
if err := rows.Err(); err != nil {
return errors.WithStack(err)
}
if !hasRevisionColumn {
query = `ALTER TABLE documents ADD COLUMN revision INTEGER DEFAULT 0`
if _, err := tx.ExecContext(ctx, query); err != nil {
return errors.WithStack(err)
}
}
return nil
}

View File

@ -22,7 +22,7 @@ func TestDocumentStore(t *testing.T) {
t.Fatalf("%+v", errors.WithStack(err))
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d&_pragma=journal_mode=wal", file, (60 * time.Second).Milliseconds())
store := NewDocumentStore(dsn)
testsuite.TestDocumentStore(context.Background(), t, store)

View File

@ -26,7 +26,7 @@ func newTestStore(testName string) (share.Store, error) {
return nil, errors.WithStack(err)
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d&_pragma=journal_mode=wal", file, (60 * time.Second).Milliseconds())
store := NewShareStore(dsn)
return store, nil

View File

@ -185,8 +185,13 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
return errors.Errorf("upsertedDoc[\"attr1\"]: expected '%v', got '%v'", e, g)
}
upsertedDocRevision, _ := upsertedDoc.Revision()
if e, g := 0, upsertedDocRevision; e != g {
return errors.Errorf("upsertedDoc.Revision(): expected '%v', got '%v'", e, g)
}
// Check that document does not have unexpected properties
if e, g := 4, len(upsertedDoc); e != g {
if e, g := 5, len(upsertedDoc); e != g {
return errors.Errorf("len(upsertedDoc): expected '%v', got '%v'", e, g)
}
@ -217,6 +222,11 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
return errors.New("upsertedDoc2.UpdatedAt() should have been different than upsertedDoc.UpdatedAt()")
}
upsertedDoc2Revision, _ := upsertedDoc2.Revision()
if e, g := 1, upsertedDoc2Revision; e != g {
return errors.Errorf("upsertedDoc.Revision(): expected '%v', got '%v'", e, g)
}
// Verify that there is no additional created document in the collection
results, err := store.Query(ctx, collection, nil)
@ -228,6 +238,11 @@ var documentStoreOpsTestCases = []documentStoreOpsTestCase{
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
}
firstResultRevision, _ := results[0].Revision()
if e, g := 1, firstResultRevision; e != g {
return errors.Errorf("results[0].Revision(): expected '%v', got '%v'", e, g)
}
return nil
},
},
@ -437,7 +452,6 @@ func testDocumentStoreOps(ctx context.Context, t *testing.T, store storage.Docum
for _, tc := range documentStoreOpsTestCases {
func(tc documentStoreOpsTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
if err := tc.Run(ctx, store); err != nil {
t.Errorf("%+v", errors.WithStack(err))
}