Compare commits
31 Commits
zim-bundle
...
master
Author | SHA1 | Date |
---|---|---|
wpetit | 1029cc8f0f | |
wpetit | 0f673671b8 | |
wpetit | 6aec6da078 | |
wpetit | 2fbc7186c0 | |
wpetit | 7633ae0419 | |
wpetit | 335b34625b | |
wpetit | 776dbba5b0 | |
wpetit | 8f9428b3f3 | |
wpetit | a268759d33 | |
wpetit | a276b92a03 | |
wpetit | b9c08f647c | |
wpetit | 59f023a7d9 | |
wpetit | 753a6c9708 | |
wpetit | b120e590b6 | |
wpetit | 242bf379a8 | |
wpetit | 065a9002a0 | |
wpetit | 83a1e89665 | |
wpetit | d9e8aac458 | |
wpetit | 32f04af138 | |
wpetit | 870db072e0 | |
wpetit | ad49c1718c | |
wpetit | f4a7366aad | |
wpetit | 02c74b6f8d | |
wpetit | 8889694125 | |
wpetit | 6a99409a15 | |
wpetit | 2fc590d708 | |
wpetit | 6e4bf2f025 | |
wpetit | 22a3326be9 | |
wpetit | 0cfb132b65 | |
wpetit | de4ab0d02c | |
wpetit | d1458bab4a |
|
@ -1,4 +1,4 @@
|
||||||
RUN_APP_ARGS=""
|
RUN_APP_ARGS=""
|
||||||
#EDGE_DOCUMENTSTORE_DSN="rpc://localhost:3001/documentstore?tenant=local&appId=%APPID%"
|
#EDGE_DOCUMENTSTORE_DSN="rpc://localhost:3001/documentstore?tenant=local&appId=%APPID%"
|
||||||
#EDGE_BLOBSTORE_DSN="rpc://localhost:3001/blobstore?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"
|
#EDGE_SHARESTORE_DSN="rpc://localhost:3001/sharestore?tenant=local"
|
|
@ -2,7 +2,7 @@
|
||||||
/bin
|
/bin
|
||||||
/.env
|
/.env
|
||||||
/tools
|
/tools
|
||||||
*.sqlite
|
*.sqlite*
|
||||||
/.gitea-release
|
/.gitea-release
|
||||||
/.edge
|
/.edge
|
||||||
/data
|
/data
|
||||||
|
|
|
@ -108,10 +108,17 @@ nfpms:
|
||||||
file_info:
|
file_info:
|
||||||
mode: 0640
|
mode: 0640
|
||||||
packager: apk
|
packager: apk
|
||||||
|
- src: misc/packaging/openrc/storage-server.logrotate.conf
|
||||||
|
dst: /etc/logrotate.d/storage-server
|
||||||
|
packager: apk
|
||||||
- dst: /var/lib/storage-server
|
- dst: /var/lib/storage-server
|
||||||
type: dir
|
type: dir
|
||||||
file_info:
|
file_info:
|
||||||
mode: 0700
|
mode: 0700
|
||||||
packager: apk
|
packager: apk
|
||||||
|
- dst: /var/log/storage-server
|
||||||
|
type: dir
|
||||||
|
file_info:
|
||||||
|
mode: 0700
|
||||||
scripts:
|
scripts:
|
||||||
postinstall: "misc/packaging/common/postinstall-storage-server.sh"
|
postinstall: "misc/packaging/common/postinstall-storage-server.sh"
|
|
@ -0,0 +1,146 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
mrand "math/rand"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
|
||||||
|
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/cache"
|
||||||
|
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
|
||||||
|
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dsn string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&dsn, "dsn", "cache://./test-cache.sqlite?driver=sqlite&_pragma=foreign_keys(1)&_pragma=journal_mode=wal&bigCacheShards=32&bigCacheHardMaxCacheSize=128&bigCacheMaxEntrySize=125&bigCacheMaxEntriesInWindow=200000", "blobstore dsn")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
|
||||||
|
blobStore, err := driver.NewBlobStore(dsn)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(ctx, "could not create blobstore", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket, err := blobStore.OpenBucket(ctx, "default")
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(ctx, "could not open bucket", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := bucket.Close(); err != nil {
|
||||||
|
logger.Fatal(ctx, "could not close bucket", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go readRandomBlobs(ctx, bucket)
|
||||||
|
|
||||||
|
for {
|
||||||
|
writeRandomBlob(ctx, bucket)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
size, err := bucket.Size(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(ctx, "could not retrieve bucket size", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(ctx, "bucket stats", logger.F("size", size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRandomBlobs(ctx context.Context, bucket storage.BlobBucket) {
|
||||||
|
for {
|
||||||
|
infos, err := bucket.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(ctx, "could not list blobs", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
total := len(infos)
|
||||||
|
if total == 0 {
|
||||||
|
logger.Debug(ctx, "no blob yet")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
blob := infos[mrand.Intn(total)]
|
||||||
|
|
||||||
|
readBlob(ctx, bucket, blob.ID())
|
||||||
|
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBlob(ctx context.Context, bucket storage.BlobBucket, blobID storage.BlobID) {
|
||||||
|
ctx = logger.With(ctx, logger.F("blobID", blobID))
|
||||||
|
|
||||||
|
reader, err := bucket.NewReader(ctx, blobID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(ctx, "could not create reader", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
logger.Fatal(ctx, "could not close reader", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := io.ReadAll(reader); err != nil {
|
||||||
|
logger.Fatal(ctx, "could not read blob", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRandomBlob(ctx context.Context, bucket storage.BlobBucket) {
|
||||||
|
blobID := storage.NewBlobID()
|
||||||
|
buff := make([]byte, 10*1024)
|
||||||
|
|
||||||
|
writer, err := bucket.NewWriter(ctx, blobID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(ctx, "could not create writer", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
logger.Fatal(ctx, "could not close writer", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := rand.Read(buff); err != nil {
|
||||||
|
logger.Fatal(ctx, "could not read random data", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := writer.Write(buff); err != nil {
|
||||||
|
logger.Fatal(ctx, "could not write blob", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
|
||||||
|
printMemUsage(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printMemUsage(ctx context.Context) {
|
||||||
|
var m runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&m)
|
||||||
|
logger.Debug(
|
||||||
|
ctx, "memory usage",
|
||||||
|
logger.F("alloc", m.Alloc/1024/1024),
|
||||||
|
logger.F("totalAlloc", m.TotalAlloc/1024/1024),
|
||||||
|
logger.F("sys", m.Sys/1024/1024),
|
||||||
|
logger.F("numGC", m.NumGC),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InfoCommand() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "info",
|
||||||
|
Usage: "Print app manifest informations",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "path",
|
||||||
|
Usage: "use `PATH` as app bundle (zip, zim or directory bundle)",
|
||||||
|
Aliases: []string{"p"},
|
||||||
|
Value: "",
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
appPath := ctx.String("path")
|
||||||
|
|
||||||
|
bundle, err := bundle.FromPath(appPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not load app bundle")
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := app.LoadManifest(bundle)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not load app manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
if valid, err := manifest.Validate(manifestMetadataValidators...); !valid {
|
||||||
|
return errors.Wrap(err, "invalid app manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := yaml.NewEncoder(os.Stdout)
|
||||||
|
|
||||||
|
if err := encoder.Encode(manifest); err != nil {
|
||||||
|
return errors.Wrap(err, "could not encode manifest")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.Close(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ func Root() *cli.Command {
|
||||||
RunCommand(),
|
RunCommand(),
|
||||||
PackageCommand(),
|
PackageCommand(),
|
||||||
HashPasswordCommand(),
|
HashPasswordCommand(),
|
||||||
|
InfoCommand(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,11 @@ import (
|
||||||
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
|
authModule "forge.cadoles.com/arcad/edge/pkg/module/auth"
|
||||||
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
|
||||||
authModuleMiddleware "forge.cadoles.com/arcad/edge/pkg/module/auth/middleware"
|
authModuleMiddleware "forge.cadoles.com/arcad/edge/pkg/module/auth/middleware"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
blobModule "forge.cadoles.com/arcad/edge/pkg/module/blob"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
castModule "forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
||||||
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
|
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
|
||||||
|
rpcModule "forge.cadoles.com/arcad/edge/pkg/module/rpc"
|
||||||
shareModule "forge.cadoles.com/arcad/edge/pkg/module/share"
|
shareModule "forge.cadoles.com/arcad/edge/pkg/module/share"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
@ -38,16 +39,24 @@ import (
|
||||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"github.com/wlynxg/anet"
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
||||||
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
|
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
|
||||||
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
|
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
|
||||||
|
|
||||||
// Register storage drivers
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
||||||
|
|
||||||
|
// Register storage drivers
|
||||||
|
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/cache"
|
||||||
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
|
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
|
||||||
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
_ "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"
|
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -103,6 +112,11 @@ func RunCommand() *cli.Command {
|
||||||
Usage: "use `FILE` as local accounts",
|
Usage: "use `FILE` as local accounts",
|
||||||
Value: ".edge/%APPID%/accounts.json",
|
Value: ".edge/%APPID%/accounts.json",
|
||||||
},
|
},
|
||||||
|
&cli.Int64Flag{
|
||||||
|
Name: "max-upload-size",
|
||||||
|
Usage: "use `MAX-UPLOAD-SIZE` as blob max upload size",
|
||||||
|
Value: 128 << (10 * 2), // 128Mb
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
address := ctx.String("address")
|
address := ctx.String("address")
|
||||||
|
@ -114,6 +128,7 @@ func RunCommand() *cli.Command {
|
||||||
documentstoreDSN := ctx.String("documentstore-dsn")
|
documentstoreDSN := ctx.String("documentstore-dsn")
|
||||||
shareStoreDSN := ctx.String("sharestore-dsn")
|
shareStoreDSN := ctx.String("sharestore-dsn")
|
||||||
accountsFile := ctx.String("accounts-file")
|
accountsFile := ctx.String("accounts-file")
|
||||||
|
maxUploadSize := ctx.Int64("max-upload-size")
|
||||||
|
|
||||||
logger.SetFormat(logger.Format(logFormat))
|
logger.SetFormat(logger.Format(logFormat))
|
||||||
logger.SetLevel(logger.Level(logLevel))
|
logger.SetLevel(logger.Level(logLevel))
|
||||||
|
@ -159,7 +174,7 @@ func RunCommand() *cli.Command {
|
||||||
|
|
||||||
appCtx := logger.With(cmdCtx, logger.F("address", address))
|
appCtx := logger.With(cmdCtx, logger.F("address", address))
|
||||||
|
|
||||||
if err := runApp(appCtx, path, address, documentstoreDSN, blobstoreDSN, shareStoreDSN, accountsFile, appsRepository); err != nil {
|
if err := runApp(appCtx, path, address, documentstoreDSN, blobstoreDSN, shareStoreDSN, accountsFile, appsRepository, maxUploadSize); err != nil {
|
||||||
logger.Error(appCtx, "could not run app", logger.CapturedE(errors.WithStack(err)))
|
logger.Error(appCtx, "could not run app", logger.CapturedE(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
}(p, port, idx)
|
}(p, port, idx)
|
||||||
|
@ -172,7 +187,7 @@ func RunCommand() *cli.Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN, shareStoreDSN, accountsFile string, appRepository appModule.Repository) error {
|
func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN, shareStoreDSN, accountsFile string, appRepository appModule.Repository, maxUploadSize int64) error {
|
||||||
absPath, err := filepath.Abs(path)
|
absPath, err := filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "could not resolve path '%s'", path)
|
return errors.Wrapf(err, "could not resolve path '%s'", path)
|
||||||
|
@ -233,9 +248,11 @@ func runApp(ctx context.Context, path, address, documentStoreDSN, blobStoreDSN,
|
||||||
return jwtutil.NewSymmetricKeySet(dummySecret)
|
return jwtutil.NewSymmetricKeySet(dummySecret)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
blobModule.Mount(maxUploadSize), // 10Mb,
|
||||||
|
fetchModule.Mount(),
|
||||||
),
|
),
|
||||||
appHTTP.WithHTTPMiddlewares(
|
appHTTP.WithHTTPMiddlewares(
|
||||||
authModuleMiddleware.AnonymousUser(key, jwa.HS256),
|
authModuleMiddleware.DefaultUser(key, jwa.HS256, authModuleMiddleware.WithAnonymousUser()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err := handler.Load(ctx, bundle); err != nil {
|
if err := handler.Load(ctx, bundle); err != nil {
|
||||||
|
@ -275,18 +292,18 @@ func getServerModules(deps *moduleDeps) []app.ServerModuleFactory {
|
||||||
module.LifecycleModuleFactory(),
|
module.LifecycleModuleFactory(),
|
||||||
module.ContextModuleFactory(),
|
module.ContextModuleFactory(),
|
||||||
module.ConsoleModuleFactory(),
|
module.ConsoleModuleFactory(),
|
||||||
cast.CastModuleFactory(),
|
castModule.CastModuleFactory(),
|
||||||
netModule.ModuleFactory(deps.Bus),
|
netModule.ModuleFactory(deps.Bus),
|
||||||
module.RPCModuleFactory(deps.Bus),
|
rpcModule.ModuleFactory(deps.Bus),
|
||||||
module.StoreModuleFactory(deps.DocumentStore),
|
module.StoreModuleFactory(deps.DocumentStore),
|
||||||
blob.ModuleFactory(deps.Bus, deps.BlobStore),
|
blobModule.ModuleFactory(deps.Bus, deps.BlobStore),
|
||||||
authModule.ModuleFactory(
|
authModule.ModuleFactory(
|
||||||
authModule.WithJWT(func() (jwk.Set, error) {
|
authModule.WithJWT(func() (jwk.Set, error) {
|
||||||
return jwtutil.NewSymmetricKeySet(dummySecret)
|
return jwtutil.NewSymmetricKeySet(dummySecret)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
appModule.ModuleFactory(deps.AppRepository),
|
appModule.ModuleFactory(deps.AppRepository),
|
||||||
fetch.ModuleFactory(deps.Bus),
|
fetchModule.ModuleFactory(deps.Bus),
|
||||||
shareModule.ModuleFactory(deps.AppID, deps.ShareStore),
|
shareModule.ModuleFactory(deps.AppID, deps.ShareStore),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,13 +361,13 @@ func findMatchingDeviceAddress(ctx context.Context, from string, defaultAddr str
|
||||||
return defaultAddr, nil
|
return defaultAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := anet.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ifa := range ifaces {
|
for _, ifa := range ifaces {
|
||||||
addrs, err := ifa.Addrs()
|
addrs, err := anet.InterfaceAddrsByInterface(&ifa)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx, "could not retrieve iface adresses",
|
ctx, "could not retrieve iface adresses",
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package cast
|
package cast
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -21,16 +24,25 @@ func LoadURLCommand() *cli.Command {
|
||||||
Aliases: []string{"u"},
|
Aliases: []string{"u"},
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "timeout",
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Value: 10 * time.Second,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
device := ctx.String("device")
|
device := ctx.String("device")
|
||||||
url := ctx.String("url")
|
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)
|
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)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,10 @@ package cast
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/urfave/cli/v2"
|
"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 {
|
func Root() *cli.Command {
|
||||||
|
@ -11,6 +15,8 @@ func Root() *cli.Command {
|
||||||
Subcommands: []*cli.Command{
|
Subcommands: []*cli.Command{
|
||||||
ScanCommand(),
|
ScanCommand(),
|
||||||
LoadURLCommand(),
|
LoadURLCommand(),
|
||||||
|
StatusCommand(),
|
||||||
|
StopCastCommand(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ func ScanCommand() *cli.Command {
|
||||||
&cli.DurationFlag{
|
&cli.DurationFlag{
|
||||||
Name: "timeout",
|
Name: "timeout",
|
||||||
Aliases: []string{"t"},
|
Aliases: []string{"t"},
|
||||||
Value: 30 * time.Second,
|
Value: 10 * time.Second,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: func(ctx *cli.Context) error {
|
Action: func(ctx *cli.Context) error {
|
||||||
|
@ -32,8 +32,8 @@ func ScanCommand() *cli.Command {
|
||||||
log.Fatalf("%+v", errors.WithStack(err))
|
log.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
for dev := range devices {
|
for _, dev := range devices {
|
||||||
log.Printf("[DEVICE] %s %s %s:%d", dev.UUID, dev.Name, dev.Host.String(), dev.Port)
|
log.Printf("[DEVICE] %s %s %s %s:%d", dev.DeviceID(), dev.DeviceType(), dev.DeviceName(), dev.DeviceHost().String(), dev.DevicePort())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,13 +22,15 @@ import (
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
|
||||||
// Register storage drivers
|
// Register storage drivers
|
||||||
|
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/cache"
|
||||||
|
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
|
||||||
|
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/cmd/storage-server/command/flag"
|
"forge.cadoles.com/arcad/edge/cmd/storage-server/command/flag"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
||||||
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver/rpc/server"
|
||||||
_ "forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
"forge.cadoles.com/arcad/edge/pkg/storage/share"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,17 +53,17 @@ func Run() *cli.Command {
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "blobstore-dsn-pattern",
|
Name: "blobstore-dsn-pattern",
|
||||||
EnvVars: []string{"STORAGE_SERVER_BLOBSTORE_DSN_PATTERN"},
|
EnvVars: []string{"STORAGE_SERVER_BLOBSTORE_DSN_PATTERN"},
|
||||||
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/blobstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
|
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/blobstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d&_pragma=journal_mode=wal", (60 * time.Second).Milliseconds()),
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "documentstore-dsn-pattern",
|
Name: "documentstore-dsn-pattern",
|
||||||
EnvVars: []string{"STORAGE_SERVER_DOCUMENTSTORE_DSN_PATTERN"},
|
EnvVars: []string{"STORAGE_SERVER_DOCUMENTSTORE_DSN_PATTERN"},
|
||||||
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/documentstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
|
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/%%APPID%%/documentstore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d&_pragma=journal_mode=wal", (60 * time.Second).Milliseconds()),
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sharestore-dsn-pattern",
|
Name: "sharestore-dsn-pattern",
|
||||||
EnvVars: []string{"STORAGE_SERVER_SHARESTORE_DSN_PATTERN"},
|
EnvVars: []string{"STORAGE_SERVER_SHARESTORE_DSN_PATTERN"},
|
||||||
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/sharestore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", (60 * time.Second).Milliseconds()),
|
Value: fmt.Sprintf("sqlite://data/%%TENANT%%/sharestore.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d&_pragma=journal_mode=wal", (60 * time.Second).Milliseconds()),
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "sentry-dsn",
|
Name: "sentry-dsn",
|
||||||
|
|
62
go.mod
62
go.mod
|
@ -1,77 +1,89 @@
|
||||||
module forge.cadoles.com/arcad/edge
|
module forge.cadoles.com/arcad/edge
|
||||||
|
|
||||||
go 1.21
|
go 1.21.4
|
||||||
|
|
||||||
|
toolchain go1.21.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
forge.cadoles.com/arcad/arcast v0.0.0-20231220090835-5d0311b7315d
|
||||||
github.com/getsentry/sentry-go v0.25.0
|
github.com/getsentry/sentry-go v0.25.0
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||||
github.com/hashicorp/mdns v1.0.5
|
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/keegancsmith/rpc v1.3.0
|
||||||
github.com/klauspost/compress v1.16.6
|
github.com/klauspost/compress v1.16.6
|
||||||
github.com/lestrrat-go/jwx/v2 v2.0.8
|
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/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
|
modernc.org/sqlite v1.20.4
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
github.com/goccy/go-json v0.9.11 // indirect
|
github.com/goccy/go-json v0.9.11 // indirect
|
||||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // 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/leodido/go-urn v1.2.1 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||||
github.com/lestrrat-go/httpcc 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/httprc v1.0.4 // indirect
|
||||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
github.com/lestrrat-go/option v1.0.0 // indirect
|
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||||
github.com/miekg/dns v1.1.53 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
google.golang.org/grpc v1.35.0 // 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
|
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cdr.dev/slog v1.4.0
|
cdr.dev/slog v1.6.1
|
||||||
github.com/alecthomas/chroma v0.7.0 // indirect
|
|
||||||
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692
|
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
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/davecgh/go-spew v1.1.1
|
||||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||||
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32
|
github.com/dop251/goja v0.0.0-20230203172422-5460598cfa32
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097
|
github.com/dop251/goja_nodejs v0.0.0-20230207183254-2229640ea097
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
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/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/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/google/uuid v1.3.0
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
github.com/igm/sockjs-go/v3 v3.0.2
|
github.com/igm/sockjs-go/v3 v3.0.2
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
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.19 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/oklog/ulid/v2 v2.1.0
|
github.com/oklog/ulid/v2 v2.1.0
|
||||||
github.com/orcaman/concurrent-map v1.0.0
|
github.com/orcaman/concurrent-map v1.0.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // 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
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20231019192040-4c72331a7648
|
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07
|
||||||
go.opencensus.io v0.22.5 // indirect
|
golang.org/x/crypto v0.16.0
|
||||||
golang.org/x/crypto v0.10.0
|
golang.org/x/mod v0.14.0
|
||||||
golang.org/x/mod v0.10.0
|
golang.org/x/net v0.19.0
|
||||||
golang.org/x/net v0.11.0
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.9.0 // indirect
|
golang.org/x/term v0.15.0 // indirect
|
||||||
golang.org/x/term v0.9.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
golang.org/x/text v0.10.0 // indirect
|
golang.org/x/tools v0.16.1 // indirect
|
||||||
golang.org/x/tools v0.8.0 // indirect
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
lukechampine.com/uint128 v1.2.0 // indirect
|
lukechampine.com/uint128 v1.2.0 // indirect
|
||||||
modernc.org/cc/v3 v3.40.0 // indirect
|
modernc.org/cc/v3 v3.40.0 // indirect
|
||||||
|
@ -83,3 +95,5 @@ require (
|
||||||
modernc.org/strutil v1.1.3 // indirect
|
modernc.org/strutil v1.1.3 // indirect
|
||||||
modernc.org/token v1.0.1 // indirect
|
modernc.org/token v1.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/allegro/bigcache/v3 v3.1.0 => github.com/Bornholm/bigcache v0.0.0-20231201111725-1ddf51584cad
|
||||||
|
|
600
go.sum
600
go.sum
|
@ -1,74 +1,26 @@
|
||||||
cdr.dev/slog v1.4.0 h1:tLXQJ/hZ5Q051h0MBHSd2Ha8xzdXj7CjtzmG/8dUvUk=
|
cdr.dev/slog v1.6.1 h1:IQjWZD0x6//sfv5n+qEhbu3wBkmtBQY5DILXNvMaIv4=
|
||||||
cdr.dev/slog v1.4.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns=
|
cdr.dev/slog v1.6.1/go.mod h1:eHEYQLaZvxnIAXC+XdTSNLb/kgA/X2RVSF72v5wsxEI=
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
|
||||||
cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY=
|
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
|
||||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
forge.cadoles.com/arcad/arcast v0.0.0-20231220090835-5d0311b7315d h1:SPDaDDF5StoprDqop8j8zozs8xK32EEWnUHLccWplKM=
|
||||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
forge.cadoles.com/arcad/arcast v0.0.0-20231220090835-5d0311b7315d/go.mod h1:QR8p4kUScWBcTQ0dE/gR+2ndpKOx77mIDSYGqSF1gms=
|
||||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
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/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=
|
|
||||||
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 h1:JW4WZlqyaNWUUahfr7MigeDW6jmtam5cTzzo1lwsFhE=
|
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/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/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
|
||||||
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/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
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/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/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.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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
@ -76,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/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 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
|
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.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 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
@ -91,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/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 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
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 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
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 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
|
||||||
github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
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.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
|
||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
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-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-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
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 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
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=
|
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||||
|
@ -121,99 +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/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 h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY=
|
||||||
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
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/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
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/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
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/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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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/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 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
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 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 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
|
||||||
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
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 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE=
|
||||||
github.com/igm/sockjs-go/v3 v3.0.2/go.mod h1:UqchsOjeagIBFHvd+RZpLaVRbCwGilEC08EDHsD1jYE=
|
github.com/igm/sockjs-go/v3 v3.0.2/go.mod h1:UqchsOjeagIBFHvd+RZpLaVRbCwGilEC08EDHsD1jYE=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
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/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 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
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 h1:wGWOpjcNrZaY8GDYZJfvyxmlLljm3YQWF+p918DXtDk=
|
||||||
github.com/keegancsmith/rpc v1.3.0/go.mod h1:6O2xnOGjPyvIPbvp0MdrOe5r6cu1GZ4JoTzpzDhWeo0=
|
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 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||||
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
@ -238,410 +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/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 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
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/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
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/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 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.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||||
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
|
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||||
github.com/miekg/dns v1.1.53/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
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 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
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 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
|
||||||
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
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/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 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
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.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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
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.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.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.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.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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
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 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
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 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.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
|
||||||
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/wlynxg/anet v0.0.1 h1:VbkEEgHxPSrRQSiyRd0pmrbcEQAEU2TTb8fb4DmSYoQ=
|
||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
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 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
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=
|
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-20231215190137-4a8add1d3d07 h1:0V95X1cBpdj5zyOe6oGtn/BQHlRpV8WlL3eTs3jaxiA=
|
||||||
gitlab.com/wpetit/goweb v0.0.0-20231019192040-4c72331a7648/go.mod h1:WdxGjM3HJWgBkUa4TwaTXUqY2BnRKlNSyUIv1aF4jxk=
|
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07/go.mod h1:Nfr7aZPiSN6biFumhiHbh9k8A3rKQRzR+o0bVtv78UY=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
|
||||||
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
|
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
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-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-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.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.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20161013035702-8b4af36cd21a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-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-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-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-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-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.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.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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||||
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-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-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-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-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-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-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-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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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-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.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.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.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.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.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.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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-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.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.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||||
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||||
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc=
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o=
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
@ -657,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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||||
|
@ -692,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/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 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
|
||||||
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
|
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=
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ ARG HTTP_PROXY=
|
||||||
ARG HTTPS_PROXY=
|
ARG HTTPS_PROXY=
|
||||||
ARG http_proxy=
|
ARG http_proxy=
|
||||||
ARG https_proxy=
|
ARG https_proxy=
|
||||||
ARG GO_VERSION=1.20.2
|
ARG GO_VERSION=1.21.5
|
||||||
|
|
||||||
# Install dev environment dependencies
|
# Install dev environment dependencies
|
||||||
RUN export DEBIAN_FRONTEND=noninteractive &&\
|
RUN export DEBIAN_FRONTEND=noninteractive &&\
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
/var/log/storage-server/storage-server.log {
|
||||||
|
missingok
|
||||||
|
sharedscripts
|
||||||
|
compress
|
||||||
|
rotate 7
|
||||||
|
postrotate
|
||||||
|
/etc/init.d/storage-server restart
|
||||||
|
endscript
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
command="/usr/bin/storage-server"
|
command="/usr/bin/storage-server"
|
||||||
command_args="run"
|
command_args="run"
|
||||||
supervisor=supervise-daemon
|
supervisor=supervise-daemon
|
||||||
output_log="/var/log/storage-server.log"
|
output_log="/var/log/storage-server/storage-server.log"
|
||||||
error_log="$output_log"
|
error_log="$output_log"
|
||||||
|
|
||||||
depend() {
|
depend() {
|
||||||
|
|
|
@ -46,7 +46,11 @@ func NewPromiseProxyFrom(rt *goja.Runtime) *PromiseProxy {
|
||||||
return NewPromiseProxy(promise, resolve, reject)
|
return NewPromiseProxy(promise, resolve, reject)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsPromise(v goja.Value) (*goja.Promise, bool) {
|
func isPromise(v any) (*goja.Promise, bool) {
|
||||||
promise, ok := v.Export().(*goja.Promise)
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
promise, ok := v.(*goja.Promise)
|
||||||
return promise, ok
|
return promise, ok
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/dop251/goja_nodejs/eventloop"
|
"github.com/dop251/goja_nodejs/eventloop"
|
||||||
|
@ -22,23 +23,7 @@ type Server struct {
|
||||||
modules []ServerModule
|
modules []ServerModule
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Load(name string, src string) error {
|
func (s *Server) ExecFuncByName(ctx context.Context, funcName string, args ...any) (any, error) {
|
||||||
var err error
|
|
||||||
|
|
||||||
s.loop.RunOnLoop(func(rt *goja.Runtime) {
|
|
||||||
_, err = rt.RunScript(name, src)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.Wrap(err, "could not run js script")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ExecFuncByName(ctx context.Context, funcName string, args ...interface{}) (goja.Value, error) {
|
|
||||||
ctx = logger.With(ctx, logger.F("function", funcName), logger.F("args", args))
|
ctx = logger.With(ctx, logger.F("function", funcName), logger.F("args", args))
|
||||||
|
|
||||||
ret, err := s.Exec(ctx, funcName, args...)
|
ret, err := s.Exec(ctx, funcName, args...)
|
||||||
|
@ -49,16 +34,23 @@ func (s *Server) ExecFuncByName(ctx context.Context, funcName string, args ...in
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Exec(ctx context.Context, callableOrFuncname any, args ...interface{}) (goja.Value, error) {
|
func (s *Server) Exec(ctx context.Context, callableOrFuncname any, args ...any) (any, error) {
|
||||||
var (
|
type result struct {
|
||||||
wg sync.WaitGroup
|
value any
|
||||||
value goja.Value
|
|
||||||
err error
|
err error
|
||||||
)
|
}
|
||||||
|
|
||||||
wg.Add(1)
|
done := make(chan result)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// Drain done channel
|
||||||
|
for range done {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
s.loop.RunOnLoop(func(rt *goja.Runtime) {
|
s.loop.RunOnLoop(func(rt *goja.Runtime) {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
var callable goja.Callable
|
var callable goja.Callable
|
||||||
switch typ := callableOrFuncname.(type) {
|
switch typ := callableOrFuncname.(type) {
|
||||||
case goja.Callable:
|
case goja.Callable:
|
||||||
|
@ -67,7 +59,9 @@ func (s *Server) Exec(ctx context.Context, callableOrFuncname any, args ...inter
|
||||||
case string:
|
case string:
|
||||||
call, ok := goja.AssertFunction(rt.Get(typ))
|
call, ok := goja.AssertFunction(rt.Get(typ))
|
||||||
if !ok {
|
if !ok {
|
||||||
err = errors.WithStack(ErrFuncDoesNotExist)
|
done <- result{
|
||||||
|
err: errors.WithStack(ErrFuncDoesNotExist),
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -75,28 +69,27 @@ func (s *Server) Exec(ctx context.Context, callableOrFuncname any, args ...inter
|
||||||
callable = call
|
callable = call
|
||||||
|
|
||||||
default:
|
default:
|
||||||
err = errors.Errorf("callableOrFuncname: expected callable or function name, got '%T'", callableOrFuncname)
|
done <- result{
|
||||||
|
err: errors.Errorf("callableOrFuncname: expected callable or function name, got '%T'", callableOrFuncname),
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug(ctx, "executing callable")
|
|
||||||
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
recovered := recover()
|
||||||
revoveredErr, ok := recovered.(error)
|
if recovered == nil {
|
||||||
if ok {
|
return
|
||||||
logger.Error(ctx, "recovered runtime error", logger.CapturedE(errors.WithStack(revoveredErr)))
|
}
|
||||||
|
|
||||||
err = errors.WithStack(ErrUnknownError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
recoveredErr, ok := recovered.(error)
|
||||||
|
if !ok {
|
||||||
panic(recovered)
|
panic(recovered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
done <- result{
|
||||||
|
err: recoveredErr,
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
jsArgs := make([]goja.Value, 0, len(args))
|
jsArgs := make([]goja.Value, 0, len(args))
|
||||||
|
@ -104,25 +97,50 @@ func (s *Server) Exec(ctx context.Context, callableOrFuncname any, args ...inter
|
||||||
jsArgs = append(jsArgs, rt.ToValue(a))
|
jsArgs = append(jsArgs, rt.ToValue(a))
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err = callable(nil, jsArgs...)
|
logger.Debug(ctx, "executing callable", logger.F("callable", callableOrFuncname))
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
value, err := callable(nil, jsArgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithStack(err)
|
done <- result{
|
||||||
|
err: errors.WithStack(err),
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
done <- result{
|
||||||
|
value: value.Export(),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(ctx, "executed callable", logger.F("callable", callableOrFuncname), logger.F("duration", time.Since(start).String()))
|
||||||
})
|
})
|
||||||
|
|
||||||
wg.Wait()
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
return nil, nil
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
|
case result := <-done:
|
||||||
|
if result.err != nil {
|
||||||
|
return nil, errors.WithStack(result.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if promise, ok := isPromise(result.value); ok {
|
||||||
|
return s.waitForPromise(promise), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return value, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) WaitForPromise(promise *goja.Promise) goja.Value {
|
func (s *Server) waitForPromise(promise *goja.Promise) any {
|
||||||
var (
|
var (
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
value goja.Value
|
value any
|
||||||
)
|
)
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
@ -142,7 +160,7 @@ func (s *Server) WaitForPromise(promise *goja.Promise) goja.Value {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
value = promise.Result()
|
value = promise.Result().Export()
|
||||||
|
|
||||||
breakLoop = true
|
breakLoop = true
|
||||||
})
|
})
|
||||||
|
@ -162,20 +180,40 @@ func (s *Server) WaitForPromise(promise *goja.Promise) goja.Value {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Start(ctx context.Context) error {
|
func (s *Server) Start(ctx context.Context, name string, src string) error {
|
||||||
s.loop.Start()
|
s.loop.Start()
|
||||||
|
|
||||||
var err error
|
done := make(chan error)
|
||||||
|
|
||||||
s.loop.RunOnLoop(func(rt *goja.Runtime) {
|
s.loop.RunOnLoop(func(rt *goja.Runtime) {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
rt.SetFieldNameMapper(goja.TagFieldNameMapper("goja", true))
|
rt.SetFieldNameMapper(goja.TagFieldNameMapper("goja", true))
|
||||||
rt.SetRandSource(createRandomSource())
|
rt.SetRandSource(createRandomSource())
|
||||||
|
|
||||||
if err = s.initModules(ctx, rt); err != nil {
|
if err := s.loadModules(ctx, rt); err != nil {
|
||||||
err = errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
|
done <- err
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := rt.RunScript(name, src); err != nil {
|
||||||
|
done <- errors.Wrap(err, "could not run js script")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.initModules(ctx, rt); err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
done <- err
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
done <- nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
|
if err := <-done; err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +224,7 @@ func (s *Server) Stop() {
|
||||||
s.loop.Stop()
|
s.loop.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) initModules(ctx context.Context, rt *goja.Runtime) error {
|
func (s *Server) loadModules(ctx context.Context, rt *goja.Runtime) error {
|
||||||
modules := make([]ServerModule, 0, len(s.factories))
|
modules := make([]ServerModule, 0, len(s.factories))
|
||||||
|
|
||||||
for _, moduleFactory := range s.factories {
|
for _, moduleFactory := range s.factories {
|
||||||
|
@ -200,7 +238,13 @@ func (s *Server) initModules(ctx context.Context, rt *goja.Runtime) error {
|
||||||
modules = append(modules, mod)
|
modules = append(modules, mod)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mod := range modules {
|
s.modules = modules
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) initModules(ctx context.Context, rt *goja.Runtime) error {
|
||||||
|
for _, mod := range s.modules {
|
||||||
initMod, ok := mod.(InitializableModule)
|
initMod, ok := mod.(InitializableModule)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
|
@ -213,8 +257,6 @@ func (s *Server) initModules(ctx context.Context, rt *goja.Runtime) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.modules = modules
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ func TestBundle(t *testing.T) {
|
||||||
bundles := []Bundle{
|
bundles := []Bundle{
|
||||||
NewDirectoryBundle("testdata/bundle"),
|
NewDirectoryBundle("testdata/bundle"),
|
||||||
NewTarBundle("testdata/bundle.tar.gz"),
|
NewTarBundle("testdata/bundle.tar.gz"),
|
||||||
NewZipBundle("testdata/bundle.zip"),
|
Must(NewZipBundleFromPath("testdata/bundle.zip")),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range bundles {
|
for _, b := range bundles {
|
||||||
|
|
|
@ -54,7 +54,7 @@ func matchArchivePattern(archivePath string) (Bundle, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches {
|
if matches {
|
||||||
return NewZipBundle(archivePath), nil
|
return NewZipBundleFromPath(archivePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
matches, err = filepath.Match(fmt.Sprintf("*.%s", ExtZim), base)
|
matches, err = filepath.Match(fmt.Sprintf("*.%s", ExtZim), base)
|
||||||
|
|
|
@ -13,15 +13,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ZipBundle struct {
|
type ZipBundle struct {
|
||||||
archivePath string
|
reader *zip.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
|
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(
|
ctx := logger.With(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
logger.F("filename", filename),
|
logger.F("filename", filename),
|
||||||
|
@ -29,7 +24,7 @@ func (b *ZipBundle) File(filename string) (io.ReadCloser, os.FileInfo, error) {
|
||||||
|
|
||||||
logger.Debug(ctx, "opening file")
|
logger.Debug(ctx, "opening file")
|
||||||
|
|
||||||
f, err := reader.Open(filename)
|
f, err := b.reader.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.WithStack(err)
|
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) {
|
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)
|
files := make([]os.FileInfo, 0)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for _, f := range reader.File {
|
for _, f := range b.reader.File {
|
||||||
if !strings.HasPrefix(f.Name, dirname) {
|
if !strings.HasPrefix(f.Name, dirname) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -82,17 +66,35 @@ func (b *ZipBundle) Dir(dirname string) ([]os.FileInfo, error) {
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ZipBundle) openArchive() (*zip.ReadCloser, error) {
|
func NewZipBundleFromReader(reader io.ReaderAt, size int64) (*ZipBundle, error) {
|
||||||
zr, err := zip.OpenReader(b.archivePath)
|
zipReader, err := zip.NewReader(reader, size)
|
||||||
if err != nil {
|
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{
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,11 @@ package bus
|
||||||
import "context"
|
import "context"
|
||||||
|
|
||||||
type Bus interface {
|
type Bus interface {
|
||||||
Subscribe(ctx context.Context, ns MessageNamespace) (<-chan Message, error)
|
Subscribe(ctx context.Context, addr Address) (<-chan Envelope, error)
|
||||||
Unsubscribe(ctx context.Context, ns MessageNamespace, ch <-chan Message)
|
Unsubscribe(addr Address, ch <-chan Envelope)
|
||||||
Publish(ctx context.Context, msg Message) error
|
Publish(env Envelope) error
|
||||||
Request(ctx context.Context, msg Message) (Message, error)
|
Request(ctx context.Context, env Envelope) (Envelope, error)
|
||||||
Reply(ctx context.Context, ns MessageNamespace, h RequestHandler) error
|
Reply(ctx context.Context, addr Address, h RequestHandler) chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestHandler func(msg Message) (Message, error)
|
type RequestHandler func(env Envelope) (any, error)
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package bus
|
||||||
|
|
||||||
|
type Address string
|
||||||
|
|
||||||
|
type Envelope interface {
|
||||||
|
Message() any
|
||||||
|
Address() Address
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseEnvelope struct {
|
||||||
|
msg any
|
||||||
|
addr Address
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address implements Envelope.
|
||||||
|
func (e *BaseEnvelope) Address() Address {
|
||||||
|
return e.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message implements Envelope.
|
||||||
|
func (e *BaseEnvelope) Message() any {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEnvelope(addr Address, msg any) *BaseEnvelope {
|
||||||
|
return &BaseEnvelope{
|
||||||
|
addr: addr,
|
||||||
|
msg: msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Envelope = &BaseEnvelope{}
|
|
@ -15,13 +15,13 @@ type Bus struct {
|
||||||
nextRequestID uint64
|
nextRequestID uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) Subscribe(ctx context.Context, ns bus.MessageNamespace) (<-chan bus.Message, error) {
|
func (b *Bus) Subscribe(ctx context.Context, address bus.Address) (<-chan bus.Envelope, error) {
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
ctx, "subscribing to messages",
|
ctx, "subscribing",
|
||||||
logger.F("messageNamespace", ns),
|
logger.F("address", address),
|
||||||
)
|
)
|
||||||
|
|
||||||
dispatchers := b.getDispatchers(ns)
|
dispatchers := b.getDispatchers(address)
|
||||||
disp := newEventDispatcher(b.opt.BufferSize)
|
disp := newEventDispatcher(b.opt.BufferSize)
|
||||||
|
|
||||||
go disp.Run(ctx)
|
go disp.Run(ctx)
|
||||||
|
@ -31,50 +31,41 @@ func (b *Bus) Subscribe(ctx context.Context, ns bus.MessageNamespace) (<-chan bu
|
||||||
return disp.Out(), nil
|
return disp.Out(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) Unsubscribe(ctx context.Context, ns bus.MessageNamespace, ch <-chan bus.Message) {
|
func (b *Bus) Unsubscribe(address bus.Address, ch <-chan bus.Envelope) {
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
ctx, "unsubscribing from messages",
|
context.Background(), "unsubscribing",
|
||||||
logger.F("messageNamespace", ns),
|
logger.F("address", address),
|
||||||
)
|
)
|
||||||
|
|
||||||
dispatchers := b.getDispatchers(ns)
|
dispatchers := b.getDispatchers(address)
|
||||||
dispatchers.RemoveByOutChannel(ch)
|
dispatchers.RemoveByOutChannel(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) Publish(ctx context.Context, msg bus.Message) error {
|
func (b *Bus) Publish(env bus.Envelope) error {
|
||||||
dispatchers := b.getDispatchers(msg.MessageNamespace())
|
dispatchers := b.getDispatchers(env.Address())
|
||||||
dispatchersList := dispatchers.List()
|
|
||||||
|
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
ctx, "publishing message",
|
context.Background(), "publish",
|
||||||
logger.F("dispatchers", len(dispatchersList)),
|
logger.F("address", env.Address()),
|
||||||
logger.F("messageNamespace", msg.MessageNamespace()),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, d := range dispatchersList {
|
dispatchers.Range(func(d *eventDispatcher) {
|
||||||
if d.Closed() {
|
if err := d.In(env); err != nil {
|
||||||
dispatchers.Remove(d)
|
logger.Error(context.Background(), "could not publish message", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if err := d.In(msg); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) getDispatchers(namespace bus.MessageNamespace) *eventDispatcherSet {
|
func (b *Bus) getDispatchers(address bus.Address) *eventDispatcherSet {
|
||||||
strNamespace := string(namespace)
|
rawAddress := string(address)
|
||||||
|
|
||||||
rawDispatchers, exists := b.dispatchers.Get(strNamespace)
|
rawDispatchers, exists := b.dispatchers.Get(rawAddress)
|
||||||
dispatchers, ok := rawDispatchers.(*eventDispatcherSet)
|
dispatchers, ok := rawDispatchers.(*eventDispatcherSet)
|
||||||
|
|
||||||
if !exists || !ok {
|
if !exists || !ok {
|
||||||
dispatchers = newEventDispatcherSet()
|
dispatchers = newEventDispatcherSet()
|
||||||
b.dispatchers.Set(strNamespace, dispatchers)
|
b.dispatchers.Set(rawAddress, dispatchers)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatchers
|
return dispatchers
|
||||||
|
|
|
@ -4,13 +4,23 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
busTesting "forge.cadoles.com/arcad/edge/pkg/bus/testing"
|
busTesting "forge.cadoles.com/arcad/edge/pkg/bus/testing"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
goleak.VerifyTestMain(m)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMemoryBus(t *testing.T) {
|
func TestMemoryBus(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("Test disabled when -short flag is set")
|
t.Skip("Test disabled when -short flag is set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("PublishSubscribe", func(t *testing.T) {
|
t.Run("PublishSubscribe", func(t *testing.T) {
|
||||||
|
@ -26,4 +36,11 @@ func TestMemoryBus(t *testing.T) {
|
||||||
b := NewBus()
|
b := NewBus()
|
||||||
busTesting.TestRequestReply(t, b)
|
busTesting.TestRequestReply(t, b)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("CanceledRequestReply", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := NewBus()
|
||||||
|
busTesting.TestCanceledRequest(t, b)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package memory
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -30,7 +29,7 @@ func (s *eventDispatcherSet) Remove(d *eventDispatcher) {
|
||||||
delete(s.items, d)
|
delete(s.items, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *eventDispatcherSet) RemoveByOutChannel(out <-chan bus.Message) {
|
func (s *eventDispatcherSet) RemoveByOutChannel(out <-chan bus.Envelope) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
@ -42,17 +41,18 @@ func (s *eventDispatcherSet) RemoveByOutChannel(out <-chan bus.Message) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *eventDispatcherSet) List() []*eventDispatcher {
|
func (s *eventDispatcherSet) Range(fn func(d *eventDispatcher)) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
dispatchers := make([]*eventDispatcher, 0, len(s.items))
|
|
||||||
|
|
||||||
for d := range s.items {
|
for d := range s.items {
|
||||||
dispatchers = append(dispatchers, d)
|
if d.Closed() {
|
||||||
}
|
s.Remove(d)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
return dispatchers
|
fn(d)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEventDispatcherSet() *eventDispatcherSet {
|
func newEventDispatcherSet() *eventDispatcherSet {
|
||||||
|
@ -62,8 +62,8 @@ func newEventDispatcherSet() *eventDispatcherSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
type eventDispatcher struct {
|
type eventDispatcher struct {
|
||||||
in chan bus.Message
|
in chan bus.Envelope
|
||||||
out chan bus.Message
|
out chan bus.Envelope
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
@ -83,11 +83,15 @@ func (d *eventDispatcher) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *eventDispatcher) close() {
|
func (d *eventDispatcher) close() {
|
||||||
d.closed = true
|
if d.closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
close(d.in)
|
close(d.in)
|
||||||
|
d.closed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *eventDispatcher) In(msg bus.Message) (err error) {
|
func (d *eventDispatcher) In(msg bus.Envelope) (err error) {
|
||||||
d.mutex.RLock()
|
d.mutex.RLock()
|
||||||
defer d.mutex.RUnlock()
|
defer d.mutex.RUnlock()
|
||||||
|
|
||||||
|
@ -100,67 +104,52 @@ func (d *eventDispatcher) In(msg bus.Message) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *eventDispatcher) Out() <-chan bus.Message {
|
func (d *eventDispatcher) Out() <-chan bus.Envelope {
|
||||||
return d.out
|
return d.out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *eventDispatcher) IsOut(out <-chan bus.Message) bool {
|
func (d *eventDispatcher) IsOut(out <-chan bus.Envelope) bool {
|
||||||
return d.out == out
|
return d.out == out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *eventDispatcher) Run(ctx context.Context) {
|
func (d *eventDispatcher) Run(ctx context.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
for {
|
logger.Debug(ctx, "closing dispatcher, flushing out incoming messages")
|
||||||
logger.Debug(ctx, "closing dispatcher, flushing out incoming messages")
|
|
||||||
|
|
||||||
close(d.out)
|
close(d.out)
|
||||||
|
|
||||||
|
for range d.in {
|
||||||
// Flush all incoming messages
|
// Flush all incoming messages
|
||||||
for {
|
|
||||||
_, ok := <-d.in
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
msg, ok := <-d.in
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout := time.After(time.Second)
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case d.out <- msg:
|
|
||||||
case <-timeout:
|
|
||||||
logger.Error(
|
|
||||||
ctx,
|
|
||||||
"out message channel timeout",
|
|
||||||
logger.F("message", msg),
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
logger.Error(
|
if err := ctx.Err(); !errors.Is(err, context.Canceled) {
|
||||||
ctx,
|
logger.Error(
|
||||||
"message subscription context canceled",
|
ctx,
|
||||||
logger.F("message", msg),
|
"message subscription context canceled",
|
||||||
logger.CapturedE(errors.WithStack(ctx.Err())),
|
logger.CapturedE(errors.WithStack(err)),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case msg, ok := <-d.in:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.out <- msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEventDispatcher(bufferSize int64) *eventDispatcher {
|
func newEventDispatcher(bufferSize int64) *eventDispatcher {
|
||||||
return &eventDispatcher{
|
return &eventDispatcher{
|
||||||
in: make(chan bus.Message, bufferSize),
|
in: make(chan bus.Envelope, bufferSize),
|
||||||
out: make(chan bus.Message, bufferSize),
|
out: make(chan bus.Envelope, bufferSize),
|
||||||
closed: false,
|
closed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,57 +11,78 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MessageNamespaceRequest bus.MessageNamespace = "reqrep/request"
|
AddressRequest bus.Address = "bus/memory/request"
|
||||||
MessageNamespaceReply bus.MessageNamespace = "reqrep/reply"
|
AddressReply bus.Address = "bus/memory/reply"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestMessage struct {
|
type RequestEnvelope struct {
|
||||||
RequestID uint64
|
requestID uint64
|
||||||
|
wrapped bus.Envelope
|
||||||
Message bus.Message
|
|
||||||
|
|
||||||
ns bus.MessageNamespace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *RequestMessage) MessageNamespace() bus.MessageNamespace {
|
func (e *RequestEnvelope) Address() bus.Address {
|
||||||
return m.ns
|
return getRequestAddress(e.wrapped.Address())
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReplyMessage struct {
|
func (e *RequestEnvelope) Message() any {
|
||||||
RequestID uint64
|
return e.wrapped.Message()
|
||||||
Message bus.Message
|
|
||||||
Error error
|
|
||||||
|
|
||||||
ns bus.MessageNamespace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ReplyMessage) MessageNamespace() bus.MessageNamespace {
|
func (e *RequestEnvelope) RequestID() uint64 {
|
||||||
return m.ns
|
return e.requestID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bus) Request(ctx context.Context, msg bus.Message) (bus.Message, error) {
|
func (e *RequestEnvelope) Unwrap() bus.Envelope {
|
||||||
|
return e.wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplyEnvelope struct {
|
||||||
|
requestID uint64
|
||||||
|
wrapped bus.Envelope
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ReplyEnvelope) Address() bus.Address {
|
||||||
|
return getReplyAddress(e.wrapped.Address(), e.requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ReplyEnvelope) Message() any {
|
||||||
|
return e.wrapped.Message()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ReplyEnvelope) Err() error {
|
||||||
|
return e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ReplyEnvelope) Unwrap() bus.Envelope {
|
||||||
|
return e.wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bus) Request(ctx context.Context, env bus.Envelope) (bus.Envelope, error) {
|
||||||
requestID := atomic.AddUint64(&b.nextRequestID, 1)
|
requestID := atomic.AddUint64(&b.nextRequestID, 1)
|
||||||
|
|
||||||
req := &RequestMessage{
|
req := &RequestEnvelope{
|
||||||
RequestID: requestID,
|
requestID: requestID,
|
||||||
Message: msg,
|
wrapped: env,
|
||||||
ns: msg.MessageNamespace(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
replyNamespace := createReplyNamespace(requestID)
|
replyAddress := getReplyAddress(env.Address(), requestID)
|
||||||
|
|
||||||
replies, err := b.Subscribe(ctx, replyNamespace)
|
subCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
replies, err := b.Subscribe(subCtx, replyAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
b.Unsubscribe(ctx, replyNamespace, replies)
|
b.Unsubscribe(replyAddress, replies)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
logger.Debug(ctx, "publishing request", logger.F("request", req))
|
logger.Debug(ctx, "publishing request", logger.F("request", req))
|
||||||
|
|
||||||
if err := b.Publish(ctx, req); err != nil {
|
if err := b.Publish(req); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,82 +91,93 @@ func (b *Bus) Request(ctx context.Context, msg bus.Message) (bus.Message, error)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, errors.WithStack(ctx.Err())
|
return nil, errors.WithStack(ctx.Err())
|
||||||
|
|
||||||
case msg, ok := <-replies:
|
case env, ok := <-replies:
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.WithStack(bus.ErrNoResponse)
|
return nil, errors.WithStack(bus.ErrNoResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
reply, ok := msg.(*ReplyMessage)
|
reply, ok := env.(*ReplyEnvelope)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.WithStack(bus.ErrUnexpectedMessage)
|
return nil, errors.WithStack(bus.ErrUnexpectedMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
if reply.Error != nil {
|
if err := reply.Err(); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply.Message, nil
|
return reply.Unwrap(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestHandler func(evt bus.Message) (bus.Message, error)
|
func (b *Bus) Reply(ctx context.Context, address bus.Address, handler bus.RequestHandler) chan error {
|
||||||
|
requestAddress := getRequestAddress(address)
|
||||||
|
|
||||||
func (b *Bus) Reply(ctx context.Context, msgNamespace bus.MessageNamespace, h bus.RequestHandler) error {
|
errs := make(chan error)
|
||||||
requests, err := b.Subscribe(ctx, msgNamespace)
|
|
||||||
|
requests, err := b.Subscribe(ctx, requestAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
go func() {
|
||||||
|
errs <- errors.WithStack(err)
|
||||||
|
close(errs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
go func() {
|
||||||
b.Unsubscribe(ctx, msgNamespace, requests)
|
defer func() {
|
||||||
|
b.Unsubscribe(requestAddress, requests)
|
||||||
|
close(errs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
errs <- errors.WithStack(ctx.Err())
|
||||||
|
return
|
||||||
|
|
||||||
|
case env, ok := <-requests:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
request, ok := env.(*RequestEnvelope)
|
||||||
|
if !ok {
|
||||||
|
errs <- errors.WithStack(bus.ErrUnexpectedMessage)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(ctx, "handling request", logger.F("request", request))
|
||||||
|
|
||||||
|
msg, err := handler(request.Unwrap())
|
||||||
|
|
||||||
|
reply := &ReplyEnvelope{
|
||||||
|
requestID: request.RequestID(),
|
||||||
|
wrapped: bus.NewEnvelope(request.Unwrap().Address(), msg),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
reply.err = errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(ctx, "publishing reply", logger.F("reply", reply))
|
||||||
|
|
||||||
|
if err := b.Publish(reply); err != nil {
|
||||||
|
errs <- errors.WithStack(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
return errs
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return errors.WithStack(ctx.Err())
|
|
||||||
|
|
||||||
case msg, ok := <-requests:
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
request, ok := msg.(*RequestMessage)
|
|
||||||
if !ok {
|
|
||||||
return errors.WithStack(bus.ErrUnexpectedMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(ctx, "handling request", logger.F("request", request))
|
|
||||||
|
|
||||||
msg, err := h(request.Message)
|
|
||||||
|
|
||||||
reply := &ReplyMessage{
|
|
||||||
RequestID: request.RequestID,
|
|
||||||
Message: nil,
|
|
||||||
Error: nil,
|
|
||||||
|
|
||||||
ns: createReplyNamespace(request.RequestID),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
reply.Error = errors.WithStack(err)
|
|
||||||
} else {
|
|
||||||
reply.Message = msg
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(ctx, "publishing reply", logger.F("reply", reply))
|
|
||||||
|
|
||||||
if err := b.Publish(ctx, reply); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createReplyNamespace(requestID uint64) bus.MessageNamespace {
|
func getRequestAddress(addr bus.Address) bus.Address {
|
||||||
return bus.NewMessageNamespace(
|
return AddressRequest + "/" + addr
|
||||||
MessageNamespaceReply,
|
}
|
||||||
bus.MessageNamespace(strconv.FormatUint(requestID, 10)),
|
|
||||||
)
|
func getReplyAddress(addr bus.Address, requestID uint64) bus.Address {
|
||||||
|
return AddressReply + "/" + addr + "/" + bus.Address(strconv.FormatUint(requestID, 10))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
package bus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
MessageNamespace string
|
|
||||||
)
|
|
||||||
|
|
||||||
type Message interface {
|
|
||||||
MessageNamespace() MessageNamespace
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageNamespace(namespaces ...MessageNamespace) MessageNamespace {
|
|
||||||
var sb strings.Builder
|
|
||||||
|
|
||||||
for i, ns := range namespaces {
|
|
||||||
if i != 0 {
|
|
||||||
if _, err := sb.WriteString(":"); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not build new message namespace"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := sb.WriteString(string(ns)); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not build new message namespace"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageNamespace(sb.String())
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ package testing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -12,74 +13,52 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testNamespace bus.MessageNamespace = "testNamespace"
|
testAddress bus.Address = "testAddress"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testMessage struct{}
|
|
||||||
|
|
||||||
func (e *testMessage) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return testNamespace
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPublishSubscribe(t *testing.T, b bus.Bus) {
|
func TestPublishSubscribe(t *testing.T, b bus.Bus) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
t.Log("subscribe")
|
t.Log("subscribe")
|
||||||
|
|
||||||
messages, err := b.Subscribe(ctx, testNamespace)
|
envelopes, err := b.Subscribe(ctx, testAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(errors.WithStack(err))
|
t.Fatal(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedTotal := 5
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
wg.Add(5)
|
wg.Add(expectedTotal)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
// 5 events should be received
|
|
||||||
t.Log("publish 0")
|
|
||||||
|
|
||||||
if err := b.Publish(ctx, &testMessage{}); err != nil {
|
count := expectedTotal
|
||||||
t.Error(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("publish 1")
|
for i := 0; i < count; i++ {
|
||||||
|
env := bus.NewEnvelope(testAddress, fmt.Sprintf("message %d", i))
|
||||||
|
|
||||||
if err := b.Publish(ctx, &testMessage{}); err != nil {
|
if err := b.Publish(env); err != nil {
|
||||||
t.Error(errors.WithStack(err))
|
t.Error(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log("publish 2")
|
t.Logf("published %d", i)
|
||||||
|
|
||||||
if err := b.Publish(ctx, &testMessage{}); err != nil {
|
|
||||||
t.Error(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("publish 3")
|
|
||||||
|
|
||||||
if err := b.Publish(ctx, &testMessage{}); err != nil {
|
|
||||||
t.Error(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("publish 4")
|
|
||||||
|
|
||||||
if err := b.Publish(ctx, &testMessage{}); err != nil {
|
|
||||||
t.Error(errors.WithStack(err))
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var count int32 = 0
|
var count int32 = 0
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
t.Log("range for events")
|
t.Log("range for received envelopes")
|
||||||
|
|
||||||
for msg := range messages {
|
for env := range envelopes {
|
||||||
t.Logf("received msg %d", atomic.LoadInt32(&count))
|
t.Logf("received msg %d", atomic.LoadInt32(&count))
|
||||||
atomic.AddInt32(&count, 1)
|
atomic.AddInt32(&count, 1)
|
||||||
|
|
||||||
if e, g := testNamespace, msg.MessageNamespace(); e != g {
|
if e, g := testAddress, env.Address(); e != g {
|
||||||
t.Errorf("evt.MessageNamespace(): expected '%v', got '%v'", e, g)
|
t.Errorf("env.Address(): expected '%v', got '%v'", e, g)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Done()
|
wg.Done()
|
||||||
|
@ -88,9 +67,9 @@ func TestPublishSubscribe(t *testing.T, b bus.Bus) {
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
b.Unsubscribe(ctx, testNamespace, messages)
|
b.Unsubscribe(testAddress, envelopes)
|
||||||
|
|
||||||
if e, g := int32(5), count; e != g {
|
if e, g := int32(expectedTotal), count; e != g {
|
||||||
t.Errorf("message received count: expected '%v', got '%v'", e, g)
|
t.Errorf("envelopes received count: expected '%v', got '%v'", e, g)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,58 +11,42 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testTypeReqRes bus.MessageNamespace = "testNamspaceReqRes"
|
testTypeReqResAddress bus.Address = "testTypeReqResAddress"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testReqResMessage struct {
|
|
||||||
i int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *testReqResMessage) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return testNamespace
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRequestReply(t *testing.T, b bus.Bus) {
|
func TestRequestReply(t *testing.T, b bus.Bus) {
|
||||||
expectedRoundTrips := 256
|
expectedRoundTrips := 256
|
||||||
timeout := time.Now().Add(time.Duration(expectedRoundTrips) * time.Second)
|
timeout := time.Now().Add(time.Duration(expectedRoundTrips) * time.Second)
|
||||||
|
|
||||||
var (
|
replyCtx, cancelReply := context.WithDeadline(context.Background(), timeout)
|
||||||
initWaitGroup sync.WaitGroup
|
defer cancelReply()
|
||||||
resWaitGroup sync.WaitGroup
|
|
||||||
)
|
|
||||||
|
|
||||||
initWaitGroup.Add(1)
|
var resWaitGroup sync.WaitGroup
|
||||||
|
|
||||||
|
replyErrs := b.Reply(replyCtx, testTypeReqResAddress, func(env bus.Envelope) (any, error) {
|
||||||
|
defer resWaitGroup.Done()
|
||||||
|
|
||||||
|
req, ok := env.Message().(int)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.WithStack(bus.ErrUnexpectedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate random work
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
|
||||||
|
t.Logf("[RES] sending res #%d", req)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
repondCtx, cancelRespond := context.WithDeadline(context.Background(), timeout)
|
for err := range replyErrs {
|
||||||
defer cancelRespond()
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
initWaitGroup.Done()
|
|
||||||
|
|
||||||
err := b.Reply(repondCtx, testNamespace, func(msg bus.Message) (bus.Message, error) {
|
|
||||||
defer resWaitGroup.Done()
|
|
||||||
|
|
||||||
req, ok := msg.(*testReqResMessage)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.WithStack(bus.ErrUnexpectedMessage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result := &testReqResMessage{req.i}
|
|
||||||
|
|
||||||
// Simulate random work
|
|
||||||
time.Sleep(time.Millisecond * 100)
|
|
||||||
|
|
||||||
t.Logf("[RES] sending res #%d", req.i)
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
initWaitGroup.Wait()
|
|
||||||
|
|
||||||
var reqWaitGroup sync.WaitGroup
|
var reqWaitGroup sync.WaitGroup
|
||||||
|
|
||||||
for i := 0; i < expectedRoundTrips; i++ {
|
for i := 0; i < expectedRoundTrips; i++ {
|
||||||
|
@ -75,32 +59,30 @@ func TestRequestReply(t *testing.T, b bus.Bus) {
|
||||||
requestCtx, cancelRequest := context.WithDeadline(context.Background(), timeout)
|
requestCtx, cancelRequest := context.WithDeadline(context.Background(), timeout)
|
||||||
defer cancelRequest()
|
defer cancelRequest()
|
||||||
|
|
||||||
req := &testReqResMessage{i}
|
|
||||||
|
|
||||||
t.Logf("[REQ] sending req #%d", i)
|
t.Logf("[REQ] sending req #%d", i)
|
||||||
|
|
||||||
result, err := b.Request(requestCtx, req)
|
response, err := b.Request(requestCtx, bus.NewEnvelope(testTypeReqResAddress, i))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("[REQ] received req #%d reply", i)
|
t.Logf("[REQ] received req #%d reply", i)
|
||||||
|
|
||||||
if result == nil {
|
if response == nil {
|
||||||
t.Error("result should not be nil")
|
t.Error("response should not be nil")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, ok := result.(*testReqResMessage)
|
result, ok := response.Message().(int)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Error(errors.WithStack(bus.ErrUnexpectedMessage))
|
t.Error(errors.WithStack(bus.ErrUnexpectedMessage))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if e, g := req.i, res.i; e != g {
|
if e, g := i, result; e != g {
|
||||||
t.Errorf("res.i: expected '%v', got '%v'", e, g)
|
t.Errorf("response.Message(): expected '%v', got '%v'", e, g)
|
||||||
}
|
}
|
||||||
}(i)
|
}(i)
|
||||||
}
|
}
|
||||||
|
@ -108,3 +90,77 @@ func TestRequestReply(t *testing.T, b bus.Bus) {
|
||||||
reqWaitGroup.Wait()
|
reqWaitGroup.Wait()
|
||||||
resWaitGroup.Wait()
|
resWaitGroup.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanceledRequest(t *testing.T, b bus.Bus) {
|
||||||
|
replyCtx, cancelReply := context.WithCancel(context.Background())
|
||||||
|
defer cancelReply()
|
||||||
|
|
||||||
|
errs := b.Reply(replyCtx, testTypeReqResAddress, func(env bus.Envelope) (any, error) {
|
||||||
|
return env.Message(), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for err := range errs {
|
||||||
|
if !errors.Is(err, context.Canceled) {
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
count := 100
|
||||||
|
|
||||||
|
wg.Add(count)
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
t.Logf("calling %d", i)
|
||||||
|
|
||||||
|
isCanceled := i%2 == 0
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
if isCanceled {
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
ctx = canceledCtx
|
||||||
|
} else {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("publishing envelope #%d", i)
|
||||||
|
|
||||||
|
reply, err := b.Request(ctx, bus.NewEnvelope(testTypeReqResAddress, int64(i)))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) && isCanceled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, bus.ErrNoResponse) && isCanceled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := reply.Message().(int64)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("response.Result: expected type '%T', got '%T'", int64(0), reply.Message())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := i, int(result); e != g {
|
||||||
|
t.Errorf("response.Result: expected '%v', got '%v'", e, g)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
282
pkg/http/blob.go
282
pkg/http/blob.go
|
@ -1,282 +0,0 @@
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/blob"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
errorCodeForbidden = "forbidden"
|
|
||||||
errorCodeInternalError = "internal-error"
|
|
||||||
errorCodeBadRequest = "bad-request"
|
|
||||||
errorCodeNotFound = "not-found"
|
|
||||||
)
|
|
||||||
|
|
||||||
type uploadResponse struct {
|
|
||||||
Bucket string `json:"bucket"`
|
|
||||||
BlobID storage.BlobID `json:"blobId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) handleAppUpload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
h.mutex.RLock()
|
|
||||||
defer h.mutex.RUnlock()
|
|
||||||
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, h.uploadMaxFileSize)
|
|
||||||
|
|
||||||
if err := r.ParseMultipartForm(h.uploadMaxFileSize); err != nil {
|
|
||||||
logger.Error(ctx, "could not parse multipart form", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, fileHeader, err := r.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not read form file", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata map[string]any
|
|
||||||
|
|
||||||
rawMetadata := r.Form.Get("metadata")
|
|
||||||
if rawMetadata != "" {
|
|
||||||
if err := json.Unmarshal([]byte(rawMetadata), &metadata); err != nil {
|
|
||||||
logger.Error(ctx, "could not parse metadata", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
|
||||||
ContextKeyOriginRequest: r,
|
|
||||||
})
|
|
||||||
|
|
||||||
requestMsg := blob.NewMessageUploadRequest(ctx, fileHeader, metadata)
|
|
||||||
|
|
||||||
reply, err := h.bus.Request(ctx, requestMsg)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(ctx, "upload reply", logger.F("reply", reply))
|
|
||||||
|
|
||||||
responseMsg, ok := reply.(*blob.MessageUploadResponse)
|
|
||||||
if !ok {
|
|
||||||
logger.Error(
|
|
||||||
ctx, "unexpected upload response message",
|
|
||||||
logger.F("message", reply),
|
|
||||||
)
|
|
||||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !responseMsg.Allow {
|
|
||||||
jsonError(w, http.StatusForbidden, errorCodeForbidden)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(w)
|
|
||||||
res := &uploadResponse{
|
|
||||||
Bucket: responseMsg.Bucket,
|
|
||||||
BlobID: responseMsg.BlobID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encoder.Encode(res); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not encode upload response"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) handleAppDownload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
h.mutex.RLock()
|
|
||||||
defer h.mutex.RUnlock()
|
|
||||||
|
|
||||||
bucket := chi.URLParam(r, "bucket")
|
|
||||||
blobID := chi.URLParam(r, "blobID")
|
|
||||||
|
|
||||||
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
|
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
|
||||||
ContextKeyOriginRequest: r,
|
|
||||||
})
|
|
||||||
|
|
||||||
requestMsg := blob.NewMessageDownloadRequest(ctx, bucket, storage.BlobID(blobID))
|
|
||||||
|
|
||||||
reply, err := h.bus.Request(ctx, requestMsg)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
replyMsg, ok := reply.(*blob.MessageDownloadResponse)
|
|
||||||
if !ok {
|
|
||||||
logger.Error(
|
|
||||||
ctx, "unexpected download response message",
|
|
||||||
logger.CapturedE(errors.WithStack(bus.ErrUnexpectedMessage)),
|
|
||||||
logger.F("message", reply),
|
|
||||||
)
|
|
||||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !replyMsg.Allow {
|
|
||||||
jsonError(w, http.StatusForbidden, errorCodeForbidden)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if replyMsg.Blob == nil {
|
|
||||||
jsonError(w, http.StatusNotFound, errorCodeNotFound)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := replyMsg.Blob.Close(); err != nil {
|
|
||||||
logger.Error(ctx, "could not close blob", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
http.ServeContent(w, r, string(replyMsg.BlobInfo.ID()), replyMsg.BlobInfo.ModTime(), replyMsg.Blob)
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveFile(w http.ResponseWriter, r *http.Request, fs fs.FS, path string) {
|
|
||||||
ctx := logger.With(r.Context(), logger.F("path", path))
|
|
||||||
|
|
||||||
file, err := fs.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Error(ctx, "error while opening fs file", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := file.Close(); err != nil {
|
|
||||||
logger.Error(ctx, "error while closing fs file", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
info, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "error while retrieving fs file stat", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, ok := file.(io.ReadSeeker)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeContent(w, r, path, info.ModTime(), reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
type jsonErrorResponse struct {
|
|
||||||
Error jsonErr `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type jsonErr struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonError(w http.ResponseWriter, status int, code string) {
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(status)
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(w)
|
|
||||||
response := jsonErrorResponse{
|
|
||||||
Error: jsonErr{
|
|
||||||
Code: code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encoder.Encode(response); err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type uploadedFile struct {
|
|
||||||
multipart.File
|
|
||||||
header *multipart.FileHeader
|
|
||||||
modTime time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stat implements fs.File
|
|
||||||
func (f *uploadedFile) Stat() (fs.FileInfo, error) {
|
|
||||||
return &uploadedFileInfo{
|
|
||||||
header: f.header,
|
|
||||||
modTime: f.modTime,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type uploadedFileInfo struct {
|
|
||||||
header *multipart.FileHeader
|
|
||||||
modTime time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDir implements fs.FileInfo
|
|
||||||
func (i *uploadedFileInfo) IsDir() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ModTime implements fs.FileInfo
|
|
||||||
func (i *uploadedFileInfo) ModTime() time.Time {
|
|
||||||
return i.modTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mode implements fs.FileInfo
|
|
||||||
func (i *uploadedFileInfo) Mode() fs.FileMode {
|
|
||||||
return os.ModePerm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name implements fs.FileInfo
|
|
||||||
func (i *uploadedFileInfo) Name() string {
|
|
||||||
return i.header.Filename
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size implements fs.FileInfo
|
|
||||||
func (i *uploadedFileInfo) Size() int64 {
|
|
||||||
return i.header.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sys implements fs.FileInfo
|
|
||||||
func (i *uploadedFileInfo) Sys() any {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ fs.File = &uploadedFile{}
|
|
||||||
_ fs.FileInfo = &uploadedFileInfo{}
|
|
||||||
)
|
|
|
@ -7,11 +7,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *Handler) handleSDKClient(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleSDKClient(w http.ResponseWriter, r *http.Request) {
|
||||||
serveFile(w, r, &sdk.FS, "client/dist/client.js")
|
ServeFile(w, r, &sdk.FS, "client/dist/client.js")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleSDKClientMap(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleSDKClientMap(w http.ResponseWriter, r *http.Request) {
|
||||||
serveFile(w, r, &sdk.FS, "client/dist/client.js.map")
|
ServeFile(w, r, &sdk.FS, "client/dist/client.js.map")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleAppFiles(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleAppFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
var (
|
||||||
|
contextKeyBus contextKey = "bus"
|
||||||
|
contextKeyHTTPRequest contextKey = "httpRequest"
|
||||||
|
contextKeyHTTPClient contextKey = "httpClient"
|
||||||
|
contextKeySessionID contextKey = "sessionId"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) contextMiddleware(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = WithContextBus(ctx, h.bus)
|
||||||
|
ctx = WithContextHTTPRequest(ctx, r)
|
||||||
|
ctx = WithContextHTTPClient(ctx, h.httpClient)
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContextBus(ctx context.Context) (bus.Bus, bool) {
|
||||||
|
return contextValue[bus.Bus](ctx, contextKeyBus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContextBus(parent context.Context, bus bus.Bus) context.Context {
|
||||||
|
return context.WithValue(parent, contextKeyBus, bus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContextHTTPRequest(ctx context.Context) (*http.Request, bool) {
|
||||||
|
return contextValue[*http.Request](ctx, contextKeyHTTPRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContextHTTPRequest(parent context.Context, request *http.Request) context.Context {
|
||||||
|
return context.WithValue(parent, contextKeyHTTPRequest, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContextHTTPClient(ctx context.Context) (*http.Client, bool) {
|
||||||
|
return contextValue[*http.Client](ctx, contextKeyHTTPClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContextHTTPClient(parent context.Context, client *http.Client) context.Context {
|
||||||
|
return context.WithValue(parent, contextKeyHTTPClient, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContextSessionID(ctx context.Context) (string, bool) {
|
||||||
|
return contextValue[string](ctx, contextKeySessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithContextSessionID(parent context.Context, sessionID string) context.Context {
|
||||||
|
return context.WithValue(parent, contextKeySessionID, sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func contextValue[T any](ctx context.Context, key any) (T, bool) {
|
||||||
|
value, ok := ctx.Value(key).(T)
|
||||||
|
if !ok {
|
||||||
|
return *new(T), false
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, true
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AddressIncomingMessage bus.Address = "http/incoming-message"
|
||||||
|
AddressOutgoingMessage bus.Address = "http/outgoing-message"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IncomingMessage struct {
|
||||||
|
Context context.Context
|
||||||
|
Payload map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIncomingMessageEnvelope(ctx context.Context, payload map[string]any) bus.Envelope {
|
||||||
|
return bus.NewEnvelope(AddressIncomingMessage, &IncomingMessage{ctx, payload})
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutgoingMessage struct {
|
||||||
|
SessionID string
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOutgoingMessageEnvelope(sessionID string, data any) bus.Envelope {
|
||||||
|
return bus.NewEnvelope(AddressOutgoingMessage, &OutgoingMessage{sessionID, data})
|
||||||
|
}
|
|
@ -24,10 +24,9 @@ type Handler struct {
|
||||||
public http.Handler
|
public http.Handler
|
||||||
router chi.Router
|
router chi.Router
|
||||||
|
|
||||||
sockjs http.Handler
|
sockjs http.Handler
|
||||||
bus bus.Bus
|
bus bus.Bus
|
||||||
sockjsOpts sockjs.Options
|
sockjsOpts sockjs.Options
|
||||||
uploadMaxFileSize int64
|
|
||||||
|
|
||||||
server *app.Server
|
server *app.Server
|
||||||
serverModuleFactories []app.ServerModuleFactory
|
serverModuleFactories []app.ServerModuleFactory
|
||||||
|
@ -57,10 +56,6 @@ func (h *Handler) Load(ctx context.Context, bdle bundle.Bundle) error {
|
||||||
|
|
||||||
server := app.NewServer(h.serverModuleFactories...)
|
server := app.NewServer(h.serverModuleFactories...)
|
||||||
|
|
||||||
if err := server.Load(serverMainScript, string(mainScript)); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := bundle.NewFileSystem("public", bdle)
|
fs := bundle.NewFileSystem("public", bdle)
|
||||||
public := HTML5Fileserver(fs)
|
public := HTML5Fileserver(fs)
|
||||||
sockjs := sockjs.NewHandler(sockJSPathPrefix, h.sockjsOpts, h.handleSockJSSession)
|
sockjs := sockjs.NewHandler(sockJSPathPrefix, h.sockjsOpts, h.handleSockJSSession)
|
||||||
|
@ -69,7 +64,7 @@ func (h *Handler) Load(ctx context.Context, bdle bundle.Bundle) error {
|
||||||
h.server.Stop()
|
h.server.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Start(ctx); err != nil {
|
if err := server.Start(ctx, serverMainScript, string(mainScript)); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +85,6 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
|
||||||
router := chi.NewRouter()
|
router := chi.NewRouter()
|
||||||
|
|
||||||
handler := &Handler{
|
handler := &Handler{
|
||||||
uploadMaxFileSize: opts.UploadMaxFileSize,
|
|
||||||
sockjsOpts: opts.SockJS,
|
sockjsOpts: opts.SockJS,
|
||||||
router: router,
|
router: router,
|
||||||
serverModuleFactories: opts.ServerModuleFactories,
|
serverModuleFactories: opts.ServerModuleFactories,
|
||||||
|
@ -108,15 +102,9 @@ func NewHandler(funcs ...HandlerOptionFunc) *Handler {
|
||||||
r.Get("/client.js.map", handler.handleSDKClientMap)
|
r.Get("/client.js.map", handler.handleSDKClientMap)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/api", func(r chi.Router) {
|
|
||||||
r.Post("/v1/upload", handler.handleAppUpload)
|
|
||||||
r.Get("/v1/download/{bucket}/{blobID}", handler.handleAppDownload)
|
|
||||||
|
|
||||||
r.Get("/v1/fetch", handler.handleAppFetch)
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, fn := range opts.HTTPMounts {
|
for _, fn := range opts.HTTPMounts {
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(handler.contextMiddleware)
|
||||||
fn(r)
|
fn(r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ type HandlerOptions struct {
|
||||||
Bus bus.Bus
|
Bus bus.Bus
|
||||||
SockJS sockjs.Options
|
SockJS sockjs.Options
|
||||||
ServerModuleFactories []app.ServerModuleFactory
|
ServerModuleFactories []app.ServerModuleFactory
|
||||||
UploadMaxFileSize int64
|
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
HTTPMounts []func(r chi.Router)
|
HTTPMounts []func(r chi.Router)
|
||||||
HTTPMiddlewares []func(next http.Handler) http.Handler
|
HTTPMiddlewares []func(next http.Handler) http.Handler
|
||||||
|
@ -31,7 +30,6 @@ func defaultHandlerOptions() *HandlerOptions {
|
||||||
Bus: memory.NewBus(),
|
Bus: memory.NewBus(),
|
||||||
SockJS: sockjsOptions,
|
SockJS: sockjsOptions,
|
||||||
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
|
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
|
||||||
UploadMaxFileSize: 10 << (10 * 2), // 10Mb
|
|
||||||
HTTPClient: &http.Client{
|
HTTPClient: &http.Client{
|
||||||
Timeout: time.Second * 30,
|
Timeout: time.Second * 30,
|
||||||
},
|
},
|
||||||
|
@ -60,12 +58,6 @@ func WithBus(bus bus.Bus) HandlerOptionFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithUploadMaxFileSize(size int64) HandlerOptionFunc {
|
|
||||||
return func(opts *HandlerOptions) {
|
|
||||||
opts.UploadMaxFileSize = size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithHTTPClient(client *http.Client) HandlerOptionFunc {
|
func WithHTTPClient(client *http.Client) HandlerOptionFunc {
|
||||||
return func(opts *HandlerOptions) {
|
return func(opts *HandlerOptions) {
|
||||||
opts.HTTPClient = client
|
opts.HTTPClient = client
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
|
||||||
"github.com/igm/sockjs-go/v3/sockjs"
|
"github.com/igm/sockjs-go/v3/sockjs"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
@ -15,11 +14,6 @@ const (
|
||||||
statusChannelClosed = iota
|
statusChannelClosed = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
ContextKeySessionID module.ContextKey = "sessionId"
|
|
||||||
ContextKeyOriginRequest module.ContextKey = "originRequest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleSockJS(w http.ResponseWriter, r *http.Request) {
|
||||||
h.mutex.RLock()
|
h.mutex.RLock()
|
||||||
defer h.mutex.RUnlock()
|
defer h.mutex.RUnlock()
|
||||||
|
@ -42,19 +36,18 @@ func (h *Handler) handleSockJSSession(sess sockjs.Session) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go h.handleServerMessages(ctx, sess)
|
go h.handleOutgoingMessages(ctx, sess)
|
||||||
h.handleClientMessages(ctx, sess)
|
h.handleIncomingMessages(ctx, sess)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session) {
|
func (h *Handler) handleOutgoingMessages(ctx context.Context, sess sockjs.Session) {
|
||||||
messages, err := h.bus.Subscribe(ctx, module.MessageNamespaceServer)
|
envelopes, err := h.bus.Subscribe(ctx, AddressOutgoingMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
// Close messages subscriber
|
h.bus.Unsubscribe(AddressOutgoingMessage, envelopes)
|
||||||
h.bus.Unsubscribe(ctx, module.MessageNamespaceServer, messages)
|
|
||||||
|
|
||||||
logger.Debug(ctx, "unsubscribed")
|
logger.Debug(ctx, "unsubscribed")
|
||||||
|
|
||||||
|
@ -72,26 +65,22 @@ func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|
||||||
case msg := <-messages:
|
case env := <-envelopes:
|
||||||
serverMessage, ok := msg.(*module.ServerMessage)
|
outgoingMessage, ok := env.Message().(*OutgoingMessage)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx,
|
ctx,
|
||||||
"unexpected server message",
|
"unexpected outgoing message",
|
||||||
logger.F("message", msg),
|
logger.F("message", env.Message()),
|
||||||
)
|
)
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := module.ContextValue[string](serverMessage.Context, ContextKeySessionID)
|
isDest := outgoingMessage.SessionID == "" || outgoingMessage.SessionID == sess.ID()
|
||||||
|
|
||||||
isDest := sessionID == "" || sessionID == sess.ID()
|
|
||||||
if !isDest {
|
if !isDest {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := json.Marshal(serverMessage.Data)
|
payload, err := json.Marshal(outgoingMessage.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -132,7 +121,7 @@ func (h *Handler) handleServerMessages(ctx context.Context, sess sockjs.Session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session) {
|
func (h *Handler) handleIncomingMessages(ctx context.Context, sess sockjs.Session) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -145,7 +134,7 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
|
||||||
|
|
||||||
data, err := sess.RecvCtx(ctx)
|
data, err := sess.RecvCtx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sockjs.ErrSessionNotOpen) {
|
if errors.Is(err, sockjs.ErrSessionNotOpen) || errors.Is(err, context.Canceled) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +163,7 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
case message.Type == WebsocketMessageTypeMessage:
|
case message.Type == WebsocketMessageTypeMessage:
|
||||||
var payload map[string]interface{}
|
var payload map[string]any
|
||||||
if err := json.Unmarshal(message.Payload, &payload); err != nil {
|
if err := json.Unmarshal(message.Payload, &payload); err != nil {
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -186,26 +175,22 @@ func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := logger.With(ctx, logger.F("payload", payload))
|
ctx := logger.With(ctx, logger.F("payload", payload))
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
ctx = WithContextHTTPRequest(ctx, sess.Request())
|
||||||
ContextKeySessionID: sess.ID(),
|
ctx = WithContextSessionID(ctx, sess.ID())
|
||||||
ContextKeyOriginRequest: sess.Request(),
|
|
||||||
})
|
|
||||||
|
|
||||||
clientMessage := module.NewClientMessage(ctx, payload)
|
incomingMessage := NewIncomingMessageEnvelope(ctx, payload)
|
||||||
|
|
||||||
logger.Debug(ctx, "publishing new client message", logger.F("message", clientMessage))
|
logger.Debug(ctx, "publishing new incoming message", logger.F("message", incomingMessage))
|
||||||
|
|
||||||
if err := h.bus.Publish(ctx, clientMessage); err != nil {
|
if err := h.bus.Publish(incomingMessage); err != nil {
|
||||||
logger.Error(ctx, "could not publish message",
|
logger.Error(ctx, "could not publish message",
|
||||||
logger.CapturedE(errors.WithStack(err)),
|
logger.CapturedE(errors.WithStack(err)),
|
||||||
logger.F("message", clientMessage),
|
logger.F("message", incomingMessage),
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug(ctx, "new client message published", logger.F("message", clientMessage))
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx,
|
ctx,
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrCodeForbidden = "forbidden"
|
||||||
|
ErrCodeInternalError = "internal-error"
|
||||||
|
ErrCodeBadRequest = "bad-request"
|
||||||
|
ErrCodeNotFound = "not-found"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonErrorResponse struct {
|
||||||
|
Error jsonErr `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonErr struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func JSONError(w http.ResponseWriter, status int, code string) {
|
||||||
|
w.Header().Add("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
response := jsonErrorResponse{
|
||||||
|
Error: jsonErr{
|
||||||
|
Code: code,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.Encode(response); err != nil {
|
||||||
|
panic(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServeFile(w http.ResponseWriter, r *http.Request, fs fs.FS, path string) {
|
||||||
|
ctx := logger.With(r.Context(), logger.F("path", path))
|
||||||
|
|
||||||
|
file, err := fs.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(ctx, "error while opening fs file", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
logger.Error(ctx, "error while closing fs file", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "error while retrieving fs file stat", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, ok := file.(io.ReadSeeker)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeContent(w, r, path, info.ModTime(), reader)
|
||||||
|
}
|
|
@ -39,21 +39,17 @@ func TestAppModuleWithMemoryRepository(t *testing.T) {
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
file := "testdata/app.js"
|
script := "testdata/app.js"
|
||||||
|
|
||||||
data, err := os.ReadFile(file)
|
data, err := os.ReadFile(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load(file, string(data)); err != nil {
|
ctx := context.Background()
|
||||||
t.Fatal(err)
|
if err := server.Start(ctx, script, string(data)); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
defer server.Stop()
|
defer server.Stop()
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := server.Start(ctx); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,101 +5,45 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/auth"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
|
||||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const AnonIssuer = "anon"
|
const AnonIssuer = "anon"
|
||||||
|
|
||||||
func AnonymousUser(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...AnonymousUserOptionFunc) func(next http.Handler) http.Handler {
|
func WithAnonymousUser(funcs ...DefaultUserOptionFunc) DefaultUserOptionFunc {
|
||||||
opts := defaultAnonymousUserOptions()
|
return func(opts *DefaultUserOptions) {
|
||||||
for _, fn := range funcs {
|
opts.GetSubject = getAnonymousSubject
|
||||||
fn(opts)
|
opts.GetPreferredUsername = getAnonymousPreferredUsername
|
||||||
}
|
opts.Issuer = AnonIssuer
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
for _, fn := range funcs {
|
||||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
fn(opts)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func generateRandomPreferredUsername(size int) (string, error) {
|
||||||
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
var letters = []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
max := big.NewInt(int64(len(letters)))
|
max := big.NewInt(int64(len(letters)))
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,47 +11,52 @@ func defaultGetCookieDomain(r *http.Request) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnonymousUserOptions struct {
|
type DefaultUserOptions struct {
|
||||||
GetCookieDomain GetCookieDomainFunc
|
GetCookieDomain GetCookieDomainFunc
|
||||||
CookieDuration time.Duration
|
CookieDuration time.Duration
|
||||||
Tenant string
|
Tenant string
|
||||||
Entrypoint string
|
Entrypoint string
|
||||||
Role 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 {
|
func defaultUserOptions() *DefaultUserOptions {
|
||||||
return &AnonymousUserOptions{
|
return &DefaultUserOptions{
|
||||||
GetCookieDomain: defaultGetCookieDomain,
|
GetCookieDomain: defaultGetCookieDomain,
|
||||||
CookieDuration: 24 * time.Hour,
|
CookieDuration: 24 * time.Hour,
|
||||||
Tenant: "",
|
Tenant: "",
|
||||||
Entrypoint: "",
|
Entrypoint: "",
|
||||||
Role: "",
|
Role: "",
|
||||||
|
GetSubject: getAnonymousSubject,
|
||||||
|
GetPreferredUsername: getAnonymousPreferredUsername,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) AnonymousUserOptionFunc {
|
func WithCookieOptions(getCookieDomain GetCookieDomainFunc, duration time.Duration) DefaultUserOptionFunc {
|
||||||
return func(opts *AnonymousUserOptions) {
|
return func(opts *DefaultUserOptions) {
|
||||||
opts.GetCookieDomain = getCookieDomain
|
opts.GetCookieDomain = getCookieDomain
|
||||||
opts.CookieDuration = duration
|
opts.CookieDuration = duration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithTenant(tenant string) AnonymousUserOptionFunc {
|
func WithTenant(tenant string) DefaultUserOptionFunc {
|
||||||
return func(opts *AnonymousUserOptions) {
|
return func(opts *DefaultUserOptions) {
|
||||||
opts.Tenant = tenant
|
opts.Tenant = tenant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithEntrypoint(entrypoint string) AnonymousUserOptionFunc {
|
func WithEntrypoint(entrypoint string) DefaultUserOptionFunc {
|
||||||
return func(opts *AnonymousUserOptions) {
|
return func(opts *DefaultUserOptions) {
|
||||||
opts.Entrypoint = entrypoint
|
opts.Entrypoint = entrypoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithRole(role string) AnonymousUserOptionFunc {
|
func WithRole(role string) DefaultUserOptionFunc {
|
||||||
return func(opts *AnonymousUserOptions) {
|
return func(opts *DefaultUserOptions) {
|
||||||
opts.Role = role
|
opts.Role = role
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
@ -68,7 +66,7 @@ func (m *Module) getClaim(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
ctx := util.AssertContext(call.Argument(0), rt)
|
ctx := util.AssertContext(call.Argument(0), rt)
|
||||||
claimName := util.AssertString(call.Argument(1), rt)
|
claimName := util.AssertString(call.Argument(1), rt)
|
||||||
|
|
||||||
req, ok := ctx.Value(edgeHTTP.ContextKeyOriginRequest).(*http.Request)
|
req, ok := edgehttp.ContextHTTPRequest(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(rt.ToValue(errors.New("could not find http request in context")))
|
panic(rt.ToValue(errors.New("could not find http request in context")))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,14 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||||
|
@ -22,7 +22,9 @@ import (
|
||||||
func TestAuthModule(t *testing.T) {
|
func TestAuthModule(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
logger.SetLevel(slog.LevelDebug)
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
key := getDummyKey()
|
key := getDummyKey()
|
||||||
|
|
||||||
|
@ -33,17 +35,15 @@ func TestAuthModule(t *testing.T) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/auth.js")
|
script := "testdata/auth.js"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load("testdata/auth.js", string(data)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := server.Start(ctx); err != nil {
|
if err := server.Start(ctx, script, string(data)); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ func TestAuthModule(t *testing.T) {
|
||||||
|
|
||||||
req.Header.Add("Authorization", "Bearer "+string(rawToken))
|
req.Header.Add("Authorization", "Bearer "+string(rawToken))
|
||||||
|
|
||||||
ctx = context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
|
ctx = edgehttp.WithContextHTTPRequest(context.Background(), req)
|
||||||
|
|
||||||
if _, err := server.ExecFuncByName(ctx, "testAuth", ctx); err != nil {
|
if _, err := server.ExecFuncByName(ctx, "testAuth", ctx); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
@ -81,7 +81,9 @@ func TestAuthModule(t *testing.T) {
|
||||||
func TestAuthAnonymousModule(t *testing.T) {
|
func TestAuthAnonymousModule(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
logger.SetLevel(slog.LevelDebug)
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
key := getDummyKey()
|
key := getDummyKey()
|
||||||
|
|
||||||
|
@ -90,17 +92,15 @@ func TestAuthAnonymousModule(t *testing.T) {
|
||||||
ModuleFactory(WithJWT(getDummyKeySet(key))),
|
ModuleFactory(WithJWT(getDummyKeySet(key))),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/auth_anonymous.js")
|
script := "testdata/auth_anonymous.js"
|
||||||
|
|
||||||
|
data, err := os.ReadFile("testdata/auth_anonymous.js")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load("testdata/auth_anonymous.js", string(data)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := server.Start(ctx); err != nil {
|
if err := server.Start(ctx, script, string(data)); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ func TestAuthAnonymousModule(t *testing.T) {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = context.WithValue(context.Background(), edgeHTTP.ContextKeyOriginRequest, req)
|
ctx = edgehttp.WithContextHTTPRequest(context.Background(), req)
|
||||||
|
|
||||||
if _, err := server.ExecFuncByName(ctx, "testAuth", ctx); err != nil {
|
if _, err := server.ExecFuncByName(ctx, "testAuth", ctx); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
package blob
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/storage"
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MessageNamespaceUploadRequest bus.MessageNamespace = "uploadRequest"
|
|
||||||
MessageNamespaceUploadResponse bus.MessageNamespace = "uploadResponse"
|
|
||||||
MessageNamespaceDownloadRequest bus.MessageNamespace = "downloadRequest"
|
|
||||||
MessageNamespaceDownloadResponse bus.MessageNamespace = "downloadResponse"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MessageUploadRequest struct {
|
|
||||||
Context context.Context
|
|
||||||
RequestID string
|
|
||||||
FileHeader *multipart.FileHeader
|
|
||||||
Metadata map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MessageUploadRequest) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return MessageNamespaceUploadRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageUploadRequest(ctx context.Context, fileHeader *multipart.FileHeader, metadata map[string]interface{}) *MessageUploadRequest {
|
|
||||||
return &MessageUploadRequest{
|
|
||||||
Context: ctx,
|
|
||||||
RequestID: ulid.Make().String(),
|
|
||||||
FileHeader: fileHeader,
|
|
||||||
Metadata: metadata,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageUploadResponse struct {
|
|
||||||
RequestID string
|
|
||||||
BlobID storage.BlobID
|
|
||||||
Bucket string
|
|
||||||
Allow bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MessageUploadResponse) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return MessageNamespaceDownloadResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageUploadResponse(requestID string) *MessageUploadResponse {
|
|
||||||
return &MessageUploadResponse{
|
|
||||||
RequestID: requestID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageDownloadRequest struct {
|
|
||||||
Context context.Context
|
|
||||||
RequestID string
|
|
||||||
Bucket string
|
|
||||||
BlobID storage.BlobID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MessageDownloadRequest) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return MessageNamespaceDownloadRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageDownloadRequest(ctx context.Context, bucket string, blobID storage.BlobID) *MessageDownloadRequest {
|
|
||||||
return &MessageDownloadRequest{
|
|
||||||
Context: ctx,
|
|
||||||
RequestID: ulid.Make().String(),
|
|
||||||
Bucket: bucket,
|
|
||||||
BlobID: blobID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageDownloadResponse struct {
|
|
||||||
RequestID string
|
|
||||||
Allow bool
|
|
||||||
BlobInfo storage.BlobInfo
|
|
||||||
Blob io.ReadSeekCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MessageDownloadResponse) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return MessageNamespaceDownloadResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageDownloadResponse(requestID string) *MessageDownloadResponse {
|
|
||||||
return &MessageDownloadResponse{
|
|
||||||
RequestID: requestID,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AddressUpload bus.Address = "module/blob/upload"
|
||||||
|
AddressDownload bus.Address = "module/blob/download"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UploadRequest struct {
|
||||||
|
Context context.Context
|
||||||
|
FileHeader *multipart.FileHeader
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUploadRequestEnvelope(ctx context.Context, fileHeader *multipart.FileHeader, metadata map[string]interface{}) bus.Envelope {
|
||||||
|
return bus.NewEnvelope(AddressUpload, &UploadRequest{
|
||||||
|
Context: ctx,
|
||||||
|
FileHeader: fileHeader,
|
||||||
|
Metadata: metadata,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadResponse struct {
|
||||||
|
Allow bool
|
||||||
|
Bucket string
|
||||||
|
BlobID storage.BlobID
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadRequest struct {
|
||||||
|
Context context.Context
|
||||||
|
Bucket string
|
||||||
|
BlobID storage.BlobID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDownloadRequestEnvelope(ctx context.Context, bucket string, blobID storage.BlobID) bus.Envelope {
|
||||||
|
return bus.NewEnvelope(AddressDownload, &DownloadRequest{
|
||||||
|
Context: ctx,
|
||||||
|
Bucket: bucket,
|
||||||
|
BlobID: blobID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadResponse struct {
|
||||||
|
Allow bool
|
||||||
|
Blob io.ReadSeekCloser
|
||||||
|
BlobInfo storage.BlobInfo
|
||||||
|
}
|
|
@ -0,0 +1,230 @@
|
||||||
|
package blob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type uploadResponse struct {
|
||||||
|
Bucket string `json:"bucket"`
|
||||||
|
BlobID storage.BlobID `json:"blobId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Mount(uploadMaxFileSize int64) func(r chi.Router) {
|
||||||
|
return func(r chi.Router) {
|
||||||
|
r.Post("/api/v1/upload", getAppUploadHandler(uploadMaxFileSize))
|
||||||
|
r.Get("/api/v1/download/{bucket}/{blobID}", handleAppDownload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAppUploadHandler(uploadMaxFileSize int64) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, uploadMaxFileSize)
|
||||||
|
|
||||||
|
if err := r.ParseMultipartForm(uploadMaxFileSize); err != nil {
|
||||||
|
logger.Error(ctx, "could not parse multipart form", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
edgehttp.JSONError(w, http.StatusBadRequest, edgehttp.ErrCodeBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, fileHeader, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not read form file", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
edgehttp.JSONError(w, http.StatusBadRequest, edgehttp.ErrCodeBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata map[string]any
|
||||||
|
|
||||||
|
rawMetadata := r.Form.Get("metadata")
|
||||||
|
if rawMetadata != "" {
|
||||||
|
if err := json.Unmarshal([]byte(rawMetadata), &metadata); err != nil {
|
||||||
|
logger.Error(ctx, "could not parse metadata", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
edgehttp.JSONError(w, http.StatusBadRequest, edgehttp.ErrCodeBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestEnv := NewUploadRequestEnvelope(ctx, fileHeader, metadata)
|
||||||
|
|
||||||
|
bus, ok := edgehttp.ContextBus(ctx)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(ctx, "could find bus on context")
|
||||||
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := bus.Request(ctx, requestEnv)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(ctx, "upload reply", logger.F("reply", reply))
|
||||||
|
|
||||||
|
replyMessage, ok := reply.Message().(*UploadResponse)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "unexpected upload response message",
|
||||||
|
logger.F("message", reply.Message()),
|
||||||
|
)
|
||||||
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !replyMessage.Allow {
|
||||||
|
edgehttp.JSONError(w, http.StatusForbidden, edgehttp.ErrCodeForbidden)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
res := &uploadResponse{
|
||||||
|
Bucket: replyMessage.Bucket,
|
||||||
|
BlobID: replyMessage.BlobID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := encoder.Encode(res); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not encode upload response"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bucket := chi.URLParam(r, "bucket")
|
||||||
|
blobID := chi.URLParam(r, "blobID")
|
||||||
|
|
||||||
|
ctx := logger.With(r.Context(), logger.F("blobID", blobID), logger.F("bucket", bucket))
|
||||||
|
|
||||||
|
requestMsg := NewDownloadRequestEnvelope(ctx, bucket, storage.BlobID(blobID))
|
||||||
|
|
||||||
|
bs, ok := edgehttp.ContextBus(ctx)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(ctx, "could find bus on context")
|
||||||
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := bs.Request(ctx, requestMsg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not retrieve file", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
replyMessage, ok := reply.Message().(*DownloadResponse)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "unexpected download response message",
|
||||||
|
logger.CapturedE(errors.WithStack(bus.ErrUnexpectedMessage)),
|
||||||
|
logger.F("message", reply),
|
||||||
|
)
|
||||||
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !replyMessage.Allow {
|
||||||
|
edgehttp.JSONError(w, http.StatusForbidden, edgehttp.ErrCodeForbidden)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if replyMessage.Blob == nil {
|
||||||
|
edgehttp.JSONError(w, http.StatusNotFound, edgehttp.ErrCodeNotFound)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := replyMessage.Blob.Close(); err != nil {
|
||||||
|
logger.Error(ctx, "could not close blob", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// TODO Fix usage of ServeContent
|
||||||
|
// http.ServeContent(w, r, string(replyMessage.BlobInfo.ID()), replyMessage.BlobInfo.ModTime(), replyMessage.Blob)
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", replyMessage.BlobInfo.ContentType())
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, replyMessage.Blob); err != nil {
|
||||||
|
logger.Error(ctx, "could not write blob", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type uploadedFile struct {
|
||||||
|
multipart.File
|
||||||
|
header *multipart.FileHeader
|
||||||
|
modTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat implements fs.File
|
||||||
|
func (f *uploadedFile) Stat() (fs.FileInfo, error) {
|
||||||
|
return &uploadedFileInfo{
|
||||||
|
header: f.header,
|
||||||
|
modTime: f.modTime,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type uploadedFileInfo struct {
|
||||||
|
header *multipart.FileHeader
|
||||||
|
modTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir implements fs.FileInfo
|
||||||
|
func (i *uploadedFileInfo) IsDir() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModTime implements fs.FileInfo
|
||||||
|
func (i *uploadedFileInfo) ModTime() time.Time {
|
||||||
|
return i.modTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode implements fs.FileInfo
|
||||||
|
func (i *uploadedFileInfo) Mode() fs.FileMode {
|
||||||
|
return os.ModePerm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements fs.FileInfo
|
||||||
|
func (i *uploadedFileInfo) Name() string {
|
||||||
|
return i.header.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size implements fs.FileInfo
|
||||||
|
func (i *uploadedFileInfo) Size() int64 {
|
||||||
|
return i.header.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sys implements fs.FileInfo
|
||||||
|
func (i *uploadedFileInfo) Sys() any {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ fs.File = &uploadedFile{}
|
||||||
|
_ fs.FileInfo = &uploadedFileInfo{}
|
||||||
|
)
|
|
@ -236,33 +236,34 @@ func (m *Module) getBucketSize(call goja.FunctionCall, rt *goja.Runtime) goja.Va
|
||||||
func (m *Module) handleMessages() {
|
func (m *Module) handleMessages() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
go func() {
|
uploadRequestErrs := m.bus.Reply(ctx, AddressUpload, func(env bus.Envelope) (any, error) {
|
||||||
err := m.bus.Reply(ctx, MessageNamespaceUploadRequest, func(msg bus.Message) (bus.Message, error) {
|
uploadRequest, ok := env.Message().(*UploadRequest)
|
||||||
uploadRequest, ok := msg.(*MessageUploadRequest)
|
if !ok {
|
||||||
if !ok {
|
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message upload request, got '%T'", env.Message())
|
||||||
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message upload request, got '%T'", msg)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
res, err := m.handleUploadRequest(uploadRequest)
|
res, err := m.handleUploadRequest(uploadRequest)
|
||||||
if err != nil {
|
|
||||||
logger.Error(ctx, "could not handle upload request", logger.CapturedE(errors.WithStack(err)))
|
|
||||||
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(ctx, "upload request response", logger.F("response", res))
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
logger.Error(ctx, "could not handle upload request", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(ctx, "upload request response", logger.F("response", res))
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for err := range uploadRequestErrs {
|
||||||
|
logger.Error(ctx, "error while replying to upload requests", logger.CapturedE(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
err := m.bus.Reply(ctx, MessageNamespaceDownloadRequest, func(msg bus.Message) (bus.Message, error) {
|
downloadRequestErrs := m.bus.Reply(ctx, AddressDownload, func(env bus.Envelope) (any, error) {
|
||||||
downloadRequest, ok := msg.(*MessageDownloadRequest)
|
downloadRequest, ok := env.Message().(*DownloadRequest)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message download request, got '%T'", msg)
|
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message download request, got '%T'", env.Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := m.handleDownloadRequest(downloadRequest)
|
res, err := m.handleDownloadRequest(downloadRequest)
|
||||||
|
@ -274,14 +275,15 @@ func (m *Module) handleMessages() {
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
for err := range downloadRequestErrs {
|
||||||
|
logger.Fatal(ctx, "error while replying to download requests", logger.CapturedE(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) handleUploadRequest(req *MessageUploadRequest) (*MessageUploadResponse, error) {
|
func (m *Module) handleUploadRequest(req *UploadRequest) (*UploadResponse, error) {
|
||||||
blobID := storage.NewBlobID()
|
blobID := storage.NewBlobID()
|
||||||
res := NewMessageUploadResponse(req.RequestID)
|
res := &UploadResponse{}
|
||||||
|
|
||||||
ctx := logger.With(req.Context, logger.F("blobID", blobID))
|
ctx := logger.With(req.Context, logger.F("blobID", blobID))
|
||||||
|
|
||||||
|
@ -302,11 +304,11 @@ func (m *Module) handleUploadRequest(req *MessageUploadRequest) (*MessageUploadR
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, ok := rawResult.Export().(map[string]interface{})
|
result, ok := rawResult.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Errorf(
|
return nil, errors.Errorf(
|
||||||
"unexpected onBlobUpload result: expected 'map[string]interface{}', got '%T'",
|
"unexpected onBlobUpload result: expected 'map[string]interface{}', got '%T'",
|
||||||
rawResult.Export(),
|
rawResult,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,8 +395,8 @@ func (m *Module) saveBlob(ctx context.Context, bucketName string, blobID storage
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDownloadResponse, error) {
|
func (m *Module) handleDownloadRequest(req *DownloadRequest) (*DownloadResponse, error) {
|
||||||
res := NewMessageDownloadResponse(req.RequestID)
|
res := &DownloadResponse{}
|
||||||
|
|
||||||
rawResult, err := m.server.ExecFuncByName(req.Context, "onBlobDownload", req.Context, req.Bucket, req.BlobID)
|
rawResult, err := m.server.ExecFuncByName(req.Context, "onBlobDownload", req.Context, req.Bucket, req.BlobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -407,11 +409,11 @@ func (m *Module) handleDownloadRequest(req *MessageDownloadRequest) (*MessageDow
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, ok := rawResult.Export().(map[string]interface{})
|
result, ok := rawResult.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Errorf(
|
return nil, errors.Errorf(
|
||||||
"unexpected onBlobDownload result: expected 'map[string]interface{}', got '%T'",
|
"unexpected onBlobDownload result: expected 'map[string]interface{}', got '%T'",
|
||||||
rawResult.Export(),
|
rawResult,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,9 @@ import (
|
||||||
func TestBlobModule(t *testing.T) {
|
func TestBlobModule(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
logger.SetLevel(slog.LevelDebug)
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
bus := memory.NewBus()
|
bus := memory.NewBus()
|
||||||
store := sqlite.NewBlobStore(":memory:?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000")
|
store := sqlite.NewBlobStore(":memory:?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000")
|
||||||
|
@ -28,19 +30,17 @@ func TestBlobModule(t *testing.T) {
|
||||||
ModuleFactory(bus, store),
|
ModuleFactory(bus, store),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := os.ReadFile("testdata/blob.js")
|
script := "testdata/blob.js"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load("testdata/blob.js", string(data)); err != nil {
|
ctx := context.Background()
|
||||||
t.Fatal(err)
|
if err := server.Start(ctx, script, string(data)); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
defer server.Stop()
|
defer server.Stop()
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := server.Start(ctx); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{}
|
|
@ -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{}
|
|
@ -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{}
|
|
@ -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{}
|
|
@ -2,22 +2,13 @@ package cast
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/barnybug/go-cast"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"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 {
|
type CachedDevice struct {
|
||||||
Device
|
Device
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
@ -27,66 +18,49 @@ func (d CachedDevice) Expired() bool {
|
||||||
return d.UpdatedAt.Add(30 * time.Minute).Before(time.Now())
|
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
|
var cache sync.Map
|
||||||
|
|
||||||
func getCachedDevice(uuid string) (Device, bool) {
|
func getCachedDevice(uuid string) (Device, bool) {
|
||||||
value, exists := cache.Load(uuid)
|
value, exists := cache.Load(uuid)
|
||||||
if !exists {
|
if !exists {
|
||||||
return Device{}, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedDevice, ok := value.(CachedDevice)
|
cachedDevice, ok := value.(CachedDevice)
|
||||||
if !ok {
|
if !ok {
|
||||||
return Device{}, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if cachedDevice.Expired() {
|
if cachedDevice.Expired() {
|
||||||
return Device{}, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return cachedDevice.Device, true
|
return cachedDevice.Device, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func cacheDevice(dev Device) {
|
func cacheDevice(dev Device) {
|
||||||
cache.Store(dev.UUID, CachedDevice{
|
cache.Store(dev.DeviceID(), CachedDevice{
|
||||||
Device: dev,
|
Device: dev,
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
|
func getDeviceClientByID(ctx context.Context, deviceID string) (Client, error) {
|
||||||
device, err := FindDeviceByUUID(ctx, uuid)
|
device, err := findCachedDeviceByID(ctx, deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
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
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindDeviceByUUID(ctx context.Context, uuid string) (Device, error) {
|
func findCachedDeviceByID(ctx context.Context, deviceID string) (Device, error) {
|
||||||
device, exists := getCachedDevice(uuid)
|
device, exists := getCachedDevice(deviceID)
|
||||||
if exists {
|
if exists {
|
||||||
return device, nil
|
return device, nil
|
||||||
}
|
}
|
||||||
|
@ -94,18 +68,14 @@ func FindDeviceByUUID(ctx context.Context, uuid string) (Device, error) {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
devices, err := SearchDevices(ctx)
|
device, err := Find(ctx, deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Device{}, nil
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for dev := range devices {
|
cacheDevice(device)
|
||||||
if dev.UUID == uuid {
|
|
||||||
return dev, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Device{}, errors.Errorf("could not find device '%s'", uuid)
|
return device, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListDevices(ctx context.Context, refresh bool) ([]Device, error) {
|
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
|
return devices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, err := SearchDevices(ctx)
|
devices, err := SearchDevices(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for dev := range ch {
|
|
||||||
devices = append(devices, dev)
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices, nil
|
return devices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchDevicesMutex sync.Mutex
|
var searchDevicesMutex sync.Mutex
|
||||||
|
|
||||||
func SearchDevices(ctx context.Context) (chan Device, error) {
|
func SearchDevices(ctx context.Context) ([]Device, error) {
|
||||||
service := NewService(ctx)
|
searchDevicesMutex.Lock()
|
||||||
defer service.Stop()
|
defer searchDevicesMutex.Unlock()
|
||||||
|
|
||||||
go func() {
|
devices, err := Scan(ctx)
|
||||||
searchDevicesMutex.Lock()
|
if err != nil {
|
||||||
defer searchDevicesMutex.Unlock()
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
|
for _, device := range devices {
|
||||||
logger.Error(ctx, "error while running cast service discovery", logger.CapturedE(errors.WithStack(err)))
|
cacheDevice(device)
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return devices, nil
|
return devices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var loadURLMutex sync.Mutex
|
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()
|
loadURLMutex.Lock()
|
||||||
defer loadURLMutex.Unlock()
|
defer loadURLMutex.Unlock()
|
||||||
|
|
||||||
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
client, err := getDeviceClientByID(ctx, deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.Connect(ctx); err != nil {
|
defer func() {
|
||||||
return errors.WithStack(err)
|
if err := client.Close(); err != nil {
|
||||||
}
|
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
|
||||||
defer client.Close()
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
controller, err := client.URL(ctx)
|
if err := client.Load(ctx, url); err != nil {
|
||||||
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 errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
var stopCastMutex sync.Mutex
|
||||||
|
|
||||||
func StopCast(ctx context.Context, deviceUUID string) error {
|
func StopCast(ctx context.Context, deviceID string) error {
|
||||||
stopCastMutex.Lock()
|
stopCastMutex.Lock()
|
||||||
defer stopCastMutex.Unlock()
|
defer stopCastMutex.Unlock()
|
||||||
|
|
||||||
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
client, err := getDeviceClientByID(ctx, deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.Connect(ctx); err != nil {
|
defer func() {
|
||||||
return errors.WithStack(err)
|
if err := client.Close(); err != nil {
|
||||||
}
|
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
|
||||||
defer client.Close()
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if _, err := client.Receiver().QuitApp(ctx); err != nil {
|
if err := client.Unload(ctx); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,42 +171,25 @@ func StopCast(ctx context.Context, deviceUUID string) error {
|
||||||
|
|
||||||
var getStatusMutex sync.Mutex
|
var getStatusMutex sync.Mutex
|
||||||
|
|
||||||
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
|
func GetStatus(ctx context.Context, deviceID string) (DeviceStatus, error) {
|
||||||
getStatusMutex.Lock()
|
getStatusMutex.Lock()
|
||||||
defer getStatusMutex.Unlock()
|
defer getStatusMutex.Unlock()
|
||||||
|
|
||||||
client, err := getDeviceClientByUUID(ctx, deviceUUID)
|
client, err := getDeviceClientByID(ctx, deviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.Connect(ctx); err != nil {
|
defer func() {
|
||||||
return nil, errors.WithStack(err)
|
if err := client.Close(); err != nil {
|
||||||
}
|
logger.Error(ctx, "could not close client", logger.CapturedE(errors.WithStack(err)))
|
||||||
defer client.Close()
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
ctrlStatus, err := client.Receiver().GetStatus(ctx)
|
status, err := client.Status(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
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
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
package cast
|
package cast_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cdr.dev/slog"
|
"cdr.dev/slog"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/cast"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"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) {
|
func TestCastLoadURL(t *testing.T) {
|
||||||
|
@ -21,56 +27,65 @@ func TestCastLoadURL(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.SetLevel(slog.LevelDebug)
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
devices, err := ListDevices(ctx, true)
|
devices, err := cast.ListDevices(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(errors.WithStack(err))
|
t.Error(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("DEVICES: %s", spew.Sdump(devices))
|
t.Logf("DEVICES: %s", spew.Sdump(devices))
|
||||||
|
|
||||||
if e, g := 1, len(devices); e != g {
|
if e, g := 1, len(devices); e > g {
|
||||||
t.Fatalf("len(devices): expected '%v', got '%v'", 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 {
|
if err != nil {
|
||||||
t.Error(errors.WithStack(err))
|
t.Error(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("CACHED DEVICES: %s", spew.Sdump(devices))
|
t.Logf("CACHED DEVICES: %s", spew.Sdump(devices))
|
||||||
|
|
||||||
if e, g := 1, len(devices); e != g {
|
if e, g := 1, len(devices); e > g {
|
||||||
t.Fatalf("len(devices): expected '%v', got '%v'", 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)
|
ctx, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel2()
|
defer cancel2()
|
||||||
|
|
||||||
if err := LoadURL(ctx, dev.UUID, "https://go.dev"); err != nil {
|
if err := cast.LoadURL(ctx, device.DeviceID(), "https://go.dev"); err != nil {
|
||||||
t.Error(errors.WithStack(err))
|
t.Error(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel3()
|
defer cancel3()
|
||||||
|
|
||||||
status, err := getStatus(ctx, dev.UUID)
|
status, err := cast.GetStatus(ctx, device.DeviceID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(errors.WithStack(err))
|
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)
|
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel4()
|
defer cancel4()
|
||||||
|
|
||||||
if err := StopCast(ctx, dev.UUID); err != nil {
|
if err := cast.StopCast(ctx, device.DeviceID()); err != nil {
|
||||||
t.Error(errors.WithStack(err))
|
t.Error(errors.WithStack(err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}(device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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{}
|
|
@ -1,10 +1,8 @@
|
||||||
package cast
|
package chromecast
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -15,26 +13,31 @@ import (
|
||||||
"github.com/barnybug/go-cast/log"
|
"github.com/barnybug/go-cast/log"
|
||||||
"github.com/hashicorp/mdns"
|
"github.com/hashicorp/mdns"
|
||||||
"github.com/pkg/errors"
|
"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
|
found chan *cast.Client
|
||||||
entriesCh chan *mdns.ServiceEntry
|
entriesCh chan *mdns.ServiceEntry
|
||||||
|
|
||||||
stopPeriodic chan struct{}
|
stopPeriodic chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(ctx context.Context) *Service {
|
func NewDiscovery(ctx context.Context) *Discovery {
|
||||||
s := &Service{
|
d := &Discovery{
|
||||||
found: make(chan *cast.Client),
|
found: make(chan *cast.Client),
|
||||||
entriesCh: make(chan *mdns.ServiceEntry, 10),
|
entriesCh: make(chan *mdns.ServiceEntry, 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
go s.listener(ctx)
|
go d.listener(ctx)
|
||||||
return s
|
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)
|
ifaces, err := findMulticastInterfaces(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
|
@ -82,7 +85,7 @@ func (d *Service) Run(ctx context.Context, interval time.Duration) error {
|
||||||
return nil
|
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{
|
err := mdns.Query(&mdns.QueryParam{
|
||||||
Service: "_googlecast._tcp",
|
Service: "_googlecast._tcp",
|
||||||
Domain: "local",
|
Domain: "local",
|
||||||
|
@ -99,7 +102,7 @@ func (d *Service) queryIface(iface net.Interface, disableIPv4, disableIPv6 bool)
|
||||||
return nil
|
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)
|
ticker := time.NewTicker(interval)
|
||||||
for {
|
for {
|
||||||
select {
|
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 {
|
if d.stopPeriodic != nil {
|
||||||
close(d.stopPeriodic)
|
close(d.stopPeriodic)
|
||||||
d.stopPeriodic = nil
|
d.stopPeriodic = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Service) Found() chan *cast.Client {
|
func (d *Discovery) Found() chan *cast.Client {
|
||||||
return d.found
|
return d.found
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Service) listener(ctx context.Context) {
|
func (d *Discovery) listener(ctx context.Context) {
|
||||||
for entry := range d.entriesCh {
|
for entry := range d.entriesCh {
|
||||||
name := strings.Split(entry.Name, "._googlecast")
|
name := strings.Split(entry.Name, "._googlecast")
|
||||||
// Skip everything that doesn't have googlecast in the fdqn
|
// 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 d.found <- client:
|
||||||
case <-time.After(time.Second):
|
case <-time.After(time.Second):
|
||||||
case <-ctx.Done():
|
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 {
|
func decodeTxtRecord(txt string) map[string]string {
|
||||||
m := make(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) {
|
func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := anet.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -219,7 +205,7 @@ func findMulticastInterfaces(ctx context.Context) ([]net.Interface, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveSupportedProtocols(iface net.Interface) (bool, bool, error) {
|
func retrieveSupportedProtocols(iface net.Interface) (bool, bool, error) {
|
||||||
adresses, err := iface.Addrs()
|
adresses, err := anet.InterfaceAddrsByInterface(&iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, errors.WithStack(err)
|
return false, false, errors.WithStack(err)
|
||||||
}
|
}
|
|
@ -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{}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -2,4 +2,7 @@ package cast
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
var ErrDeviceNotFound = errors.New("device not found")
|
var (
|
||||||
|
ErrDeviceNotFound = errors.New("device not found")
|
||||||
|
ErrUnknownDeviceType = errors.New("unknown device type")
|
||||||
|
)
|
||||||
|
|
|
@ -67,7 +67,7 @@ func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.V
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.Resolve(devices)
|
promise.Resolve(toLegacyDevices(devices...))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return rt.ToValue(promise)
|
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)))
|
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 {
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
status, err := getStatus(ctx, deviceUUID)
|
status, err := GetStatus(ctx, deviceUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithStack(err)
|
err = errors.WithStack(err)
|
||||||
logger.Error(ctx, "error while getting casting device status", logger.CapturedE(err))
|
logger.Error(ctx, "error while getting casting device status", logger.CapturedE(err))
|
||||||
|
|
|
@ -2,7 +2,6 @@ package cast
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -24,24 +23,24 @@ func TestCastModule(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.SetLevel(slog.LevelDebug)
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
server := app.NewServer(
|
server := app.NewServer(
|
||||||
module.ConsoleModuleFactory(),
|
module.ConsoleModuleFactory(),
|
||||||
CastModuleFactory(),
|
CastModuleFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/cast.js")
|
script := "testdata/cast.js"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load("testdata/cast.js", string(data)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := server.Start(ctx); err != nil {
|
if err := server.Start(ctx, script, string(data)); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,24 +58,24 @@ func TestCastModuleRefreshDevices(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.SetLevel(slog.LevelDebug)
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
server := app.NewServer(
|
server := app.NewServer(
|
||||||
module.ConsoleModuleFactory(),
|
module.ConsoleModuleFactory(),
|
||||||
CastModuleFactory(),
|
CastModuleFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/refresh_devices.js")
|
script := "testdata/refresh_devices.js"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load("testdata/refresh_devices.js", string(data)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := server.Start(ctx); err != nil {
|
if err := server.Start(ctx, script, string(data)); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,12 +86,5 @@ func TestCastModuleRefreshDevices(t *testing.T) {
|
||||||
t.Error(errors.WithStack(err))
|
t.Error(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
promise, ok := app.IsPromise(result)
|
spew.Dump(result)
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected promise")
|
|
||||||
}
|
|
||||||
|
|
||||||
value := server.WaitForPromise(promise)
|
|
||||||
|
|
||||||
spew.Dump(value.Export())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package cast
|
||||||
|
|
||||||
|
type DeviceStatus interface {
|
||||||
|
Title() string
|
||||||
|
State() string
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ func (m *ConsoleModule) Name() string {
|
||||||
func (m *ConsoleModule) log(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
func (m *ConsoleModule) log(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
fields := make([]logger.Field, 0)
|
fields := make([]any, 0)
|
||||||
|
|
||||||
stack := rt.CaptureCallStack(0, nil)
|
stack := rt.CaptureCallStack(0, nil)
|
||||||
if len(stack) > 1 {
|
if len(stack) > 1 {
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package fetch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AddressFetchRequest bus.Address = "module/fetch/request"
|
||||||
|
AddressFetchResponse bus.Address = "module/fetch/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FetchRequest struct {
|
||||||
|
Context context.Context
|
||||||
|
RequestID string
|
||||||
|
URL *url.URL
|
||||||
|
RemoteAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFetchRequestEnvelope(ctx context.Context, remoteAddr string, url *url.URL) bus.Envelope {
|
||||||
|
return bus.NewEnvelope(AddressFetchRequest, &FetchRequest{
|
||||||
|
Context: ctx,
|
||||||
|
URL: url,
|
||||||
|
RemoteAddr: remoteAddr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type FetchResponse struct {
|
||||||
|
Allow bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFetchResponseEnvelope(allow bool) bus.Envelope {
|
||||||
|
return bus.NewEnvelope(AddressFetchResponse, &FetchResponse{
|
||||||
|
Allow: allow,
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,49 +0,0 @@
|
||||||
package fetch
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MessageNamespaceFetchRequest bus.MessageNamespace = "fetchRequest"
|
|
||||||
MessageNamespaceFetchResponse bus.MessageNamespace = "fetchResponse"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MessageFetchRequest struct {
|
|
||||||
Context context.Context
|
|
||||||
RequestID string
|
|
||||||
URL *url.URL
|
|
||||||
RemoteAddr string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MessageFetchRequest) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return MessageNamespaceFetchRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageFetchRequest(ctx context.Context, remoteAddr string, url *url.URL) *MessageFetchRequest {
|
|
||||||
return &MessageFetchRequest{
|
|
||||||
Context: ctx,
|
|
||||||
RequestID: ulid.Make().String(),
|
|
||||||
RemoteAddr: remoteAddr,
|
|
||||||
URL: url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageFetchResponse struct {
|
|
||||||
RequestID string
|
|
||||||
Allow bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MessageFetchResponse) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return MessageNamespaceFetchResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageFetchResponse(requestID string) *MessageFetchResponse {
|
|
||||||
return &MessageFetchResponse{
|
|
||||||
RequestID: requestID,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +1,67 @@
|
||||||
package http
|
package fetch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/fetch"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *Handler) handleAppFetch(w http.ResponseWriter, r *http.Request) {
|
func Mount() func(r chi.Router) {
|
||||||
h.mutex.RLock()
|
return func(r chi.Router) {
|
||||||
defer h.mutex.RUnlock()
|
r.Get("/api/v1/fetch", handleAppFetch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppFetch(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
ctx = module.WithContext(ctx, map[module.ContextKey]any{
|
|
||||||
ContextKeyOriginRequest: r,
|
|
||||||
})
|
|
||||||
|
|
||||||
rawURL := r.URL.Query().Get("url")
|
rawURL := r.URL.Query().Get("url")
|
||||||
|
|
||||||
url, err := url.Parse(rawURL)
|
url, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonError(w, http.StatusBadRequest, errorCodeBadRequest)
|
edgehttp.JSONError(w, http.StatusBadRequest, edgehttp.ErrCodeBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
requestMsg := fetch.NewMessageFetchRequest(ctx, r.RemoteAddr, url)
|
requestMsg := NewFetchRequestEnvelope(ctx, r.RemoteAddr, url)
|
||||||
|
|
||||||
reply, err := h.bus.Request(ctx, requestMsg)
|
bus, ok := edgehttp.ContextBus(ctx)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(ctx, "could find bus on context")
|
||||||
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := bus.Request(ctx, requestMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(ctx, "could not retrieve fetch request reply", logger.CapturedE(errors.WithStack(err)))
|
logger.Error(ctx, "could not retrieve fetch request reply", logger.CapturedE(errors.WithStack(err)))
|
||||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug(ctx, "fetch reply", logger.F("reply", reply))
|
logger.Debug(ctx, "fetch reply", logger.F("reply", reply))
|
||||||
|
|
||||||
responseMsg, ok := reply.(*fetch.MessageFetchResponse)
|
responseMsg, ok := reply.Message().(*FetchResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx, "unexpected fetch response message",
|
ctx, "unexpected fetch response message",
|
||||||
logger.F("message", reply),
|
logger.F("message", reply),
|
||||||
)
|
)
|
||||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !responseMsg.Allow {
|
if !responseMsg.Allow {
|
||||||
jsonError(w, http.StatusForbidden, errorCodeForbidden)
|
edgehttp.JSONError(w, http.StatusForbidden, edgehttp.ErrCodeForbidden)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -65,7 +72,7 @@ func (h *Handler) handleAppFetch(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx, "could not create proxy request",
|
ctx, "could not create proxy request",
|
||||||
logger.CapturedE(errors.WithStack(err)),
|
logger.CapturedE(errors.WithStack(err)),
|
||||||
)
|
)
|
||||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -78,13 +85,21 @@ func (h *Handler) handleAppFetch(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
proxyReq.Header.Add("X-Forwarded-From", r.RemoteAddr)
|
proxyReq.Header.Add("X-Forwarded-From", r.RemoteAddr)
|
||||||
|
|
||||||
res, err := h.httpClient.Do(proxyReq)
|
httpClient, ok := edgehttp.ContextHTTPClient(ctx)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(ctx, "could find http client on context")
|
||||||
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := httpClient.Do(proxyReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(
|
logger.Error(
|
||||||
ctx, "could not execute proxy request",
|
ctx, "could not execute proxy request",
|
||||||
logger.CapturedE(errors.WithStack(err)),
|
logger.CapturedE(errors.WithStack(err)),
|
||||||
)
|
)
|
||||||
jsonError(w, http.StatusInternalServerError, errorCodeInternalError)
|
edgehttp.JSONError(w, http.StatusInternalServerError, edgehttp.ErrCodeInternalError)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -40,10 +40,10 @@ func (m *Module) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
func (m *Module) handleMessages() {
|
func (m *Module) handleMessages() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
err := m.bus.Reply(ctx, MessageNamespaceFetchRequest, func(msg bus.Message) (bus.Message, error) {
|
fetchErrs := m.bus.Reply(ctx, AddressFetchRequest, func(env bus.Envelope) (any, error) {
|
||||||
fetchRequest, ok := msg.(*MessageFetchRequest)
|
fetchRequest, ok := env.Message().(*FetchRequest)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected message fetch request, got '%T'", msg)
|
return nil, errors.Wrapf(bus.ErrUnexpectedMessage, "expected fetch request, got '%T'", env.Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := m.handleFetchRequest(fetchRequest)
|
res, err := m.handleFetchRequest(fetchRequest)
|
||||||
|
@ -57,13 +57,14 @@ func (m *Module) handleMessages() {
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
for err := range fetchErrs {
|
||||||
|
logger.Fatal(ctx, "error while replying to fetch requests", logger.CapturedE(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) handleFetchRequest(req *MessageFetchRequest) (*MessageFetchResponse, error) {
|
func (m *Module) handleFetchRequest(req *FetchRequest) (*FetchResponse, error) {
|
||||||
res := NewMessageFetchResponse(req.RequestID)
|
res := &FetchResponse{}
|
||||||
|
|
||||||
ctx := logger.With(
|
ctx := logger.With(
|
||||||
req.Context,
|
req.Context,
|
||||||
|
@ -83,11 +84,11 @@ func (m *Module) handleFetchRequest(req *MessageFetchRequest) (*MessageFetchResp
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, ok := rawResult.Export().(map[string]interface{})
|
result, ok := rawResult.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Errorf(
|
return nil, errors.Errorf(
|
||||||
"unexpected onClientFetch result: expected 'map[string]interface{}', got '%T'",
|
"unexpected onClientFetch result: expected 'map[string]interface{}', got '%T'",
|
||||||
rawResult.Export(),
|
rawResult,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ package fetch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -18,7 +18,9 @@ import (
|
||||||
func TestFetchModule(t *testing.T) {
|
func TestFetchModule(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
logger.SetLevel(slog.LevelDebug)
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
bus := memory.NewBus()
|
bus := memory.NewBus()
|
||||||
|
|
||||||
|
@ -28,22 +30,20 @@ func TestFetchModule(t *testing.T) {
|
||||||
ModuleFactory(bus),
|
ModuleFactory(bus),
|
||||||
)
|
)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile("testdata/fetch.js")
|
path := "testdata/fetch.js"
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load("testdata/fetch.js", string(data)); err != nil {
|
ctx := context.Background()
|
||||||
|
if err := server.Start(ctx, path, string(data)); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
defer server.Stop()
|
defer server.Stop()
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := server.Start(ctx); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for module to startup
|
// Wait for module to startup
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
@ -53,33 +53,33 @@ func TestFetchModule(t *testing.T) {
|
||||||
remoteAddr := "127.0.0.1"
|
remoteAddr := "127.0.0.1"
|
||||||
url, _ := url.Parse("http://example.com")
|
url, _ := url.Parse("http://example.com")
|
||||||
|
|
||||||
rawReply, err := bus.Request(ctx, NewMessageFetchRequest(ctx, remoteAddr, url))
|
reply, err := bus.Request(ctx, NewFetchRequestEnvelope(ctx, remoteAddr, url))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
reply, ok := rawReply.(*MessageFetchResponse)
|
response, ok := reply.Message().(*FetchResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("unexpected reply type '%T'", rawReply)
|
t.Fatalf("unexpected reply message type '%T'", reply.Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
if e, g := true, reply.Allow; e != g {
|
if e, g := true, response.Allow; e != g {
|
||||||
t.Errorf("reply.Allow: expected '%v', got '%v'", e, g)
|
t.Errorf("reply.Allow: expected '%v', got '%v'", e, g)
|
||||||
}
|
}
|
||||||
|
|
||||||
url, _ = url.Parse("https://google.com")
|
url, _ = url.Parse("https://google.com")
|
||||||
|
|
||||||
rawReply, err = bus.Request(ctx, NewMessageFetchRequest(ctx, remoteAddr, url))
|
reply, err = bus.Request(ctx, NewFetchRequestEnvelope(ctx, remoteAddr, url))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
reply, ok = rawReply.(*MessageFetchResponse)
|
response, ok = reply.Message().(*FetchResponse)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("unexpected reply type '%T'", rawReply)
|
t.Fatalf("unexpected reply message type '%T'", reply.Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
if e, g := false, reply.Allow; e != g {
|
if e, g := false, response.Allow; e != g {
|
||||||
t.Errorf("reply.Allow: expected '%v', got '%v'", e, g)
|
t.Errorf("reply.Allow: expected '%v', got '%v'", e, g)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,21 +26,20 @@ func (m *LifecycleModule) OnInit(ctx context.Context, rt *goja.Runtime) (err err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if recovered := recover(); recovered != nil {
|
recovered := recover()
|
||||||
revoveredErr, ok := recovered.(error)
|
if recovered == nil {
|
||||||
if ok {
|
return
|
||||||
logger.Error(ctx, "recovered runtime error", logger.CapturedE(errors.WithStack(revoveredErr)))
|
}
|
||||||
|
|
||||||
err = errors.WithStack(app.ErrUnknownError)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
recoveredErr, ok := recovered.(error)
|
||||||
|
if !ok {
|
||||||
panic(recovered)
|
panic(recovered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = recoveredErr
|
||||||
}()
|
}()
|
||||||
|
|
||||||
call(nil)
|
call(nil, rt.ToValue(ctx))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
package module
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MessageNamespaceClient bus.MessageNamespace = "client"
|
|
||||||
MessageNamespaceServer bus.MessageNamespace = "server"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServerMessage struct {
|
|
||||||
Context context.Context
|
|
||||||
Data interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ServerMessage) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return MessageNamespaceServer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServerMessage(ctx context.Context, data interface{}) *ServerMessage {
|
|
||||||
return &ServerMessage{ctx, data}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientMessage struct {
|
|
||||||
Context context.Context
|
|
||||||
Data map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ClientMessage) MessageNamespace() bus.MessageNamespace {
|
|
||||||
return MessageNamespaceClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClientMessage(ctx context.Context, data map[string]interface{}) *ClientMessage {
|
|
||||||
return &ClientMessage{ctx, data}
|
|
||||||
}
|
|
|
@ -5,8 +5,7 @@ import (
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
|
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -38,10 +37,9 @@ func (m *Module) broadcast(call goja.FunctionCall, rt *goja.Runtime) goja.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
data := call.Argument(0).Export()
|
data := call.Argument(0).Export()
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
msg := module.NewServerMessage(ctx, data)
|
env := edgehttp.NewOutgoingMessageEnvelope("", data)
|
||||||
if err := m.bus.Publish(ctx, msg); err != nil {
|
if err := m.bus.Publish(env); err != nil {
|
||||||
panic(rt.ToValue(errors.WithStack(err)))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,38 +51,36 @@ func (m *Module) send(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
panic(rt.ToValue(errors.New("invalid number of argument")))
|
panic(rt.ToValue(errors.New("invalid number of argument")))
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx context.Context
|
|
||||||
|
|
||||||
firstArg := call.Argument(0)
|
firstArg := call.Argument(0)
|
||||||
|
|
||||||
sessionID, ok := firstArg.Export().(string)
|
sessionID, ok := firstArg.Export().(string)
|
||||||
if ok {
|
if !ok {
|
||||||
ctx = module.WithContext(context.Background(), map[module.ContextKey]any{
|
ctx := util.AssertContext(firstArg, rt)
|
||||||
edgeHTTP.ContextKeySessionID: sessionID,
|
sessionID, ok = edgehttp.ContextSessionID(ctx)
|
||||||
})
|
if !ok {
|
||||||
} else {
|
panic(rt.ToValue(errors.New("could not find session id in context")))
|
||||||
ctx = util.AssertContext(firstArg, rt)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data := call.Argument(1).Export()
|
data := call.Argument(1).Export()
|
||||||
|
|
||||||
msg := module.NewServerMessage(ctx, data)
|
env := edgehttp.NewOutgoingMessageEnvelope(sessionID, data)
|
||||||
if err := m.bus.Publish(ctx, msg); err != nil {
|
if err := m.bus.Publish(env); err != nil {
|
||||||
panic(rt.ToValue(errors.WithStack(err)))
|
panic(rt.ToValue(errors.WithStack(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) handleClientMessages() {
|
func (m *Module) handleIncomingMessages() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
ctx,
|
ctx,
|
||||||
"subscribing to bus messages",
|
"subscribing to bus envelopes",
|
||||||
)
|
)
|
||||||
|
|
||||||
clientMessages, err := m.bus.Subscribe(ctx, module.MessageNamespaceClient)
|
envelopes, err := m.bus.Subscribe(ctx, edgehttp.AddressIncomingMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.WithStack(err))
|
panic(errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
@ -92,16 +88,16 @@ func (m *Module) handleClientMessages() {
|
||||||
defer func() {
|
defer func() {
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
ctx,
|
ctx,
|
||||||
"unsubscribing from bus messages",
|
"unsubscribing from bus envelopes",
|
||||||
)
|
)
|
||||||
|
|
||||||
m.bus.Unsubscribe(ctx, module.MessageNamespaceClient, clientMessages)
|
m.bus.Unsubscribe(edgehttp.AddressIncomingMessage, envelopes)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
ctx,
|
ctx,
|
||||||
"waiting for next message",
|
"waiting for next envelope",
|
||||||
)
|
)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -112,13 +108,13 @@ func (m *Module) handleClientMessages() {
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
case msg := <-clientMessages:
|
case env := <-envelopes:
|
||||||
clientMessage, ok := msg.(*module.ClientMessage)
|
incomingMessage, ok := env.Message().(*edgehttp.IncomingMessage)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error(
|
logger.Warn(
|
||||||
ctx,
|
ctx,
|
||||||
"unexpected message type",
|
"unexpected message type",
|
||||||
logger.F("message", msg),
|
logger.F("message", env.Message()),
|
||||||
)
|
)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
@ -126,11 +122,11 @@ func (m *Module) handleClientMessages() {
|
||||||
|
|
||||||
logger.Debug(
|
logger.Debug(
|
||||||
ctx,
|
ctx,
|
||||||
"received client message",
|
"received incoming message",
|
||||||
logger.F("message", clientMessage),
|
logger.F("message", incomingMessage),
|
||||||
)
|
)
|
||||||
|
|
||||||
if _, err := m.server.ExecFuncByName(clientMessage.Context, "onClientMessage", clientMessage.Context, clientMessage.Data); err != nil {
|
if _, err := m.server.ExecFuncByName(incomingMessage.Context, "onClientMessage", incomingMessage.Context, incomingMessage.Payload); err != nil {
|
||||||
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
if errors.Is(err, app.ErrFuncDoesNotExist) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -152,7 +148,7 @@ func ModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
||||||
bus: bus,
|
bus: bus,
|
||||||
}
|
}
|
||||||
|
|
||||||
go module.handleClientMessages()
|
go module.handleIncomingMessages()
|
||||||
|
|
||||||
return module
|
return module
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,278 +0,0 @@
|
||||||
package module
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
|
||||||
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gitlab.com/wpetit/goweb/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RPCRequest struct {
|
|
||||||
Method string
|
|
||||||
Params interface{}
|
|
||||||
ID interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type RPCError struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RPCResponse struct {
|
|
||||||
Result interface{}
|
|
||||||
Error *RPCError
|
|
||||||
ID interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type RPCModule struct {
|
|
||||||
server *app.Server
|
|
||||||
bus bus.Bus
|
|
||||||
callbacks sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) Name() string {
|
|
||||||
return "rpc"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) Export(export *goja.Object) {
|
|
||||||
if err := export.Set("register", m.register); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'register' function"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := export.Set("unregister", m.unregister); err != nil {
|
|
||||||
panic(errors.Wrap(err, "could not set 'unregister' function"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) OnInit(ctx context.Context, rt *goja.Runtime) error {
|
|
||||||
go m.handleMessages(ctx)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
||||||
fnName := util.AssertString(call.Argument(0), rt)
|
|
||||||
|
|
||||||
var (
|
|
||||||
callable goja.Callable
|
|
||||||
ok bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(call.Arguments) > 1 {
|
|
||||||
callable, ok = goja.AssertFunction(call.Argument(1))
|
|
||||||
} else {
|
|
||||||
callable, ok = goja.AssertFunction(rt.Get(fnName))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
panic(rt.NewTypeError("method should be a valid function"))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
logger.Debug(ctx, "registering method", logger.F("method", fnName))
|
|
||||||
|
|
||||||
m.callbacks.Store(fnName, callable)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
|
||||||
fnName := util.AssertString(call.Argument(0), rt)
|
|
||||||
|
|
||||||
m.callbacks.Delete(fnName)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) handleMessages(ctx context.Context) {
|
|
||||||
clientMessages, err := m.bus.Subscribe(ctx, MessageNamespaceClient)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
m.bus.Unsubscribe(ctx, MessageNamespaceClient, clientMessages)
|
|
||||||
}()
|
|
||||||
|
|
||||||
sendRes := func(ctx context.Context, req *RPCRequest, result goja.Value) {
|
|
||||||
res := &RPCResponse{
|
|
||||||
ID: req.ID,
|
|
||||||
Result: result.Export(),
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(ctx, "sending rpc response", logger.F("response", res))
|
|
||||||
|
|
||||||
if err := m.sendResponse(ctx, res); err != nil {
|
|
||||||
logger.Error(
|
|
||||||
ctx, "could not send response",
|
|
||||||
logger.CapturedE(errors.WithStack(err)),
|
|
||||||
logger.F("response", res),
|
|
||||||
logger.F("request", req),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for msg := range clientMessages {
|
|
||||||
go m.handleMessage(ctx, msg, sendRes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) handleMessage(ctx context.Context, msg bus.Message, sendRes func(ctx context.Context, req *RPCRequest, result goja.Value)) {
|
|
||||||
clientMessage, ok := msg.(*ClientMessage)
|
|
||||||
if !ok {
|
|
||||||
logger.Warn(ctx, "unexpected bus message", logger.F("message", msg))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, req := m.isRPCRequest(clientMessage)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug(ctx, "received rpc request", logger.F("request", req))
|
|
||||||
|
|
||||||
rawCallable, exists := m.callbacks.Load(req.Method)
|
|
||||||
if !exists {
|
|
||||||
logger.Debug(ctx, "method not found", logger.F("req", req))
|
|
||||||
|
|
||||||
if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil {
|
|
||||||
logger.Error(
|
|
||||||
ctx, "could not send method not found response",
|
|
||||||
logger.CapturedE(errors.WithStack(err)),
|
|
||||||
logger.F("request", req),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callable, ok := rawCallable.(goja.Callable)
|
|
||||||
if !ok {
|
|
||||||
logger.Debug(ctx, "invalid method", logger.F("req", req))
|
|
||||||
|
|
||||||
if err := m.sendMethodNotFoundResponse(clientMessage.Context, req); err != nil {
|
|
||||||
logger.Error(
|
|
||||||
ctx, "could not send method not found response",
|
|
||||||
logger.CapturedE(errors.WithStack(err)),
|
|
||||||
logger.F("request", req),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := m.server.Exec(clientMessage.Context, callable, clientMessage.Context, req.Params)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(
|
|
||||||
ctx, "rpc call error",
|
|
||||||
logger.CapturedE(errors.WithStack(err)),
|
|
||||||
logger.F("request", req),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := m.sendErrorResponse(clientMessage.Context, req, err); err != nil {
|
|
||||||
logger.Error(
|
|
||||||
ctx, "could not send error response",
|
|
||||||
logger.CapturedE(errors.WithStack(err)),
|
|
||||||
logger.F("originalError", err),
|
|
||||||
logger.F("request", req),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
promise, ok := app.IsPromise(result)
|
|
||||||
if ok {
|
|
||||||
go func(ctx context.Context, req *RPCRequest, promise *goja.Promise) {
|
|
||||||
result := m.server.WaitForPromise(promise)
|
|
||||||
sendRes(ctx, req, result)
|
|
||||||
}(clientMessage.Context, req, promise)
|
|
||||||
} else {
|
|
||||||
sendRes(clientMessage.Context, req, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) sendErrorResponse(ctx context.Context, req *RPCRequest, err error) error {
|
|
||||||
return m.sendResponse(ctx, &RPCResponse{
|
|
||||||
ID: req.ID,
|
|
||||||
Result: nil,
|
|
||||||
Error: &RPCError{
|
|
||||||
Code: -32603,
|
|
||||||
Message: err.Error(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) sendMethodNotFoundResponse(ctx context.Context, req *RPCRequest) error {
|
|
||||||
return m.sendResponse(ctx, &RPCResponse{
|
|
||||||
ID: req.ID,
|
|
||||||
Result: nil,
|
|
||||||
Error: &RPCError{
|
|
||||||
Code: -32601,
|
|
||||||
Message: fmt.Sprintf("method not found"),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) sendResponse(ctx context.Context, res *RPCResponse) error {
|
|
||||||
msg := NewServerMessage(ctx, map[string]interface{}{
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": res.ID,
|
|
||||||
"error": res.Error,
|
|
||||||
"result": res.Result,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := m.bus.Publish(ctx, msg); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RPCModule) isRPCRequest(msg *ClientMessage) (bool, *RPCRequest) {
|
|
||||||
jsonRPC, exists := msg.Data["jsonrpc"]
|
|
||||||
if !exists || jsonRPC != "2.0" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rawMethod, exists := msg.Data["method"]
|
|
||||||
if !exists {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
method, ok := rawMethod.(string)
|
|
||||||
if !ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
id := msg.Data["id"]
|
|
||||||
params := msg.Data["params"]
|
|
||||||
|
|
||||||
return true, &RPCRequest{
|
|
||||||
ID: id,
|
|
||||||
Method: method,
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func RPCModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
|
||||||
return func(server *app.Server) app.ServerModule {
|
|
||||||
mod := &RPCModule{
|
|
||||||
server: server,
|
|
||||||
bus: bus,
|
|
||||||
}
|
|
||||||
|
|
||||||
return mod
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ app.InitializableModule = &RPCModule{}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Address bus.Address = "module/rpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Context context.Context
|
||||||
|
Method string
|
||||||
|
Params any
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequestEnvelope(ctx context.Context, method string, params any) bus.Envelope {
|
||||||
|
return bus.NewEnvelope(Address, &Request{ctx, method, params})
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrMethodNotFound = errors.New("method not found")
|
||||||
|
)
|
|
@ -0,0 +1,19 @@
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type JSONRPCRequest struct {
|
||||||
|
ID any
|
||||||
|
Method string
|
||||||
|
Params any
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONRPCError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *JSONRPCError) Error() string {
|
||||||
|
return fmt.Sprintf("json-rpc error: %d - %s", e.Code, e.Message)
|
||||||
|
}
|
|
@ -0,0 +1,260 @@
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
edgehttp "forge.cadoles.com/arcad/edge/pkg/http"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module/util"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
server *app.Server
|
||||||
|
bus bus.Bus
|
||||||
|
callbacks sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Name() string {
|
||||||
|
return "rpc"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) Export(export *goja.Object) {
|
||||||
|
if err := export.Set("register", m.register); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'register' function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := export.Set("unregister", m.unregister); err != nil {
|
||||||
|
panic(errors.Wrap(err, "could not set 'unregister' function"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) OnInit(ctx context.Context, rt *goja.Runtime) error {
|
||||||
|
requestErrs := m.bus.Reply(ctx, Address, m.handleRequest)
|
||||||
|
go func() {
|
||||||
|
for err := range requestErrs {
|
||||||
|
logger.Error(ctx, "error while replying to rpc requests", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
httpIncomingMessages, err := m.bus.Subscribe(ctx, edgehttp.AddressIncomingMessage)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go m.handleIncomingHTTPMessages(ctx, httpIncomingMessages)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) register(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
fnName := util.AssertString(call.Argument(0), rt)
|
||||||
|
|
||||||
|
var (
|
||||||
|
callable goja.Callable
|
||||||
|
ok bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(call.Arguments) > 1 {
|
||||||
|
callable, ok = goja.AssertFunction(call.Argument(1))
|
||||||
|
} else {
|
||||||
|
callable, ok = goja.AssertFunction(rt.Get(fnName))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
panic(rt.NewTypeError("method should be a valid function"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
logger.Debug(ctx, "registering method", logger.F("method", fnName))
|
||||||
|
|
||||||
|
m.callbacks.Store(fnName, callable)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) unregister(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
|
||||||
|
fnName := util.AssertString(call.Argument(0), rt)
|
||||||
|
|
||||||
|
m.callbacks.Delete(fnName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) handleRequest(env bus.Envelope) (any, error) {
|
||||||
|
request, ok := env.Message().(*Request)
|
||||||
|
if !ok {
|
||||||
|
logger.Warn(context.Background(), "unexpected bus message", logger.F("message", env.Message()))
|
||||||
|
|
||||||
|
return nil, errors.WithStack(bus.ErrUnexpectedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := logger.With(request.Context, logger.F("request", request))
|
||||||
|
|
||||||
|
logger.Debug(ctx, "received rpc request")
|
||||||
|
|
||||||
|
rawCallable, exists := m.callbacks.Load(request.Method)
|
||||||
|
if !exists {
|
||||||
|
logger.Debug(ctx, "method not found")
|
||||||
|
|
||||||
|
return nil, errors.WithStack(ErrMethodNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
callable, ok := rawCallable.(goja.Callable)
|
||||||
|
if !ok {
|
||||||
|
logger.Debug(ctx, "invalid method")
|
||||||
|
|
||||||
|
return nil, errors.WithStack(ErrMethodNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := m.server.Exec(ctx, callable, request.Context, request.Params)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "rpc call error",
|
||||||
|
logger.CapturedE(errors.WithStack(err)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) handleIncomingHTTPMessages(ctx context.Context, incoming <-chan bus.Envelope) {
|
||||||
|
defer func() {
|
||||||
|
m.bus.Unsubscribe(edgehttp.AddressIncomingMessage, incoming)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for env := range incoming {
|
||||||
|
msg, ok := env.Message().(*edgehttp.IncomingMessage)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(ctx, "unexpected incoming http message type", logger.F("message", env.Message()))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonReq, ok := m.isRPCRequest(msg.Payload)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID, ok := edgehttp.ContextSessionID(msg.Context)
|
||||||
|
if !ok {
|
||||||
|
logger.Error(ctx, "could not find session id in context")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
request := NewRequestEnvelope(msg.Context, jsonReq.Method, jsonReq.Params)
|
||||||
|
|
||||||
|
requestCtx := logger.With(msg.Context, logger.F("rpcRequestMethod", jsonReq.Method), logger.F("rpcRequestID", jsonReq.ID))
|
||||||
|
|
||||||
|
reply, err := m.bus.Request(requestCtx, request)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not execute rpc request",
|
||||||
|
logger.CapturedE(err),
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors.Is(err, ErrMethodNotFound) {
|
||||||
|
if err := m.sendMethodNotFoundResponse(sessionID, jsonReq.ID); err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not send json rpc error response",
|
||||||
|
logger.CapturedE(errors.WithStack(err)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.sendErrorResponse(sessionID, jsonReq.ID, err); err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not send json rpc error response",
|
||||||
|
logger.CapturedE(errors.WithStack(err)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.sendResponse(sessionID, jsonReq.ID, reply.Message(), nil); err != nil {
|
||||||
|
logger.Error(
|
||||||
|
ctx, "could not send json rpc result response",
|
||||||
|
logger.CapturedE(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) sendErrorResponse(sessionID string, requestID any, err error) error {
|
||||||
|
return m.sendResponse(sessionID, requestID, nil, &JSONRPCError{
|
||||||
|
Code: -32603,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) sendMethodNotFoundResponse(sessionID string, requestID any) error {
|
||||||
|
return m.sendResponse(sessionID, requestID, nil, &JSONRPCError{
|
||||||
|
Code: -32601,
|
||||||
|
Message: "method not found",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) sendResponse(sessionID string, requestID any, result any, err error) error {
|
||||||
|
env := edgehttp.NewOutgoingMessageEnvelope(sessionID, map[string]interface{}{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": requestID,
|
||||||
|
"error": err,
|
||||||
|
"result": result,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := m.bus.Publish(env); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) isRPCRequest(payload map[string]any) (*JSONRPCRequest, bool) {
|
||||||
|
jsonRPC, exists := payload["jsonrpc"]
|
||||||
|
if !exists || jsonRPC != "2.0" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
rawMethod, exists := payload["method"]
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
method, ok := rawMethod.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
id := payload["id"]
|
||||||
|
params := payload["params"]
|
||||||
|
|
||||||
|
return &JSONRPCRequest{
|
||||||
|
ID: id,
|
||||||
|
Method: method,
|
||||||
|
Params: params,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModuleFactory(bus bus.Bus) app.ServerModuleFactory {
|
||||||
|
return func(server *app.Server) app.ServerModule {
|
||||||
|
mod := &Module{
|
||||||
|
server: server,
|
||||||
|
bus: bus,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ app.InitializableModule = &Module{}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerExecDeadlock(t *testing.T) {
|
||||||
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := memory.NewBus(memory.WithBufferSize(1))
|
||||||
|
|
||||||
|
server := app.NewServer(
|
||||||
|
module.ConsoleModuleFactory(),
|
||||||
|
ModuleFactory(b),
|
||||||
|
module.LifecycleModuleFactory(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data, err := os.ReadFile("testdata/deadlock.js")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Log("starting server")
|
||||||
|
|
||||||
|
if err := server.Start(ctx, "deadlock.js", string(data)); err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer server.Stop()
|
||||||
|
|
||||||
|
t.Log("server started")
|
||||||
|
|
||||||
|
count := 100
|
||||||
|
delay := 100
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
wg.Add(count)
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
t.Logf("calling %d", i)
|
||||||
|
|
||||||
|
isCanceled := i%2 == 0
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
if isCanceled {
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
ctx = canceledCtx
|
||||||
|
} else {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
env := NewRequestEnvelope(ctx, "doSomethingLong", map[string]any{
|
||||||
|
"i": i,
|
||||||
|
"delay": delay,
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Logf("publishing envelope #%d", i)
|
||||||
|
|
||||||
|
reply, err := b.Request(ctx, env)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) && isCanceled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, bus.ErrNoResponse) && isCanceled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Errorf("%+v", errors.WithStack(err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := reply.Message().(int64)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("response.Result: expected type '%T', got '%T'", int64(0), reply.Message())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, g := i, int(result); e != g {
|
||||||
|
t.Errorf("response.Result: expected '%v', got '%v'", e, g)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
function onInit() {
|
||||||
|
rpc.register("doSomethingLong", doSomethingLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSomethingLong(ctx, params) {
|
||||||
|
var start = Date.now()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
var now = Date.now()
|
||||||
|
if (now - start >= params.delay) break
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.i;
|
||||||
|
}
|
|
@ -33,18 +33,14 @@ func TestModule(t *testing.T) {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load("testdata/share.js", string(data)); err != nil {
|
ctx := context.Background()
|
||||||
|
if err := server.Start(ctx, "testdata/share.js", string(data)); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
defer server.Stop()
|
||||||
if err := server.Start(ctx); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := server.ExecFuncByName(context.Background(), "testModule"); err != nil {
|
if _, err := server.ExecFuncByName(context.Background(), "testModule"); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Stop()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,18 +27,14 @@ func TestStoreModule(t *testing.T) {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Load("testdata/store.js", string(data)); err != nil {
|
ctx := context.Background()
|
||||||
|
if err := server.Start(ctx, "testdata/store.js", string(data)); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
defer server.Stop()
|
||||||
if err := server.Start(ctx); err != nil {
|
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := server.ExecFuncByName(context.Background(), "testStore"); err != nil {
|
if _, err := server.ExecFuncByName(context.Background(), "testStore"); err != nil {
|
||||||
t.Fatalf("%+v", errors.WithStack(err))
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
server.Stop()
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5,5 +5,6 @@ import LoginIcon from './login.svg';
|
||||||
import HomeIcon from './home.svg';
|
import HomeIcon from './home.svg';
|
||||||
import LinkIcon from './link.svg';
|
import LinkIcon from './link.svg';
|
||||||
import LogoutIcon from './logout.svg';
|
import LogoutIcon from './logout.svg';
|
||||||
|
import LoaderIcon from './loader.svg';
|
||||||
|
|
||||||
export { UserCircleIcon, MenuIcon, CloudIcon, LoginIcon, HomeIcon, LinkIcon, LogoutIcon }
|
export { LoaderIcon, UserCircleIcon, MenuIcon, CloudIcon, LoginIcon, HomeIcon, LinkIcon, LogoutIcon }
|
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <path fill="none" d="M0 0h24v24H0z"></path> <path d="M12 2a1 1 0 0 1 1 1v3a1 1 0 0 1-2 0V3a1 1 0 0 1 1-1zm0 15a1 1 0 0 1 1 1v3a1 1 0 0 1-2 0v-3a1 1 0 0 1 1-1zm8.66-10a1 1 0 0 1-.366 1.366l-2.598 1.5a1 1 0 1 1-1-1.732l2.598-1.5A1 1 0 0 1 20.66 7zM7.67 14.5a1 1 0 0 1-.366 1.366l-2.598 1.5a1 1 0 1 1-1-1.732l2.598-1.5a1 1 0 0 1 1.366.366zM20.66 17a1 1 0 0 1-1.366.366l-2.598-1.5a1 1 0 0 1 1-1.732l2.598 1.5A1 1 0 0 1 20.66 17zM7.67 9.5a1 1 0 0 1-1.366.366l-2.598-1.5a1 1 0 1 1 1-1.732l2.598 1.5A1 1 0 0 1 7.67 9.5z"></path> </g> </g></svg>
|
After Width: | Height: | Size: 773 B |
|
@ -0,0 +1,57 @@
|
||||||
|
import { LitElement, html, css } from 'lit';
|
||||||
|
import { LoaderIcon } from './icons';
|
||||||
|
|
||||||
|
export class Loader extends LitElement {
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid rgb(229,231,235);
|
||||||
|
border-top: 10px solid transparent;
|
||||||
|
background-color: #fff;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 35px;
|
||||||
|
animation-duration: 3s;
|
||||||
|
animation-name: spin;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotateZ(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotateZ(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
<img class="icon" src="${LoaderIcon}" />
|
||||||
|
Chargement en cours
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,9 @@ export class Menu extends LitElement {
|
||||||
@property()
|
@property()
|
||||||
_profile: Profile
|
_profile: Profile
|
||||||
|
|
||||||
|
@property()
|
||||||
|
_loading: boolean = false
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -95,6 +98,7 @@ export class Menu extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetchApps() {
|
_fetchApps() {
|
||||||
|
this._loading = true;
|
||||||
return fetch(`${BASE_API_URL}/apps`)
|
return fetch(`${BASE_API_URL}/apps`)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(result => {
|
.then(result => {
|
||||||
|
@ -130,9 +134,14 @@ export class Menu extends LitElement {
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
})
|
})
|
||||||
.then((manifests: Manifest[]) => {
|
.then((manifests: Manifest[]) => {
|
||||||
|
this._loading = false
|
||||||
this._apps = manifests;
|
this._apps = manifests;
|
||||||
})
|
})
|
||||||
.catch(err => console.error(err))
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
this._loading = false;
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetchProfile() {
|
_fetchProfile() {
|
||||||
|
@ -158,19 +167,24 @@ export class Menu extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderApps() {
|
_renderApps() {
|
||||||
const apps = this._apps
|
let apps;
|
||||||
.filter(manifest => this._canAccess(manifest))
|
if (this._loading) {
|
||||||
.map(manifest => {
|
apps = [ html`<edge-loader></edge-loader>` ]
|
||||||
const iconUrl = ( ( manifest.url || '') + ( manifest.metadata?.paths?.icon || '' ) ) || LinkIcon;
|
} else {
|
||||||
return html`
|
apps = this._apps
|
||||||
<edge-menu-sub-item
|
.filter(manifest => this._canAccess(manifest))
|
||||||
name='${ manifest.id }'
|
.map(manifest => {
|
||||||
label='${ manifest.title }'
|
const iconUrl = ( ( manifest.url || '') + ( manifest.metadata?.paths?.icon || '' ) ) || LinkIcon;
|
||||||
icon-url='${ iconUrl }'
|
return html`
|
||||||
link-url='${ manifest.url || '#' }'>
|
<edge-menu-sub-item
|
||||||
</edge-menu-sub-item>
|
name='${ manifest.id }'
|
||||||
`
|
label='${ manifest.title }'
|
||||||
});
|
icon-url='${ iconUrl }'
|
||||||
|
link-url='${ manifest.url || '#' }'>
|
||||||
|
</edge-menu-sub-item>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<edge-menu-item name='apps' label='Apps' icon-url='${CloudIcon}'>
|
<edge-menu-item name='apps' label='Apps' icon-url='${CloudIcon}'>
|
||||||
|
|
|
@ -6,10 +6,12 @@ import { MenuItem as MenuItemElement } from './components/menu-item.js';
|
||||||
import { MenuSubItem as MenuSubItemElement } from './components/menu-sub-item.js';
|
import { MenuSubItem as MenuSubItemElement } from './components/menu-sub-item.js';
|
||||||
import { CrossFrameMessenger } from './crossframe-messenger.js';
|
import { CrossFrameMessenger } from './crossframe-messenger.js';
|
||||||
import { MenuManager } from './menu-manager.js';
|
import { MenuManager } from './menu-manager.js';
|
||||||
|
import { Loader } from './components/loader';
|
||||||
|
|
||||||
customElements.define('edge-menu', MenuElement);
|
customElements.define('edge-menu', MenuElement);
|
||||||
customElements.define('edge-menu-item', MenuItemElement);
|
customElements.define('edge-menu-item', MenuItemElement);
|
||||||
customElements.define('edge-menu-sub-item', MenuSubItemElement);
|
customElements.define('edge-menu-sub-item', MenuSubItemElement);
|
||||||
|
customElements.define('edge-loader', Loader);
|
||||||
|
|
||||||
export const Client = new EdgeClient();
|
export const Client = new EdgeClient();
|
||||||
export const Frame = new CrossFrameMessenger();
|
export const Frame = new CrossFrameMessenger();
|
||||||
|
|
|
@ -9,12 +9,16 @@ import (
|
||||||
"github.com/oklog/ulid/v2"
|
"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
|
type DocumentID string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DocumentAttrID = "_id"
|
DocumentAttrID = "_id"
|
||||||
|
DocumentAttrRevision = "_revision"
|
||||||
DocumentAttrCreatedAt = "_createdAt"
|
DocumentAttrCreatedAt = "_createdAt"
|
||||||
DocumentAttrUpdatedAt = "_updatedAt"
|
DocumentAttrUpdatedAt = "_updatedAt"
|
||||||
)
|
)
|
||||||
|
@ -44,6 +48,20 @@ func (d Document) ID() (DocumentID, bool) {
|
||||||
return "", false
|
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) {
|
func (d Document) CreatedAt() (time.Time, bool) {
|
||||||
return d.timeAttr(DocumentAttrCreatedAt)
|
return d.timeAttr(DocumentAttrCreatedAt)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ func NewBlobStore(dsn string) (storage.BlobStore, error) {
|
||||||
|
|
||||||
factory, exists := blobStoreFactories[url.Scheme]
|
factory, exists := blobStoreFactories[url.Scheme]
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, errors.WithStack(ErrSchemeNotRegistered)
|
return nil, errors.Wrapf(ErrSchemeNotRegistered, "no driver associated with scheme '%s'", url.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
store, err := factory(url)
|
store, err := factory(url)
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"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
|
||||||
|
blobCache *lfu.Cache[string, []byte]
|
||||||
|
bucketCache *lfu.Cache[string, storage.BlobBucket]
|
||||||
|
blobInfoCache *lfu.Cache[string, storage.BlobInfo]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements storage.BlobBucket.
|
||||||
|
func (b *BlobBucket) Close() error {
|
||||||
|
// Close only when bucket is evicted from cache
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete implements storage.BlobBucket.
|
||||||
|
func (b *BlobBucket) Delete(ctx context.Context, id storage.BlobID) error {
|
||||||
|
defer b.clearCache(ctx, id)
|
||||||
|
|
||||||
|
if err := b.bucket.Delete(ctx, id); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements storage.BlobBucket.
|
||||||
|
func (b *BlobBucket) Get(ctx context.Context, id storage.BlobID) (storage.BlobInfo, error) {
|
||||||
|
key := b.getCacheKey(id)
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
return blobInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := b.bucket.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrBucketClosed) {
|
||||||
|
b.clearCache(ctx, id)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// List implements storage.BlobBucket.
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ifo := range infos {
|
||||||
|
key := b.getCacheKey(ifo.ID())
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements storage.BlobBucket.
|
||||||
|
func (b *BlobBucket) Name() string {
|
||||||
|
return b.bucket.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader implements storage.BlobBucket.
|
||||||
|
func (b *BlobBucket) NewReader(ctx context.Context, id storage.BlobID) (io.ReadSeekCloser, error) {
|
||||||
|
if cached, exist := b.inContentCache(id); exist {
|
||||||
|
logger.Debug(
|
||||||
|
ctx, "found blob content in cache",
|
||||||
|
logger.F("cacheKey", b.getCacheKey(id)),
|
||||||
|
)
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := b.bucket.NewReader(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrBucketClosed) {
|
||||||
|
b.clearCache(ctx, id)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &readCacher{
|
||||||
|
reader: reader,
|
||||||
|
cache: b.blobCache,
|
||||||
|
key: b.getCacheKey(id),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BlobBucket) getCacheKey(id storage.BlobID) string {
|
||||||
|
return fmt.Sprintf("%s-%s", b.Name(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BlobBucket) inContentCache(id storage.BlobID) (io.ReadSeekCloser, bool) {
|
||||||
|
key := b.getCacheKey(id)
|
||||||
|
data, err := b.blobCache.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, lfu.ErrNotFound) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error(context.Background(), "could not retrieve cached value", logger.CapturedE(errors.WithStack(err)))
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cachedReader{data, 0}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BlobBucket) clearCache(ctx context.Context, id storage.BlobID) {
|
||||||
|
key := b.getCacheKey(id)
|
||||||
|
|
||||||
|
logger.Debug(ctx, "clearing cache", logger.F("cacheKey", key))
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
func (b *BlobBucket) NewWriter(ctx context.Context, id storage.BlobID) (io.WriteCloser, error) {
|
||||||
|
defer b.clearCache(ctx, id)
|
||||||
|
|
||||||
|
writer, err := b.bucket.NewWriter(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrBucketClosed) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size implements storage.BlobBucket.
|
||||||
|
func (b *BlobBucket) Size(ctx context.Context) (int64, error) {
|
||||||
|
size, err := b.bucket.Size(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrBucketClosed) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.BlobBucket = &BlobBucket{}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"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
|
||||||
|
blobCache *lfu.Cache[string, []byte]
|
||||||
|
bucketCache *lfu.Cache[string, storage.BlobBucket]
|
||||||
|
blobInfoCache *lfu.Cache[string, storage.BlobInfo]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBucket implements storage.BlobStore.
|
||||||
|
func (s *BlobStore) DeleteBucket(ctx context.Context, name string) error {
|
||||||
|
if err := s.store.DeleteBucket(ctx, name); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.bucketCache.Delete(name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListBuckets implements storage.BlobStore.
|
||||||
|
func (s *BlobStore) ListBuckets(ctx context.Context) ([]string, error) {
|
||||||
|
buckets, err := s.store.ListBuckets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buckets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenBucket implements storage.BlobStore.
|
||||||
|
func (s *BlobStore) OpenBucket(ctx context.Context, name string) (storage.BlobBucket, error) {
|
||||||
|
bucket, err := s.bucketCache.Get(name)
|
||||||
|
if err == nil {
|
||||||
|
logger.Debug(ctx, "found bucket in cache", logger.F("name", name))
|
||||||
|
|
||||||
|
return &BlobBucket{
|
||||||
|
bucket: bucket,
|
||||||
|
blobCache: s.blobCache,
|
||||||
|
blobInfoCache: s.blobInfoCache,
|
||||||
|
bucketCache: s.bucketCache,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
blobCache: s.blobCache,
|
||||||
|
blobInfoCache: s.blobInfoCache,
|
||||||
|
bucketCache: s.bucketCache,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBlobStore(store storage.BlobStore, funcs ...OptionFunc) (*BlobStore, error) {
|
||||||
|
options := NewOptions(funcs...)
|
||||||
|
|
||||||
|
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
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
blobCache: blobCache,
|
||||||
|
bucketCache: blobBucketCache,
|
||||||
|
blobInfoCache: blobInfoCache,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.BlobStore = &BlobStore{}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver/sqlite"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/testsuite"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBlobStore(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if testing.Verbose() {
|
||||||
|
logger.SetLevel(logger.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
|
file := "./testdata/blobstore_test.sqlite"
|
||||||
|
|
||||||
|
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||||
|
|
||||||
|
backend := sqlite.NewBlobStore(dsn)
|
||||||
|
|
||||||
|
store, err := NewBlobStore(backend)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
testsuite.TestBlobStore(context.Background(), t, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBlobStore(t *testing.B) {
|
||||||
|
logger.SetLevel(logger.LevelError)
|
||||||
|
|
||||||
|
file := "./testdata/blobstore_test.sqlite"
|
||||||
|
|
||||||
|
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
|
||||||
|
backend := sqlite.NewBlobStore(dsn)
|
||||||
|
|
||||||
|
store, err := NewBlobStore(backend)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%+v", errors.WithStack(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
testsuite.BenchmarkBlobStore(t, store)
|
||||||
|
}
|
|
@ -0,0 +1,264 @@
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage"
|
||||||
|
"forge.cadoles.com/arcad/edge/pkg/storage/driver"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
driver.RegisterBlobStoreFactory("cache", blobStoreFactory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func blobStoreFactory(dsn *url.URL) (storage.BlobStore, error) {
|
||||||
|
query := dsn.Query()
|
||||||
|
|
||||||
|
rawDriver := query.Get("driver")
|
||||||
|
if rawDriver == "" {
|
||||||
|
return nil, errors.New("missing required url parameter 'driver'")
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Del("driver")
|
||||||
|
|
||||||
|
blobStoreOptionFuncs := make([]OptionFunc, 0)
|
||||||
|
|
||||||
|
cacheTTL, err := parseDuration(&query, "cacheTTL")
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, errNotFound) {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheTTL = time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
blobStoreOptionFuncs = append(blobStoreOptionFuncs, WithCacheTTL(cacheTTL))
|
||||||
|
|
||||||
|
blobBucketCacheSize, err := parseInt(&query, "blobBucketCacheSize")
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, errNotFound) {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blobBucketCacheSize = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if !errors.Is(err, errNotFound) {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bloInfoCacheSize = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
Path: dsn.Path,
|
||||||
|
RawQuery: query.Encode(),
|
||||||
|
}
|
||||||
|
|
||||||
|
backend, err := driver.NewBlobStore(url.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := NewBlobStore(backend, blobStoreOptionFuncs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return store, 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 != "" {
|
||||||
|
query.Del(name)
|
||||||
|
|
||||||
|
value, err := strconv.ParseInt(rawValue, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrapf(err, "could not parse url parameter '%s'", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, errors.WithStack(errNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDuration(query *url.Values, name string) (time.Duration, error) {
|
||||||
|
rawValue := query.Get(name)
|
||||||
|
if rawValue != "" {
|
||||||
|
query.Del(name)
|
||||||
|
|
||||||
|
value, err := time.ParseDuration(rawValue)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errors.Wrapf(err, "could not parse url parameter '%s'", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue