Merge branch 'dev' into cluster

This commit is contained in:
Ingo Oppermann
2023-04-12 15:27:38 +02:00
995 changed files with 130216 additions and 13987 deletions

View File

@@ -62,7 +62,7 @@ jobs:
build-args: | build-args: |
CORE_IMAGE=datarhei/base:${{ env.OS_NAME }}-core-${{ env.OS_VERSION }}-${{ env.CORE_VERSION }} CORE_IMAGE=datarhei/base:${{ env.OS_NAME }}-core-${{ env.OS_VERSION }}-${{ env.CORE_VERSION }}
FFMPEG_IMAGE=datarhei/base:${{ env.OS_NAME }}-ffmpeg-rpi-${{ env.OS_VERSION }}-${{ env.FFMPEG_VERSION }} FFMPEG_IMAGE=datarhei/base:${{ env.OS_NAME }}-ffmpeg-rpi-${{ env.OS_VERSION }}-${{ env.FFMPEG_VERSION }}
platforms: linux/arm/v7,linux/arm/v6,linux/arm64 platforms: linux/arm/v7,linux/arm64
push: true push: true
tags: | tags: |
datarhei/core:rpi-${{ env.CORE_VERSION }} datarhei/core:rpi-${{ env.CORE_VERSION }}

View File

@@ -11,7 +11,7 @@ jobs:
fetch-depth: 2 fetch-depth: 2
- uses: actions/setup-go@v2 - uses: actions/setup-go@v2
with: with:
go-version: '1.18' go-version: "1.19"
- name: Run coverage - name: Run coverage
run: go test -coverprofile=coverage.out -covermode=atomic -v ./... run: go test -coverprofile=coverage.out -covermode=atomic -v ./...
- name: Upload coverage to Codecov - name: Upload coverage to Codecov

View File

@@ -1,5 +1,5 @@
# CORE ALPINE BASE IMAGE # CORE ALPINE BASE IMAGE
OS_NAME=alpine OS_NAME=alpine
OS_VERSION=3.15 OS_VERSION=3.16
GOLANG_IMAGE=golang:1.18.6-alpine3.15 GOLANG_IMAGE=golang:1.20-alpine3.16
CORE_VERSION=16.10.1 CORE_VERSION=16.12.0

View File

@@ -1,3 +1,3 @@
# CORE NVIDIA CUDA BUNDLE # CORE NVIDIA CUDA BUNDLE
FFMPEG_VERSION=4.4.2 FFMPEG_VERSION=5.1.2
CUDA_VERSION=11.4.2 CUDA_VERSION=11.7.1

View File

@@ -1,2 +1,2 @@
# CORE BUNDLE # CORE BUNDLE
FFMPEG_VERSION=4.4.2 FFMPEG_VERSION=5.1.2

View File

@@ -1,2 +1,2 @@
# CORE RASPBERRY-PI BUNDLE # CORE RASPBERRY-PI BUNDLE
FFMPEG_VERSION=4.4.2 FFMPEG_VERSION=5.1.2

View File

@@ -1,2 +1,2 @@
# CORE BUNDLE # CORE BUNDLE
FFMPEG_VERSION=4.4.2 FFMPEG_VERSION=5.1.2

View File

@@ -1,5 +1,5 @@
# CORE UBUNTU BASE IMAGE # CORE UBUNTU BASE IMAGE
OS_NAME=ubuntu OS_NAME=ubuntu
OS_VERSION=20.04 OS_VERSION=20.04
GOLANG_IMAGE=golang:1.18.6-alpine3.15 GOLANG_IMAGE=golang:1.20-alpine3.16
CORE_VERSION=16.10.1 CORE_VERSION=16.12.0

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
.env .env
/core* /core*
/import* /import*
/ffmigrate*
/data/** /data/**
/test/** /test/**
.vscode .vscode

View File

@@ -1,5 +1,38 @@
# Core # Core
### Core v16.12.0 > v16.?.?
- Fix better naming for storage endpoint documentation
- Fix freeing up S3 mounts
- Fix URL validation if the path contains FFmpeg specific placeholders
- Fix purging default file from HTTP cache
### Core v16.11.0 > v16.12.0
- Add S3 storage support
- Add support for variables in placeholde parameter
- Add support for RTMP token as stream key as last element in path
- Add support for soft memory limit with debug.memory_limit_mbytes in config
- Add support for partial process config updates
- Add support for alternative syntax for auth0 tenants as environment variable
- Fix config timestamps created_at and loaded_at
- Fix /config/reload return type
- Fix modifying DTS in RTMP packets ([restreamer/#487](https://github.com/datarhei/restreamer/issues/487), [restreamer/#367](https://github.com/datarhei/restreamer/issues/367))
- Fix default internal SRT latency to 20ms
### Core v16.10.1 > v16.11.0
- Add FFmpeg 4.4 to FFmpeg 5.1 migration tool
- Add alternative SRT streamid
- Mod bump FFmpeg to v5.1.2 (datarhei/core:tag bundles)
- Fix crash with custom SSL certificates ([restreamer/#425](https://github.com/datarhei/restreamer/issues/425))
- Fix proper version handling for config
- Fix widged session data
- Fix resetting process stats when process stopped
- Fix stale FFmpeg process detection for streams with only audio
- Fix wrong return status code ([#6](https://github.com/datarhei/core/issues/6)))
- Fix use SRT defaults for key material exchange
### Core v16.10.0 > v16.10.1 ### Core v16.10.0 > v16.10.1
- Add email address in TLS config for Let's Encrypt - Add email address in TLS config for Let's Encrypt
@@ -20,11 +53,11 @@
- Fix process cleanup on delete, remove empty directories from disk - Fix process cleanup on delete, remove empty directories from disk
- Fix SRT blocking port on restart (upgrade datarhei/gosrt) - Fix SRT blocking port on restart (upgrade datarhei/gosrt)
- Fix RTMP communication (Blackmagic Web Presenter, thx 235 MEDIA) - Fix RTMP communication (Blackmagic Web Presenter, thx 235 MEDIA)
- Fix RTMP communication (Blackmagic ATEM Mini, datarhei/restreamer#385) - Fix RTMP communication (Blackmagic ATEM Mini, [#385](https://github.com/datarhei/restreamer/issues/385))
- Fix injecting commit, branch, and build info - Fix injecting commit, branch, and build info
- Fix API metadata endpoints responses - Fix API metadata endpoints responses
#### Core v16.9.0 > v16.9.1 #### Core v16.9.0 > v16.9.1^
- Fix v1 import app - Fix v1 import app
- Fix race condition - Fix race condition

View File

@@ -1,6 +1,6 @@
ARG GOLANG_IMAGE=golang:1.18.4-alpine3.15 ARG GOLANG_IMAGE=golang:1.20-alpine3.16
ARG BUILD_IMAGE=alpine:3.15 ARG BUILD_IMAGE=alpine:3.16
FROM $GOLANG_IMAGE as builder FROM $GOLANG_IMAGE as builder
@@ -12,12 +12,14 @@ RUN apk add \
cd /dist/core && \ cd /dist/core && \
go version && \ go version && \
make release_linux && \ make release_linux && \
make import_linux make import_linux && \
make ffmigrate_linux
FROM $BUILD_IMAGE FROM $BUILD_IMAGE
COPY --from=builder /dist/core/core /core/bin/core COPY --from=builder /dist/core/core /core/bin/core
COPY --from=builder /dist/core/import /core/bin/import COPY --from=builder /dist/core/import /core/bin/import
COPY --from=builder /dist/core/ffmigrate /core/bin/ffmigrate
COPY --from=builder /dist/core/mime.types /core/mime.types COPY --from=builder /dist/core/mime.types /core/mime.types
COPY --from=builder /dist/core/run.sh /core/bin/run.sh COPY --from=builder /dist/core/run.sh /core/bin/run.sh

View File

@@ -1,4 +1,4 @@
FROM golang:1.18.3-alpine3.15 FROM golang:1.20-alpine3.16
RUN apk add alpine-sdk RUN apk add alpine-sdk

View File

@@ -75,6 +75,14 @@ import:
import_linux: import_linux:
cd app/import && CGO_ENABLED=0 GOOS=linux GOARCH=${OSARCH} go build -o ../../import -ldflags="-s -w" cd app/import && CGO_ENABLED=0 GOOS=linux GOARCH=${OSARCH} go build -o ../../import -ldflags="-s -w"
## ffmigrate: Build ffmpeg migration binary
ffmigrate:
cd app/ffmigrate && CGO_ENABLED=${CGO_ENABLED} GOOS=${GOOS} GOARCH=${GOARCH} go build -o ../../ffmigrate -ldflags="-s -w"
# github workflow workaround
ffmigrate_linux:
cd app/ffmigrate && CGO_ENABLED=0 GOOS=linux GOARCH=${OSARCH} go build -o ../../ffmigrate -ldflags="-s -w"
## coverage: Generate code coverage analysis ## coverage: Generate code coverage analysis
coverage: coverage:
go test -race -coverprofile test/cover.out ./... go test -race -coverprofile test/cover.out ./...
@@ -96,7 +104,7 @@ release_linux:
docker: docker:
docker build -t core:$(SHORTCOMMIT) . docker build -t core:$(SHORTCOMMIT) .
.PHONY: help init build swagger test vet fmt vulncheck vendor commit coverage lint release import update .PHONY: help init build swagger test vet fmt vulncheck vendor commit coverage lint release import ffmigrate update
## help: Show all commands ## help: Show all commands
help: Makefile help: Makefile

1054
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
golog "log" golog "log"
"math"
gonet "net" gonet "net"
gohttp "net/http" gohttp "net/http"
"net/url" "net/url"
@@ -17,9 +18,12 @@ import (
"github.com/datarhei/core/v16/app" "github.com/datarhei/core/v16/app"
"github.com/datarhei/core/v16/cluster" "github.com/datarhei/core/v16/cluster"
"github.com/datarhei/core/v16/config" "github.com/datarhei/core/v16/config"
configstore "github.com/datarhei/core/v16/config/store"
configvars "github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/ffmpeg" "github.com/datarhei/core/v16/ffmpeg"
"github.com/datarhei/core/v16/http" "github.com/datarhei/core/v16/http"
"github.com/datarhei/core/v16/http/cache" "github.com/datarhei/core/v16/http/cache"
httpfs "github.com/datarhei/core/v16/http/fs"
"github.com/datarhei/core/v16/http/jwt" "github.com/datarhei/core/v16/http/jwt"
"github.com/datarhei/core/v16/http/router" "github.com/datarhei/core/v16/http/router"
"github.com/datarhei/core/v16/io/fs" "github.com/datarhei/core/v16/io/fs"
@@ -29,8 +33,9 @@ import (
"github.com/datarhei/core/v16/net" "github.com/datarhei/core/v16/net"
"github.com/datarhei/core/v16/prometheus" "github.com/datarhei/core/v16/prometheus"
"github.com/datarhei/core/v16/restream" "github.com/datarhei/core/v16/restream"
restreamapp "github.com/datarhei/core/v16/restream/app"
"github.com/datarhei/core/v16/restream/replace" "github.com/datarhei/core/v16/restream/replace"
"github.com/datarhei/core/v16/restream/store" restreamstore "github.com/datarhei/core/v16/restream/store"
"github.com/datarhei/core/v16/rtmp" "github.com/datarhei/core/v16/rtmp"
"github.com/datarhei/core/v16/service" "github.com/datarhei/core/v16/service"
"github.com/datarhei/core/v16/session" "github.com/datarhei/core/v16/session"
@@ -38,6 +43,7 @@ import (
"github.com/datarhei/core/v16/update" "github.com/datarhei/core/v16/update"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"go.uber.org/zap"
) )
// The API interface is the implementation for the restreamer API. // The API interface is the implementation for the restreamer API.
@@ -65,6 +71,7 @@ type api struct {
ffmpeg ffmpeg.FFmpeg ffmpeg ffmpeg.FFmpeg
diskfs fs.Filesystem diskfs fs.Filesystem
memfs fs.Filesystem memfs fs.Filesystem
s3fs map[string]fs.Filesystem
rtmpserver rtmp.Server rtmpserver rtmp.Server
srtserver srt.Server srtserver srt.Server
metrics monitor.HistoryMonitor metrics monitor.HistoryMonitor
@@ -99,7 +106,7 @@ type api struct {
config struct { config struct {
path string path string
store config.Store store configstore.Store
config *config.Config config *config.Config
} }
@@ -116,6 +123,7 @@ var ErrConfigReload = fmt.Errorf("configuration reload")
func New(configpath string, logwriter io.Writer) (API, error) { func New(configpath string, logwriter io.Writer) (API, error) {
a := &api{ a := &api{
state: "idle", state: "idle",
s3fs: map[string]fs.Filesystem{},
} }
a.config.path = configpath a.config.path = configpath
@@ -148,7 +156,8 @@ func (a *api) Reload() error {
logger := log.New("Core").WithOutput(log.NewConsoleWriter(a.log.writer, log.Lwarn, true)) logger := log.New("Core").WithOutput(log.NewConsoleWriter(a.log.writer, log.Lwarn, true))
store, err := config.NewJSONStore(a.config.path, func() { rootfs, _ := fs.NewDiskFilesystem(fs.DiskConfig{})
store, err := configstore.NewJSON(rootfs, a.config.path, func() {
a.errorChan <- ErrConfigReload a.errorChan <- ErrConfigReload
}) })
if err != nil { if err != nil {
@@ -160,7 +169,7 @@ func (a *api) Reload() error {
cfg.Merge() cfg.Merge()
if len(cfg.Host.Name) == 0 && cfg.Host.Auto { if len(cfg.Host.Name) == 0 && cfg.Host.Auto {
cfg.SetPublicIPs() cfg.Host.Name = net.GetPublicIPs(5 * time.Second)
} }
cfg.Validate(false) cfg.Validate(false)
@@ -228,8 +237,10 @@ func (a *api) Reload() error {
logger.Info().WithFields(logfields).Log("") logger.Info().WithFields(logfields).Log("")
logger.Info().WithField("path", a.config.path).Log("Read config file")
configlogger := logger.WithComponent("Config") configlogger := logger.WithComponent("Config")
cfg.Messages(func(level string, v config.Variable, message string) { cfg.Messages(func(level string, v configvars.Variable, message string) {
configlogger = configlogger.WithFields(log.Fields{ configlogger = configlogger.WithFields(log.Fields{
"variable": v.Name, "variable": v.Name,
"value": v.Value, "value": v.Value,
@@ -254,6 +265,8 @@ func (a *api) Reload() error {
return fmt.Errorf("not all variables are set or valid") return fmt.Errorf("not all variables are set or valid")
} }
cfg.LoadedAt = time.Now()
store.SetActive(cfg) store.SetActive(cfg)
a.config.store = store a.config.store = store
@@ -286,7 +299,13 @@ func (a *api) start() error {
} }
if cfg.Sessions.Persist { if cfg.Sessions.Persist {
sessionConfig.PersistDir = filepath.Join(cfg.DB.Dir, "sessions") fs, err := fs.NewRootedDiskFilesystem(fs.RootedDiskConfig{
Root: filepath.Join(cfg.DB.Dir, "sessions"),
})
if err != nil {
return fmt.Errorf("unable to create filesystem for persisting sessions: %w", err)
}
sessionConfig.PersistFS = fs
} }
sessions, err := session.New(sessionConfig) sessions, err := session.New(sessionConfig)
@@ -367,18 +386,18 @@ func (a *api) start() error {
a.sessions = sessions a.sessions = sessions
} }
store := store.NewJSONStore(store.JSONConfig{ diskfs, err := fs.NewRootedDiskFilesystem(fs.RootedDiskConfig{
Dir: cfg.DB.Dir, Root: cfg.Storage.Disk.Dir,
Logger: a.log.logger.core.WithComponent("ProcessStore"),
})
diskfs, err := fs.NewDiskFilesystem(fs.DiskConfig{
Dir: cfg.Storage.Disk.Dir,
Size: cfg.Storage.Disk.Size * 1024 * 1024,
Logger: a.log.logger.core.WithComponent("DiskFS"), Logger: a.log.logger.core.WithComponent("DiskFS"),
}) })
if err != nil { if err != nil {
return fmt.Errorf("disk filesystem: %w", err)
}
if diskfsRoot, err := filepath.Abs(cfg.Storage.Disk.Dir); err != nil {
return err return err
} else {
diskfs.SetMetadata("base", diskfsRoot)
} }
a.diskfs = diskfs a.diskfs = diskfs
@@ -400,17 +419,60 @@ func (a *api) start() error {
} }
if a.memfs == nil { if a.memfs == nil {
memfs := fs.NewMemFilesystem(fs.MemConfig{ memfs, _ := fs.NewMemFilesystem(fs.MemConfig{
Base: baseMemFS.String(),
Size: cfg.Storage.Memory.Size * 1024 * 1024,
Purge: cfg.Storage.Memory.Purge,
Logger: a.log.logger.core.WithComponent("MemFS"), Logger: a.log.logger.core.WithComponent("MemFS"),
}) })
a.memfs = memfs memfs.SetMetadata("base", baseMemFS.String())
sizedfs, _ := fs.NewSizedFilesystem(memfs, cfg.Storage.Memory.Size*1024*1024, cfg.Storage.Memory.Purge)
a.memfs = sizedfs
} else { } else {
a.memfs.Rebase(baseMemFS.String()) a.memfs.SetMetadata("base", baseMemFS.String())
a.memfs.Resize(cfg.Storage.Memory.Size * 1024 * 1024) if sizedfs, ok := a.memfs.(fs.SizedFilesystem); ok {
sizedfs.Resize(cfg.Storage.Memory.Size * 1024 * 1024)
}
}
for _, s3 := range cfg.Storage.S3 {
if _, ok := a.s3fs[s3.Name]; ok {
return fmt.Errorf("the name '%s' for a s3 filesystem is already in use", s3.Name)
}
baseS3FS := url.URL{
Scheme: "http",
Path: s3.Mountpoint,
}
host, port, _ := gonet.SplitHostPort(cfg.Address)
if len(host) == 0 {
baseS3FS.Host = "localhost:" + port
} else {
baseS3FS.Host = cfg.Address
}
if s3.Auth.Enable {
baseS3FS.User = url.UserPassword(s3.Auth.Username, s3.Auth.Password)
}
s3fs, err := fs.NewS3Filesystem(fs.S3Config{
Name: s3.Name,
Endpoint: s3.Endpoint,
AccessKeyID: s3.AccessKeyID,
SecretAccessKey: s3.SecretAccessKey,
Region: s3.Region,
Bucket: s3.Bucket,
UseSSL: s3.UseSSL,
Logger: a.log.logger.core.WithComponent("FS"),
})
if err != nil {
return fmt.Errorf("s3 filesystem (%s): %w", s3.Name, err)
}
s3fs.SetMetadata("base", baseS3FS.String())
a.s3fs[s3.Name] = s3fs
} }
var portrange net.Portranger var portrange net.Portranger
@@ -418,18 +480,18 @@ func (a *api) start() error {
if cfg.Playout.Enable { if cfg.Playout.Enable {
portrange, err = net.NewPortrange(cfg.Playout.MinPort, cfg.Playout.MaxPort) portrange, err = net.NewPortrange(cfg.Playout.MinPort, cfg.Playout.MaxPort)
if err != nil { if err != nil {
return err return fmt.Errorf("playout port range: %w", err)
} }
} }
validatorIn, err := ffmpeg.NewValidator(cfg.FFmpeg.Access.Input.Allow, cfg.FFmpeg.Access.Input.Block) validatorIn, err := ffmpeg.NewValidator(cfg.FFmpeg.Access.Input.Allow, cfg.FFmpeg.Access.Input.Block)
if err != nil { if err != nil {
return err return fmt.Errorf("input address validator: %w", err)
} }
validatorOut, err := ffmpeg.NewValidator(cfg.FFmpeg.Access.Output.Allow, cfg.FFmpeg.Access.Output.Block) validatorOut, err := ffmpeg.NewValidator(cfg.FFmpeg.Access.Output.Allow, cfg.FFmpeg.Access.Output.Block)
if err != nil { if err != nil {
return err return fmt.Errorf("output address validator: %w", err)
} }
ffmpeg, err := ffmpeg.New(ffmpeg.Config{ ffmpeg, err := ffmpeg.New(ffmpeg.Config{
@@ -443,7 +505,7 @@ func (a *api) start() error {
Collector: a.sessions.Collector("ffmpeg"), Collector: a.sessions.Collector("ffmpeg"),
}) })
if err != nil { if err != nil {
return err return fmt.Errorf("unable to create ffmpeg: %w", err)
} }
a.ffmpeg = ffmpeg a.ffmpeg = ffmpeg
@@ -451,9 +513,27 @@ func (a *api) start() error {
a.replacer = replace.New() a.replacer = replace.New()
{ {
a.replacer.RegisterTemplate("diskfs", a.diskfs.Base()) a.replacer.RegisterTemplateFunc("diskfs", func(config *restreamapp.Config, section string) string {
a.replacer.RegisterTemplate("memfs", a.memfs.Base()) return a.diskfs.Metadata("base")
}, nil)
a.replacer.RegisterTemplateFunc("fs:disk", func(config *restreamapp.Config, section string) string {
return a.diskfs.Metadata("base")
}, nil)
a.replacer.RegisterTemplateFunc("memfs", func(config *restreamapp.Config, section string) string {
return a.memfs.Metadata("base")
}, nil)
a.replacer.RegisterTemplateFunc("fs:mem", func(config *restreamapp.Config, section string) string {
return a.memfs.Metadata("base")
}, nil)
for name, s3 := range a.s3fs {
a.replacer.RegisterTemplate("fs:"+name, s3.Metadata("base"), nil)
}
a.replacer.RegisterTemplateFunc("rtmp", func(config *restreamapp.Config, section string) string {
host, port, _ := gonet.SplitHostPort(cfg.RTMP.Address) host, port, _ := gonet.SplitHostPort(cfg.RTMP.Address)
if len(host) == 0 { if len(host) == 0 {
host = "localhost" host = "localhost"
@@ -469,29 +549,67 @@ func (a *api) start() error {
template += "?token=" + cfg.RTMP.Token template += "?token=" + cfg.RTMP.Token
} }
a.replacer.RegisterTemplate("rtmp", template) return template
}, nil)
a.replacer.RegisterTemplateFunc("srt", func(config *restreamapp.Config, section string) string {
host, port, _ = gonet.SplitHostPort(cfg.SRT.Address) host, port, _ = gonet.SplitHostPort(cfg.SRT.Address)
if len(host) == 0 { if len(host) == 0 {
host = "localhost" host = "localhost"
} }
template = "srt://" + host + ":" + port + "?mode=caller&transtype=live&streamid=#!:m={mode},r={name}" template := "srt://" + host + ":" + port + "?mode=caller&transtype=live&latency={latency}&streamid={name}"
if section == "output" {
template += ",mode:publish"
} else {
template += ",mode:request"
}
if len(cfg.SRT.Token) != 0 { if len(cfg.SRT.Token) != 0 {
template += ",token=" + cfg.SRT.Token template += ",token:" + cfg.SRT.Token
} }
if len(cfg.SRT.Passphrase) != 0 { if len(cfg.SRT.Passphrase) != 0 {
template += "&passphrase=" + cfg.SRT.Passphrase template += "&passphrase=" + cfg.SRT.Passphrase
} }
a.replacer.RegisterTemplate("srt", template)
return template
}, map[string]string{
"latency": "20000", // 20 milliseconds, FFmpeg requires microseconds
})
}
filesystems := []fs.Filesystem{
a.diskfs,
a.memfs,
}
for _, fs := range a.s3fs {
filesystems = append(filesystems, fs)
}
var store restreamstore.Store = nil
{
fs, err := fs.NewRootedDiskFilesystem(fs.RootedDiskConfig{
Root: cfg.DB.Dir,
})
if err != nil {
return err
}
store, err = restreamstore.NewJSON(restreamstore.JSONConfig{
Filesystem: fs,
Filepath: "/db.json",
Logger: a.log.logger.core.WithComponent("ProcessStore"),
})
if err != nil {
return err
}
} }
restream, err := restream.New(restream.Config{ restream, err := restream.New(restream.Config{
ID: cfg.ID, ID: cfg.ID,
Name: cfg.Name, Name: cfg.Name,
Store: store, Store: store,
DiskFS: a.diskfs, Filesystems: filesystems,
MemFS: a.memfs,
Replace: a.replacer, Replace: a.replacer,
FFmpeg: a.ffmpeg, FFmpeg: a.ffmpeg,
MaxProcesses: cfg.FFmpeg.MaxProcesses, MaxProcesses: cfg.FFmpeg.MaxProcesses,
@@ -569,9 +687,12 @@ func (a *api) start() error {
metrics.Register(monitor.NewCPUCollector()) metrics.Register(monitor.NewCPUCollector())
metrics.Register(monitor.NewMemCollector()) metrics.Register(monitor.NewMemCollector())
metrics.Register(monitor.NewNetCollector()) metrics.Register(monitor.NewNetCollector())
metrics.Register(monitor.NewDiskCollector(a.diskfs.Base())) metrics.Register(monitor.NewDiskCollector(a.diskfs.Metadata("base")))
metrics.Register(monitor.NewFilesystemCollector("diskfs", diskfs)) metrics.Register(monitor.NewFilesystemCollector("diskfs", a.diskfs))
metrics.Register(monitor.NewFilesystemCollector("memfs", a.memfs)) metrics.Register(monitor.NewFilesystemCollector("memfs", a.memfs))
for name, fs := range a.s3fs {
metrics.Register(monitor.NewFilesystemCollector(name, fs))
}
metrics.Register(monitor.NewRestreamCollector(a.restream)) metrics.Register(monitor.NewRestreamCollector(a.restream))
metrics.Register(monitor.NewFFmpegCollector(a.ffmpeg)) metrics.Register(monitor.NewFFmpegCollector(a.ffmpeg))
metrics.Register(monitor.NewSessionCollector(a.sessions, []string{})) metrics.Register(monitor.NewSessionCollector(a.sessions, []string{}))
@@ -646,7 +767,7 @@ func (a *api) start() error {
} }
if cfg.Storage.Disk.Cache.Enable { if cfg.Storage.Disk.Cache.Enable {
diskCache, err := cache.NewLRUCache(cache.LRUConfig{ cache, err := cache.NewLRUCache(cache.LRUConfig{
TTL: time.Duration(cfg.Storage.Disk.Cache.TTL) * time.Second, TTL: time.Duration(cfg.Storage.Disk.Cache.TTL) * time.Second,
MaxSize: cfg.Storage.Disk.Cache.Size * 1024 * 1024, MaxSize: cfg.Storage.Disk.Cache.Size * 1024 * 1024,
MaxFileSize: cfg.Storage.Disk.Cache.FileSize * 1024 * 1024, MaxFileSize: cfg.Storage.Disk.Cache.FileSize * 1024 * 1024,
@@ -656,50 +777,39 @@ func (a *api) start() error {
}) })
if err != nil { if err != nil {
return fmt.Errorf("unable to create disk cache: %w", err) return fmt.Errorf("unable to create cache: %w", err)
} }
a.cache = diskCache a.cache = cache
} }
var autocertManager *certmagic.Config var autocertManager *certmagic.Config
if cfg.TLS.Enable && cfg.TLS.Auto { if cfg.TLS.Enable {
if cfg.TLS.Auto {
if len(cfg.Host.Name) == 0 { if len(cfg.Host.Name) == 0 {
return fmt.Errorf("at least one host must be provided in host.name or RS_HOST_NAME") return fmt.Errorf("at least one host must be provided in host.name or CORE_HOST_NAME")
} }
certmagic.Default.Storage = &certmagic.FileStorage{
Path: cfg.DB.Dir + "/cert",
}
certmagic.Default.DefaultServerName = cfg.Host.Name[0]
certmagic.Default.Logger = zap.NewNop()
certmagic.DefaultACME.Agreed = true certmagic.DefaultACME.Agreed = true
certmagic.DefaultACME.Email = cfg.TLS.Email certmagic.DefaultACME.Email = cfg.TLS.Email
certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
certmagic.DefaultACME.DisableHTTPChallenge = false certmagic.DefaultACME.DisableHTTPChallenge = false
certmagic.DefaultACME.DisableTLSALPNChallenge = true certmagic.DefaultACME.DisableTLSALPNChallenge = true
certmagic.DefaultACME.Logger = nil certmagic.DefaultACME.Logger = zap.NewNop()
certmagic.Default.Storage = &certmagic.FileStorage{
Path: cfg.DB.Dir + "/cert",
}
certmagic.Default.DefaultServerName = cfg.Host.Name[0]
certmagic.Default.Logger = nil
certmagic.Default.OnEvent = func(event string, data interface{}) {
message := ""
switch data := data.(type) {
case string:
message = data
case fmt.Stringer:
message = data.String()
}
if len(message) != 0 {
a.log.logger.core.WithComponent("certmagic").Info().WithField("event", event).Log(message)
}
}
magic := certmagic.NewDefault() magic := certmagic.NewDefault()
acme := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME) acme := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
acme.Logger = zap.NewNop()
magic.Issuers = []certmagic.Issuer{acme} magic.Issuers = []certmagic.Issuer{acme}
magic.Logger = zap.NewNop()
autocertManager = magic autocertManager = magic
@@ -738,6 +848,19 @@ func (a *api) start() error {
if err != nil { if err != nil {
logger.Error().WithField("error", err).Log("Failed to acquire certificate") logger.Error().WithField("error", err).Log("Failed to acquire certificate")
certerror = true certerror = true
/*
problems, err := letsdebug.Check(host, letsdebug.HTTP01)
if err != nil {
logger.Error().WithField("error", err).Log("Failed to debug certificate acquisition")
}
for _, p := range problems {
logger.Error().WithFields(log.Fields{
"name": p.Name,
"detail": p.Detail,
}).Log(p.Explanation)
}
*/
break break
} }
@@ -757,6 +880,9 @@ func (a *api) start() error {
cfg.TLS.CertFile = "" cfg.TLS.CertFile = ""
cfg.TLS.KeyFile = "" cfg.TLS.KeyFile = ""
} }
} else {
a.log.logger.core.Info().Log("Enabling TLS with cert and key files")
}
} }
if cfg.RTMP.Enable { if cfg.RTMP.Enable {
@@ -772,14 +898,15 @@ func (a *api) start() error {
Cluster: a.cluster, Cluster: a.cluster,
} }
if autocertManager != nil && cfg.RTMP.EnableTLS { if cfg.RTMP.EnableTLS {
config.TLSConfig = &tls.Config{
GetCertificate: autocertManager.GetCertificate,
}
config.Logger = config.Logger.WithComponent("RTMP/S") config.Logger = config.Logger.WithComponent("RTMP/S")
a.log.logger.rtmps = a.log.logger.core.WithComponent("RTMPS").WithField("address", cfg.RTMP.AddressTLS) a.log.logger.rtmps = a.log.logger.core.WithComponent("RTMPS").WithField("address", cfg.RTMP.AddressTLS)
if autocertManager != nil {
config.TLSConfig = &tls.Config{
GetCertificate: autocertManager.GetCertificate,
}
}
} }
rtmpserver, err := rtmp.New(config) rtmpserver, err := rtmp.New(config)
@@ -843,20 +970,59 @@ func (a *api) start() error {
a.log.logger.main = a.log.logger.core.WithComponent(logcontext).WithField("address", cfg.Address) a.log.logger.main = a.log.logger.core.WithComponent(logcontext).WithField("address", cfg.Address)
mainserverhandler, err := http.NewServer(http.Config{ httpfilesystems := []httpfs.FS{
{
Name: a.diskfs.Name(),
Mountpoint: "",
AllowWrite: false,
EnableAuth: false,
Username: "",
Password: "",
DefaultFile: "index.html",
DefaultContentType: "text/html",
Gzip: true,
Filesystem: a.diskfs,
Cache: a.cache,
},
{
Name: a.memfs.Name(),
Mountpoint: "/memfs",
AllowWrite: true,
EnableAuth: cfg.Storage.Memory.Auth.Enable,
Username: cfg.Storage.Memory.Auth.Username,
Password: cfg.Storage.Memory.Auth.Password,
DefaultFile: "",
DefaultContentType: "application/data",
Gzip: true,
Filesystem: a.memfs,
Cache: nil,
},
}
for _, s3 := range cfg.Storage.S3 {
httpfilesystems = append(httpfilesystems, httpfs.FS{
Name: s3.Name,
Mountpoint: s3.Mountpoint,
AllowWrite: true,
EnableAuth: s3.Auth.Enable,
Username: s3.Auth.Username,
Password: s3.Auth.Password,
DefaultFile: "",
DefaultContentType: "application/data",
Gzip: true,
Filesystem: a.s3fs[s3.Name],
Cache: a.cache,
})
}
serverConfig := http.Config{
Logger: a.log.logger.main, Logger: a.log.logger.main,
LogBuffer: a.log.buffer, LogBuffer: a.log.buffer,
Restream: a.restream, Restream: a.restream,
Metrics: a.metrics, Metrics: a.metrics,
Prometheus: a.prom, Prometheus: a.prom,
MimeTypesFile: cfg.Storage.MimeTypes, MimeTypesFile: cfg.Storage.MimeTypes,
DiskFS: a.diskfs, Filesystems: httpfilesystems,
MemFS: http.MemFSConfig{
EnableAuth: cfg.Storage.Memory.Auth.Enable,
Username: cfg.Storage.Memory.Auth.Username,
Password: cfg.Storage.Memory.Auth.Password,
Filesystem: a.memfs,
},
IPLimiter: iplimiter, IPLimiter: iplimiter,
Profiling: cfg.Debug.Profiling, Profiling: cfg.Debug.Profiling,
Cors: http.CorsConfig{ Cors: http.CorsConfig{
@@ -866,12 +1032,13 @@ func (a *api) start() error {
SRT: a.srtserver, SRT: a.srtserver,
JWT: a.httpjwt, JWT: a.httpjwt,
Config: a.config.store, Config: a.config.store,
Cache: a.cache,
Sessions: a.sessions, Sessions: a.sessions,
Router: router, Router: router,
ReadOnly: cfg.API.ReadOnly, ReadOnly: cfg.API.ReadOnly,
Cluster: a.cluster, Cluster: a.cluster,
}) }
mainserverhandler, err := http.NewServer(serverConfig)
if err != nil { if err != nil {
return fmt.Errorf("unable to create server: %w", err) return fmt.Errorf("unable to create server: %w", err)
@@ -906,35 +1073,10 @@ func (a *api) start() error {
a.log.logger.sidecar = a.log.logger.core.WithComponent("HTTP").WithField("address", cfg.Address) a.log.logger.sidecar = a.log.logger.core.WithComponent("HTTP").WithField("address", cfg.Address)
sidecarserverhandler, err := http.NewServer(http.Config{ serverConfig.Logger = a.log.logger.sidecar
Logger: a.log.logger.sidecar, serverConfig.IPLimiter = iplimiter
LogBuffer: a.log.buffer,
Restream: a.restream, sidecarserverhandler, err := http.NewServer(serverConfig)
Metrics: a.metrics,
Prometheus: a.prom,
MimeTypesFile: cfg.Storage.MimeTypes,
DiskFS: a.diskfs,
MemFS: http.MemFSConfig{
EnableAuth: cfg.Storage.Memory.Auth.Enable,
Username: cfg.Storage.Memory.Auth.Username,
Password: cfg.Storage.Memory.Auth.Password,
Filesystem: a.memfs,
},
IPLimiter: iplimiter,
Profiling: cfg.Debug.Profiling,
Cors: http.CorsConfig{
Origins: cfg.Storage.CORS.Origins,
},
RTMP: a.rtmpserver,
SRT: a.srtserver,
JWT: a.httpjwt,
Config: a.config.store,
Cache: a.cache,
Sessions: a.sessions,
Router: router,
ReadOnly: cfg.API.ReadOnly,
Cluster: a.cluster,
})
if err != nil { if err != nil {
return fmt.Errorf("unable to create sidecar HTTP server: %w", err) return fmt.Errorf("unable to create sidecar HTTP server: %w", err)
@@ -1126,6 +1268,12 @@ func (a *api) start() error {
}(ctx) }(ctx)
} }
if cfg.Debug.MemoryLimit > 0 {
debug.SetMemoryLimit(cfg.Debug.MemoryLimit * 1024 * 1024)
} else {
debug.SetMemoryLimit(math.MaxInt64)
}
// Start the restream processes // Start the restream processes
restream.Start() restream.Start()
@@ -1216,6 +1364,9 @@ func (a *api) stop() {
a.cache = nil a.cache = nil
} }
// Free the S3 mounts
a.s3fs = map[string]fs.Filesystem{}
// Stop the SRT server // Stop the SRT server
if a.srtserver != nil { if a.srtserver != nil {
a.log.logger.srt.Info().Log("Stopping ...") a.log.logger.srt.Info().Log("Stopping ...")
@@ -1296,7 +1447,7 @@ func (a *api) Destroy() {
// Free the MemFS // Free the MemFS
if a.memfs != nil { if a.memfs != nil {
a.memfs.DeleteAll() a.memfs.RemoveAll()
a.memfs = nil a.memfs = nil
} }
} }

176
app/ffmigrate/main.go Normal file
View File

@@ -0,0 +1,176 @@
package main
import (
"fmt"
"os"
"regexp"
cfgstore "github.com/datarhei/core/v16/config/store"
cfgvars "github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/ffmpeg"
"github.com/datarhei/core/v16/io/file"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/restream/store"
"github.com/Masterminds/semver/v3"
_ "github.com/joho/godotenv/autoload"
)
func main() {
logger := log.New("Migration").WithOutput(log.NewConsoleWriter(os.Stderr, log.Linfo, true)).WithFields(log.Fields{
"from": "ffmpeg4",
"to": "ffmpeg5",
})
configfile := cfgstore.Location(os.Getenv("CORE_CONFIGFILE"))
diskfs, _ := fs.NewDiskFilesystem(fs.DiskConfig{})
configstore, err := cfgstore.NewJSON(diskfs, configfile, nil)
if err != nil {
logger.Error().WithError(err).Log("Loading configuration failed")
os.Exit(1)
}
if err := doMigration(logger, configstore); err != nil {
os.Exit(1)
}
}
func doMigration(logger log.Logger, configstore cfgstore.Store) error {
if logger == nil {
logger = log.New("")
}
cfg := configstore.Get()
// Merging the persisted config with the environment variables
cfg.Merge()
cfg.Validate(false)
if cfg.HasErrors() {
logger.Error().Log("The configuration contains errors")
messages := []string{}
cfg.Messages(func(level string, v cfgvars.Variable, message string) {
if level == "error" {
logger.Error().WithFields(log.Fields{
"variable": v.Name,
"value": v.Value,
"env": v.EnvName,
"description": v.Description,
}).Log(message)
messages = append(messages, v.Name+": "+message)
}
})
return fmt.Errorf("the configuration contains errors: %v", messages)
}
ff, err := ffmpeg.New(ffmpeg.Config{
Binary: cfg.FFmpeg.Binary,
})
if err != nil {
logger.Error().WithError(err).Log("Loading FFmpeg binary failed")
return fmt.Errorf("loading FFmpeg binary failed: %w", err)
}
version, err := semver.NewVersion(ff.Skills().FFmpeg.Version)
if err != nil {
logger.Error().WithError(err).Log("Parsing FFmpeg version failed")
return fmt.Errorf("parsing FFmpeg version failed: %w", err)
}
// The current FFmpeg version is 4. Nothing to do.
if version.Major() == 4 {
return nil
}
if version.Major() != 5 {
err := fmt.Errorf("unknown FFmpeg version found: %d", version.Major())
logger.Error().WithError(err).Log("Unsupported FFmpeg version found")
return fmt.Errorf("unsupported FFmpeg version found: %w", err)
}
// Check if there's a DB file
dbFilepath := cfg.DB.Dir + "/db.json"
if _, err = os.Stat(dbFilepath); err != nil {
// There's no DB to backup
logger.Info().WithField("db", dbFilepath).Log("Database not found. Migration not required")
return nil
}
// Check if we already have a backup
backupFilepath := cfg.DB.Dir + "/db_ff4.json"
if _, err = os.Stat(backupFilepath); err == nil {
// Yes, we have a backup. The migration already happened
logger.Info().WithField("backup", backupFilepath).Log("Migration already done")
return nil
}
// Create a backup
if err := file.Copy(dbFilepath, backupFilepath); err != nil {
logger.Error().WithError(err).Log("Creating backup file failed")
return fmt.Errorf("creating backup file failed: %w", err)
}
logger.Info().WithField("backup", backupFilepath).Log("Backup created")
// Load the existing DB
datastore, err := store.NewJSON(store.JSONConfig{
Filepath: cfg.DB.Dir + "/db.json",
})
if err != nil {
return err
}
data, err := datastore.Load()
if err != nil {
logger.Error().WithError(err).Log("Loading database failed")
return fmt.Errorf("loading database failed: %w", err)
}
logger.Info().Log("Migrating processes ...")
// Migrate the processes to version 5
// Only this happens:
// - for RTSP inputs, replace -stimeout with -timeout
reRTSP := regexp.MustCompile(`^rtsps?://`)
for id, p := range data.Process {
logger.Info().WithField("processid", p.ID).Log("")
for index, input := range p.Config.Input {
if !reRTSP.MatchString(input.Address) {
continue
}
for i, o := range input.Options {
if o != "-stimeout" {
continue
}
input.Options[i] = "-timeout"
}
p.Config.Input[index] = input
}
p.Config.FFVersion = version.String()
data.Process[id] = p
}
logger.Info().Log("Migrating processes done")
// Store the modified DB
if err := datastore.Store(data); err != nil {
logger.Error().WithError(err).Log("Storing database failed")
return fmt.Errorf("storing database failed: %w", err)
}
logger.Info().Log("Completed")
return nil
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/datarhei/core/v16/encoding/json" "github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/ffmpeg" "github.com/datarhei/core/v16/ffmpeg"
"github.com/datarhei/core/v16/ffmpeg/skills" "github.com/datarhei/core/v16/ffmpeg/skills"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/restream" "github.com/datarhei/core/v16/restream"
"github.com/datarhei/core/v16/restream/app" "github.com/datarhei/core/v16/restream/app"
"github.com/datarhei/core/v16/restream/store" "github.com/datarhei/core/v16/restream/store"
@@ -495,14 +496,14 @@ type importConfigAudio struct {
sampling string sampling string
} }
func importV1(path string, cfg importConfig) (store.StoreData, error) { func importV1(fs fs.Filesystem, path string, cfg importConfig) (store.StoreData, error) {
if len(cfg.id) == 0 { if len(cfg.id) == 0 {
cfg.id = uuid.New().String() cfg.id = uuid.New().String()
} }
r := store.NewStoreData() r := store.NewStoreData()
jsondata, err := os.ReadFile(path) jsondata, err := fs.ReadFile(path)
if err != nil { if err != nil {
return r, fmt.Errorf("failed to read data from %s: %w", path, err) return r, fmt.Errorf("failed to read data from %s: %w", path, err)
} }
@@ -1417,9 +1418,19 @@ func probeInput(binary string, config app.Config) app.Probe {
return app.Probe{} return app.Probe{}
} }
dummyfs, _ := fs.NewMemFilesystem(fs.MemConfig{})
store, err := store.NewJSON(store.JSONConfig{
Filesystem: dummyfs,
Filepath: "/",
Logger: nil,
})
if err != nil {
return app.Probe{}
}
rs, err := restream.New(restream.Config{ rs, err := restream.New(restream.Config{
FFmpeg: ffmpeg, FFmpeg: ffmpeg,
Store: store.NewDummyStore(store.DummyConfig{}), Store: store,
}) })
if err != nil { if err != nil {
return app.Probe{} return app.Probe{}

View File

@@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/datarhei/core/v16/encoding/json" "github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/restream/store" "github.com/datarhei/core/v16/restream/store"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -36,8 +37,13 @@ import (
var id string = "4186b095-7f0a-4e94-8c3d-f17459ab252f" var id string = "4186b095-7f0a-4e94-8c3d-f17459ab252f"
func testV1Import(t *testing.T, v1Fixture, v4Fixture string, config importConfig) { func testV1Import(t *testing.T, v1Fixture, v4Fixture string, config importConfig) {
diskfs, err := fs.NewRootedDiskFilesystem(fs.RootedDiskConfig{
Root: ".",
})
require.NoError(t, err)
// Import v1 database // Import v1 database
v4, err := importV1(v1Fixture, config) v4, err := importV1(diskfs, v1Fixture, config)
require.Equal(t, nil, err) require.Equal(t, nil, err)
// Reset variants // Reset variants
@@ -50,7 +56,7 @@ func testV1Import(t *testing.T, v1Fixture, v4Fixture string, config importConfig
require.Equal(t, nil, err) require.Equal(t, nil, err)
// Read the wanted result // Read the wanted result
wantdatav4, err := os.ReadFile(v4Fixture) wantdatav4, err := diskfs.ReadFile(v4Fixture)
require.Equal(t, nil, err) require.Equal(t, nil, err)
var wantv4 store.StoreData var wantv4 store.StoreData

View File

@@ -4,7 +4,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/datarhei/core/v16/config" cfgstore "github.com/datarhei/core/v16/config/store"
cfgvars "github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/log" "github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/restream/store" "github.com/datarhei/core/v16/restream/store"
@@ -14,18 +16,26 @@ import (
func main() { func main() {
logger := log.New("Import").WithOutput(log.NewConsoleWriter(os.Stderr, log.Linfo, true)).WithField("version", "v1") logger := log.New("Import").WithOutput(log.NewConsoleWriter(os.Stderr, log.Linfo, true)).WithField("version", "v1")
configstore, err := config.NewJSONStore(os.Getenv("CORE_CONFIGFILE"), nil) configfile := cfgstore.Location(os.Getenv("CORE_CONFIGFILE"))
diskfs, err := fs.NewDiskFilesystem(fs.DiskConfig{})
if err != nil {
logger.Error().WithError(err).Log("Access disk filesystem failed")
os.Exit(1)
}
configstore, err := cfgstore.NewJSON(diskfs, configfile, nil)
if err != nil { if err != nil {
logger.Error().WithError(err).Log("Loading configuration failed") logger.Error().WithError(err).Log("Loading configuration failed")
os.Exit(1) os.Exit(1)
} }
if err := doImport(logger, configstore); err != nil { if err := doImport(logger, diskfs, configstore); err != nil {
os.Exit(1) os.Exit(1)
} }
} }
func doImport(logger log.Logger, configstore config.Store) error { func doImport(logger log.Logger, fs fs.Filesystem, configstore cfgstore.Store) error {
if logger == nil { if logger == nil {
logger = log.New("") logger = log.New("")
} }
@@ -41,7 +51,7 @@ func doImport(logger log.Logger, configstore config.Store) error {
if cfg.HasErrors() { if cfg.HasErrors() {
logger.Error().Log("The configuration contains errors") logger.Error().Log("The configuration contains errors")
messages := []string{} messages := []string{}
cfg.Messages(func(level string, v config.Variable, message string) { cfg.Messages(func(level string, v cfgvars.Variable, message string) {
if level == "error" { if level == "error" {
logger.Error().WithFields(log.Fields{ logger.Error().WithFields(log.Fields{
"variable": v.Name, "variable": v.Name,
@@ -64,23 +74,27 @@ func doImport(logger log.Logger, configstore config.Store) error {
logger = logger.WithField("database", v1filename) logger = logger.WithField("database", v1filename)
if _, err := os.Stat(v1filename); err != nil { if _, err := fs.Stat(v1filename); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
logger.Info().Log("Database doesn't exist and nothing will be imported") logger.Info().Log("Database doesn't exist and nothing will be imported")
return nil return nil
} }
logger.Error().WithError(err).Log("Checking for v1 database") logger.Error().WithError(err).Log("Checking for v1 database")
return fmt.Errorf("checking for v1 database: %w", err) return fmt.Errorf("checking for v1 database: %w", err)
} }
logger.Info().Log("Found database") logger.Info().Log("Found database")
// Load an existing DB // Load an existing DB
datastore := store.NewJSONStore(store.JSONConfig{ datastore, err := store.NewJSON(store.JSONConfig{
Dir: cfg.DB.Dir, Filesystem: fs,
Filepath: cfg.DB.Dir + "/db.json",
}) })
if err != nil {
logger.Error().WithError(err).Log("Creating datastore for new database failed")
return fmt.Errorf("creating datastore for new database failed: %w", err)
}
data, err := datastore.Load() data, err := datastore.Load()
if err != nil { if err != nil {
@@ -102,7 +116,7 @@ func doImport(logger log.Logger, configstore config.Store) error {
importConfig.binary = cfg.FFmpeg.Binary importConfig.binary = cfg.FFmpeg.Binary
// Rewrite the old database to the new database // Rewrite the old database to the new database
r, err := importV1(v1filename, importConfig) r, err := importV1(fs, v1filename, importConfig)
if err != nil { if err != nil {
logger.Error().WithError(err).Log("Importing database failed") logger.Error().WithError(err).Log("Importing database failed")
return fmt.Errorf("importing database failed: %w", err) return fmt.Errorf("importing database failed: %w", err)

View File

@@ -1,20 +1,30 @@
package main package main
import ( import (
"strings"
"testing" "testing"
"github.com/datarhei/core/v16/config" "github.com/datarhei/core/v16/config/store"
"github.com/datarhei/core/v16/io/fs"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestImport(t *testing.T) { func TestImport(t *testing.T) {
configstore := config.NewDummyStore() memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
memfs.WriteFileReader("/mime.types", strings.NewReader("foobar"))
memfs.WriteFileReader("/bin/ffmpeg", strings.NewReader("foobar"))
configstore, err := store.NewJSON(memfs, "/config.json", nil)
require.NoError(t, err)
cfg := configstore.Get() cfg := configstore.Get()
err := configstore.Set(cfg) err = configstore.Set(cfg)
require.NoError(t, err) require.NoError(t, err)
err = doImport(nil, configstore) err = doImport(nil, memfs, configstore)
require.NoError(t, err) require.NoError(t, err)
} }

View File

@@ -29,8 +29,8 @@ func (v versionInfo) MinorString() string {
// Version of the app // Version of the app
var Version = versionInfo{ var Version = versionInfo{
Major: 16, Major: 16,
Minor: 10, Minor: 12,
Patch: 1, Patch: 0,
} }
// Commit is the git commit the app is build from. It should be filled in during compilation // Commit is the git commit the app is build from. It should be filled in during compilation

View File

@@ -242,7 +242,7 @@ func (n *node) files() {
} }
for _, file := range files { for _, file := range files {
filesChan <- "memfs:" + file.Name filesChan <- "mem:" + file.Name
} }
}(filesChan) }(filesChan)
@@ -255,7 +255,7 @@ func (n *node) files() {
} }
for _, file := range files { for _, file := range files {
filesChan <- "diskfs:" + file.Name filesChan <- "disk:" + file.Name
} }
}(filesChan) }(filesChan)
@@ -316,9 +316,9 @@ func (n *node) getURL(path string) (string, error) {
u := "" u := ""
if prefix == "memfs:" { if prefix == "mem:" {
u = n.address + "/" + filepath.Join("memfs", path) u = n.address + "/" + filepath.Join("memfs", path)
} else if prefix == "diskfs:" { } else if prefix == "disk:" {
u = n.address + path u = n.address + path
} else if prefix == "rtmp:" { } else if prefix == "rtmp:" {
u = n.rtmpAddress + path u = n.rtmpAddress + path
@@ -347,9 +347,9 @@ func (n *node) getFile(path string) (io.ReadCloser, error) {
prefix := n.prefix.FindString(path) prefix := n.prefix.FindString(path)
path = n.prefix.ReplaceAllString(path, "") path = n.prefix.ReplaceAllString(path, "")
if prefix == "memfs:" { if prefix == "mem:" {
return n.peer.MemFSGetFile(path) return n.peer.MemFSGetFile(path)
} else if prefix == "diskfs:" { } else if prefix == "disk:" {
return n.peer.DiskFSGetFile(path) return n.peer.DiskFSGetFile(path)
} }

View File

@@ -3,76 +3,82 @@ package config
import ( import (
"context" "context"
"fmt"
"net" "net"
"os"
"time" "time"
"github.com/datarhei/core/v16/config/copy"
"github.com/datarhei/core/v16/config/value"
"github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/math/rand" "github.com/datarhei/core/v16/math/rand"
haikunator "github.com/atrox/haikunatorgo/v2" haikunator "github.com/atrox/haikunatorgo/v2"
"github.com/google/uuid" "github.com/google/uuid"
) )
/*
type Config interface {
// Merge merges the values of the known environment variables into the configuration
Merge()
// Validate validates the current state of the Config for completeness and sanity. Errors are
// written to the log. Use resetLogs to indicate to reset the logs prior validation.
Validate(resetLogs bool)
// Messages calls for each log entry the provided callback. The level has the values 'error', 'warn', or 'info'.
// The name is the name of the configuration value, e.g. 'api.auth.enable'. The message is the log message.
Messages(logger func(level string, v vars.Variable, message string))
// HasErrors returns whether there are some error messages in the log.
HasErrors() bool
// Overrides returns a list of configuration value names that have been overriden by an environment variable.
Overrides() []string
Get(name string) (string, error)
Set(name, val string) error
}
*/
const version int64 = 3 const version int64 = 3
type variable struct { // Make sure that the config.Config interface is satisfied
value value // The actual value //var _ config.Config = &Config{}
defVal string // The default value in string representation
name string // A name for this value
envName string // The environment variable that corresponds to this value
envAltNames []string // Alternative environment variable names
description string // A desriptions for this value
required bool // Whether a non-empty value is required
disguise bool // Whether the value should be disguised if printed
merged bool // Whether this value has been replaced by its corresponding environment variable
}
type Variable struct {
Value string
Name string
EnvName string
Description string
Merged bool
}
type message struct {
message string // The log message
variable Variable // The config field this message refers to
level string // The loglevel for this message
}
type Auth0Tenant struct {
Domain string `json:"domain"`
Audience string `json:"audience"`
ClientID string `json:"clientid"`
Users []string `json:"users"`
}
type DataVersion struct {
Version int64 `json:"version"`
}
// Config is a wrapper for Data // Config is a wrapper for Data
type Config struct { type Config struct {
vars []*variable fs fs.Filesystem
logs []message vars vars.Variables
Data Data
} }
// New returns a Config which is initialized with its default values // New returns a Config which is initialized with its default values
func New() *Config { func New(f fs.Filesystem) *Config {
config := &Config{} config := &Config{
fs: f,
}
if config.fs == nil {
config.fs, _ = fs.NewMemFilesystem(fs.MemConfig{})
}
config.init() config.init()
return config return config
} }
func (d *Config) Get(name string) (string, error) {
return d.vars.Get(name)
}
func (d *Config) Set(name, val string) error {
return d.vars.Set(name, val)
}
// NewConfigFrom returns a clone of a Config // NewConfigFrom returns a clone of a Config
func NewConfigFrom(d *Config) *Config { func (d *Config) Clone() *Config {
data := New() data := New(d.fs)
data.CreatedAt = d.CreatedAt data.CreatedAt = d.CreatedAt
data.LoadedAt = d.LoadedAt data.LoadedAt = d.LoadedAt
@@ -100,286 +106,206 @@ func NewConfigFrom(d *Config) *Config {
data.Service = d.Service data.Service = d.Service
data.Router = d.Router data.Router = d.Router
data.Log.Topics = copyStringSlice(d.Log.Topics) data.Log.Topics = copy.Slice(d.Log.Topics)
data.Host.Name = copyStringSlice(d.Host.Name) data.Host.Name = copy.Slice(d.Host.Name)
data.API.Access.HTTP.Allow = copyStringSlice(d.API.Access.HTTP.Allow) data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow)
data.API.Access.HTTP.Block = copyStringSlice(d.API.Access.HTTP.Block) data.API.Access.HTTP.Block = copy.Slice(d.API.Access.HTTP.Block)
data.API.Access.HTTPS.Allow = copyStringSlice(d.API.Access.HTTPS.Allow) data.API.Access.HTTPS.Allow = copy.Slice(d.API.Access.HTTPS.Allow)
data.API.Access.HTTPS.Block = copyStringSlice(d.API.Access.HTTPS.Block) data.API.Access.HTTPS.Block = copy.Slice(d.API.Access.HTTPS.Block)
data.API.Auth.Auth0.Tenants = copyTenantSlice(d.API.Auth.Auth0.Tenants) data.API.Auth.Auth0.Tenants = copy.TenantSlice(d.API.Auth.Auth0.Tenants)
data.Storage.CORS.Origins = copyStringSlice(d.Storage.CORS.Origins) data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.Storage.Disk.Cache.Types.Allow = copyStringSlice(d.Storage.Disk.Cache.Types.Allow) data.Storage.Disk.Cache.Types.Allow = copy.Slice(d.Storage.Disk.Cache.Types.Allow)
data.Storage.Disk.Cache.Types.Block = copyStringSlice(d.Storage.Disk.Cache.Types.Block) data.Storage.Disk.Cache.Types.Block = copy.Slice(d.Storage.Disk.Cache.Types.Block)
data.Storage.S3 = copy.Slice(d.Storage.S3)
data.FFmpeg.Access.Input.Allow = copyStringSlice(d.FFmpeg.Access.Input.Allow) data.FFmpeg.Access.Input.Allow = copy.Slice(d.FFmpeg.Access.Input.Allow)
data.FFmpeg.Access.Input.Block = copyStringSlice(d.FFmpeg.Access.Input.Block) data.FFmpeg.Access.Input.Block = copy.Slice(d.FFmpeg.Access.Input.Block)
data.FFmpeg.Access.Output.Allow = copyStringSlice(d.FFmpeg.Access.Output.Allow) data.FFmpeg.Access.Output.Allow = copy.Slice(d.FFmpeg.Access.Output.Allow)
data.FFmpeg.Access.Output.Block = copyStringSlice(d.FFmpeg.Access.Output.Block) data.FFmpeg.Access.Output.Block = copy.Slice(d.FFmpeg.Access.Output.Block)
data.Sessions.IPIgnoreList = copyStringSlice(d.Sessions.IPIgnoreList) data.Sessions.IPIgnoreList = copy.Slice(d.Sessions.IPIgnoreList)
data.SRT.Log.Topics = copyStringSlice(d.SRT.Log.Topics) data.SRT.Log.Topics = copy.Slice(d.SRT.Log.Topics)
data.Router.BlockedPrefixes = copyStringSlice(d.Router.BlockedPrefixes) data.Router.BlockedPrefixes = copy.Slice(d.Router.BlockedPrefixes)
data.Router.Routes = copyStringMap(d.Router.Routes) data.Router.Routes = copy.StringMap(d.Router.Routes)
for i, v := range d.vars { data.vars.Transfer(&d.vars)
data.vars[i].merged = v.merged
}
return data return data
} }
func (d *Config) init() { func (d *Config) init() {
d.val(newInt64Value(&d.Version, version), "version", "", nil, "Configuration file layout version", true, false) d.vars.Register(value.NewInt64(&d.Version, version), "version", "", nil, "Configuration file layout version", true, false)
d.val(newTimeValue(&d.CreatedAt, time.Now()), "created_at", "", nil, "Configuration file creation time", false, false) d.vars.Register(value.NewTime(&d.CreatedAt, time.Now()), "created_at", "", nil, "Configuration file creation time", false, false)
d.val(newStringValue(&d.ID, uuid.New().String()), "id", "CORE_ID", nil, "ID for this instance", true, false) d.vars.Register(value.NewString(&d.ID, uuid.New().String()), "id", "CORE_ID", nil, "ID for this instance", true, false)
d.val(newStringValue(&d.Name, haikunator.New().Haikunate()), "name", "CORE_NAME", nil, "A human readable name for this instance", false, false) d.vars.Register(value.NewString(&d.Name, haikunator.New().Haikunate()), "name", "CORE_NAME", nil, "A human readable name for this instance", false, false)
d.val(newAddressValue(&d.Address, ":8080"), "address", "CORE_ADDRESS", nil, "HTTP listening address", false, false) d.vars.Register(value.NewAddress(&d.Address, ":8080"), "address", "CORE_ADDRESS", nil, "HTTP listening address", false, false)
d.val(newBoolValue(&d.CheckForUpdates, true), "update_check", "CORE_UPDATE_CHECK", nil, "Check for updates and send anonymized data", false, false) d.vars.Register(value.NewBool(&d.CheckForUpdates, true), "update_check", "CORE_UPDATE_CHECK", nil, "Check for updates and send anonymized data", false, false)
// Log // Log
d.val(newStringValue(&d.Log.Level, "info"), "log.level", "CORE_LOG_LEVEL", nil, "Loglevel: silent, error, warn, info, debug", false, false) d.vars.Register(value.NewString(&d.Log.Level, "info"), "log.level", "CORE_LOG_LEVEL", nil, "Loglevel: silent, error, warn, info, debug", false, false)
d.val(newStringListValue(&d.Log.Topics, []string{}, ","), "log.topics", "CORE_LOG_TOPICS", nil, "Show only selected log topics", false, false) d.vars.Register(value.NewStringList(&d.Log.Topics, []string{}, ","), "log.topics", "CORE_LOG_TOPICS", nil, "Show only selected log topics", false, false)
d.val(newIntValue(&d.Log.MaxLines, 1000), "log.max_lines", "CORE_LOG_MAXLINES", nil, "Number of latest log lines to keep in memory", false, false) d.vars.Register(value.NewInt(&d.Log.MaxLines, 1000), "log.max_lines", "CORE_LOG_MAX_LINES", []string{"CORE_LOG_MAXLINES"}, "Number of latest log lines to keep in memory", false, false)
// DB // DB
d.val(newMustDirValue(&d.DB.Dir, "./config"), "db.dir", "CORE_DB_DIR", nil, "Directory for holding the operational data", false, false) d.vars.Register(value.NewMustDir(&d.DB.Dir, "./config", d.fs), "db.dir", "CORE_DB_DIR", nil, "Directory for holding the operational data", false, false)
// Host // Host
d.val(newStringListValue(&d.Host.Name, []string{}, ","), "host.name", "CORE_HOST_NAME", nil, "Comma separated list of public host/domain names or IPs", false, false) d.vars.Register(value.NewStringList(&d.Host.Name, []string{}, ","), "host.name", "CORE_HOST_NAME", nil, "Comma separated list of public host/domain names or IPs", false, false)
d.val(newBoolValue(&d.Host.Auto, true), "host.auto", "CORE_HOST_AUTO", nil, "Enable detection of public IP addresses", false, false) d.vars.Register(value.NewBool(&d.Host.Auto, true), "host.auto", "CORE_HOST_AUTO", nil, "Enable detection of public IP addresses", false, false)
// API // API
d.val(newBoolValue(&d.API.ReadOnly, false), "api.read_only", "CORE_API_READ_ONLY", nil, "Allow only ready only access to the API", false, false) d.vars.Register(value.NewBool(&d.API.ReadOnly, false), "api.read_only", "CORE_API_READ_ONLY", nil, "Allow only ready only access to the API", false, false)
d.val(newCIDRListValue(&d.API.Access.HTTP.Allow, []string{}, ","), "api.access.http.allow", "CORE_API_ACCESS_HTTP_ALLOW", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false) d.vars.Register(value.NewCIDRList(&d.API.Access.HTTP.Allow, []string{}, ","), "api.access.http.allow", "CORE_API_ACCESS_HTTP_ALLOW", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false)
d.val(newCIDRListValue(&d.API.Access.HTTP.Block, []string{}, ","), "api.access.http.block", "CORE_API_ACCESS_HTTP_BLOCK", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false) d.vars.Register(value.NewCIDRList(&d.API.Access.HTTP.Block, []string{}, ","), "api.access.http.block", "CORE_API_ACCESS_HTTP_BLOCK", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false)
d.val(newCIDRListValue(&d.API.Access.HTTPS.Allow, []string{}, ","), "api.access.https.allow", "CORE_API_ACCESS_HTTPS_ALLOW", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false) d.vars.Register(value.NewCIDRList(&d.API.Access.HTTPS.Allow, []string{}, ","), "api.access.https.allow", "CORE_API_ACCESS_HTTPS_ALLOW", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false)
d.val(newCIDRListValue(&d.API.Access.HTTPS.Block, []string{}, ","), "api.access.https.block", "CORE_API_ACCESS_HTTPS_BLOCK", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false) d.vars.Register(value.NewCIDRList(&d.API.Access.HTTPS.Block, []string{}, ","), "api.access.https.block", "CORE_API_ACCESS_HTTPS_BLOCK", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false)
d.val(newBoolValue(&d.API.Auth.Enable, false), "api.auth.enable", "CORE_API_AUTH_ENABLE", nil, "Enable authentication for all clients", false, false) d.vars.Register(value.NewBool(&d.API.Auth.Enable, false), "api.auth.enable", "CORE_API_AUTH_ENABLE", nil, "Enable authentication for all clients", false, false)
d.val(newBoolValue(&d.API.Auth.DisableLocalhost, false), "api.auth.disable_localhost", "CORE_API_AUTH_DISABLE_LOCALHOST", nil, "Disable authentication for clients from localhost", false, false) d.vars.Register(value.NewBool(&d.API.Auth.DisableLocalhost, false), "api.auth.disable_localhost", "CORE_API_AUTH_DISABLE_LOCALHOST", nil, "Disable authentication for clients from localhost", false, false)
d.val(newStringValue(&d.API.Auth.Username, ""), "api.auth.username", "CORE_API_AUTH_USERNAME", []string{"RS_USERNAME"}, "Username", false, false) d.vars.Register(value.NewString(&d.API.Auth.Username, ""), "api.auth.username", "CORE_API_AUTH_USERNAME", []string{"RS_USERNAME"}, "Username", false, false)
d.val(newStringValue(&d.API.Auth.Password, ""), "api.auth.password", "CORE_API_AUTH_PASSWORD", []string{"RS_PASSWORD"}, "Password", false, true) d.vars.Register(value.NewString(&d.API.Auth.Password, ""), "api.auth.password", "CORE_API_AUTH_PASSWORD", []string{"RS_PASSWORD"}, "Password", false, true)
// Auth JWT // Auth JWT
d.val(newStringValue(&d.API.Auth.JWT.Secret, rand.String(32)), "api.auth.jwt.secret", "CORE_API_AUTH_JWT_SECRET", nil, "JWT secret, leave empty for generating a random value", false, true) d.vars.Register(value.NewString(&d.API.Auth.JWT.Secret, rand.String(32)), "api.auth.jwt.secret", "CORE_API_AUTH_JWT_SECRET", nil, "JWT secret, leave empty for generating a random value", false, true)
// Auth Auth0 // Auth Auth0
d.val(newBoolValue(&d.API.Auth.Auth0.Enable, false), "api.auth.auth0.enable", "CORE_API_AUTH_AUTH0_ENABLE", nil, "Enable Auth0", false, false) d.vars.Register(value.NewBool(&d.API.Auth.Auth0.Enable, false), "api.auth.auth0.enable", "CORE_API_AUTH_AUTH0_ENABLE", nil, "Enable Auth0", false, false)
d.val(newTenantListValue(&d.API.Auth.Auth0.Tenants, []Auth0Tenant{}, ","), "api.auth.auth0.tenants", "CORE_API_AUTH_AUTH0_TENANTS", nil, "List of Auth0 tenants", false, false) d.vars.Register(value.NewTenantList(&d.API.Auth.Auth0.Tenants, []value.Auth0Tenant{}, ","), "api.auth.auth0.tenants", "CORE_API_AUTH_AUTH0_TENANTS", nil, "List of Auth0 tenants", false, false)
// TLS // TLS
d.val(newAddressValue(&d.TLS.Address, ":8181"), "tls.address", "CORE_TLS_ADDRESS", nil, "HTTPS listening address", false, false) d.vars.Register(value.NewAddress(&d.TLS.Address, ":8181"), "tls.address", "CORE_TLS_ADDRESS", nil, "HTTPS listening address", false, false)
d.val(newBoolValue(&d.TLS.Enable, false), "tls.enable", "CORE_TLS_ENABLE", nil, "Enable HTTPS", false, false) d.vars.Register(value.NewBool(&d.TLS.Enable, false), "tls.enable", "CORE_TLS_ENABLE", nil, "Enable HTTPS", false, false)
d.val(newBoolValue(&d.TLS.Auto, false), "tls.auto", "CORE_TLS_AUTO", nil, "Enable Let's Encrypt certificate", false, false) d.vars.Register(value.NewBool(&d.TLS.Auto, false), "tls.auto", "CORE_TLS_AUTO", nil, "Enable Let's Encrypt certificate", false, false)
d.val(newEmailValue(&d.TLS.Email, "cert@datarhei.com"), "tls.email", "CORE_TLS_EMAIL", nil, "Email for Let's Encrypt registration", false, false) d.vars.Register(value.NewEmail(&d.TLS.Email, "cert@datarhei.com"), "tls.email", "CORE_TLS_EMAIL", nil, "Email for Let's Encrypt registration", false, false)
d.val(newFileValue(&d.TLS.CertFile, ""), "tls.cert_file", "CORE_TLS_CERTFILE", nil, "Path to certificate file in PEM format", false, false) d.vars.Register(value.NewFile(&d.TLS.CertFile, "", d.fs), "tls.cert_file", "CORE_TLS_CERT_FILE", []string{"CORE_TLS_CERTFILE"}, "Path to certificate file in PEM format", false, false)
d.val(newFileValue(&d.TLS.KeyFile, ""), "tls.key_file", "CORE_TLS_KEYFILE", nil, "Path to key file in PEM format", false, false) d.vars.Register(value.NewFile(&d.TLS.KeyFile, "", d.fs), "tls.key_file", "CORE_TLS_KEY_FILE", []string{"CORE_TLS_KEYFILE"}, "Path to key file in PEM format", false, false)
// Storage // Storage
d.val(newFileValue(&d.Storage.MimeTypes, "./mime.types"), "storage.mimetypes_file", "CORE_STORAGE_MIMETYPES_FILE", []string{"CORE_MIMETYPES_FILE"}, "Path to file with mime-types", false, false) d.vars.Register(value.NewFile(&d.Storage.MimeTypes, "./mime.types", d.fs), "storage.mimetypes_file", "CORE_STORAGE_MIMETYPES_FILE", []string{"CORE_MIMETYPES_FILE"}, "Path to file with mime-types", false, false)
// Storage (Disk) // Storage (Disk)
d.val(newMustDirValue(&d.Storage.Disk.Dir, "./data"), "storage.disk.dir", "CORE_STORAGE_DISK_DIR", nil, "Directory on disk, exposed on /", false, false) d.vars.Register(value.NewMustDir(&d.Storage.Disk.Dir, "./data", d.fs), "storage.disk.dir", "CORE_STORAGE_DISK_DIR", nil, "Directory on disk, exposed on /", false, false)
d.val(newInt64Value(&d.Storage.Disk.Size, 0), "storage.disk.max_size_mbytes", "CORE_STORAGE_DISK_MAXSIZEMBYTES", nil, "Max. allowed megabytes for storage.disk.dir, 0 for unlimited", false, false) d.vars.Register(value.NewInt64(&d.Storage.Disk.Size, 0), "storage.disk.max_size_mbytes", "CORE_STORAGE_DISK_MAX_SIZE_MBYTES", []string{"CORE_STORAGE_DISK_MAXSIZEMBYTES"}, "Max. allowed megabytes for storage.disk.dir, 0 for unlimited", false, false)
d.val(newBoolValue(&d.Storage.Disk.Cache.Enable, true), "storage.disk.cache.enable", "CORE_STORAGE_DISK_CACHE_ENABLE", nil, "Enable cache for /", false, false) d.vars.Register(value.NewBool(&d.Storage.Disk.Cache.Enable, true), "storage.disk.cache.enable", "CORE_STORAGE_DISK_CACHE_ENABLE", nil, "Enable cache for /", false, false)
d.val(newUint64Value(&d.Storage.Disk.Cache.Size, 0), "storage.disk.cache.max_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXSIZEMBYTES", nil, "Max. allowed cache size, 0 for unlimited", false, false) d.vars.Register(value.NewUint64(&d.Storage.Disk.Cache.Size, 0), "storage.disk.cache.max_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAX_SIZE_MBYTES", []string{"CORE_STORAGE_DISK_CACHE_MAXSIZEMBYTES"}, "Max. allowed cache size, 0 for unlimited", false, false)
d.val(newInt64Value(&d.Storage.Disk.Cache.TTL, 300), "storage.disk.cache.ttl_seconds", "CORE_STORAGE_DISK_CACHE_TTLSECONDS", nil, "Seconds to keep files in cache", false, false) d.vars.Register(value.NewInt64(&d.Storage.Disk.Cache.TTL, 300), "storage.disk.cache.ttl_seconds", "CORE_STORAGE_DISK_CACHE_TTL_SECONDS", []string{"CORE_STORAGE_DISK_CACHE_TTLSECONDS"}, "Seconds to keep files in cache", false, false)
d.val(newUint64Value(&d.Storage.Disk.Cache.FileSize, 1), "storage.disk.cache.max_file_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXFILESIZEMBYTES", nil, "Max. file size to put in cache", false, false) d.vars.Register(value.NewUint64(&d.Storage.Disk.Cache.FileSize, 1), "storage.disk.cache.max_file_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAX_FILE_SIZE_MBYTES", []string{"CORE_STORAGE_DISK_CACHE_MAXFILESIZEMBYTES"}, "Max. file size to put in cache", false, false)
d.val(newStringListValue(&d.Storage.Disk.Cache.Types.Allow, []string{}, " "), "storage.disk.cache.type.allow", "CORE_STORAGE_DISK_CACHE_TYPES_ALLOW", []string{"CORE_STORAGE_DISK_CACHE_TYPES"}, "File extensions to cache, empty for all", false, false) d.vars.Register(value.NewStringList(&d.Storage.Disk.Cache.Types.Allow, []string{}, " "), "storage.disk.cache.type.allow", "CORE_STORAGE_DISK_CACHE_TYPES_ALLOW", []string{"CORE_STORAGE_DISK_CACHE_TYPES"}, "File extensions to cache, empty for all", false, false)
d.val(newStringListValue(&d.Storage.Disk.Cache.Types.Block, []string{".m3u8", ".mpd"}, " "), "storage.disk.cache.type.block", "CORE_STORAGE_DISK_CACHE_TYPES_BLOCK", nil, "File extensions not to cache, empty for none", false, false) d.vars.Register(value.NewStringList(&d.Storage.Disk.Cache.Types.Block, []string{".m3u8", ".mpd"}, " "), "storage.disk.cache.type.block", "CORE_STORAGE_DISK_CACHE_TYPES_BLOCK", nil, "File extensions not to cache, empty for none", false, false)
// Storage (Memory) // Storage (Memory)
d.val(newBoolValue(&d.Storage.Memory.Auth.Enable, true), "storage.memory.auth.enable", "CORE_STORAGE_MEMORY_AUTH_ENABLE", nil, "Enable basic auth for PUT,POST, and DELETE on /memfs", false, false) d.vars.Register(value.NewBool(&d.Storage.Memory.Auth.Enable, true), "storage.memory.auth.enable", "CORE_STORAGE_MEMORY_AUTH_ENABLE", nil, "Enable basic auth for PUT,POST, and DELETE on /memfs", false, false)
d.val(newStringValue(&d.Storage.Memory.Auth.Username, "admin"), "storage.memory.auth.username", "CORE_STORAGE_MEMORY_AUTH_USERNAME", nil, "Username for Basic-Auth of /memfs", false, false) d.vars.Register(value.NewString(&d.Storage.Memory.Auth.Username, "admin"), "storage.memory.auth.username", "CORE_STORAGE_MEMORY_AUTH_USERNAME", nil, "Username for Basic-Auth of /memfs", false, false)
d.val(newStringValue(&d.Storage.Memory.Auth.Password, rand.StringAlphanumeric(18)), "storage.memory.auth.password", "CORE_STORAGE_MEMORY_AUTH_PASSWORD", nil, "Password for Basic-Auth of /memfs", false, true) d.vars.Register(value.NewString(&d.Storage.Memory.Auth.Password, rand.StringAlphanumeric(18)), "storage.memory.auth.password", "CORE_STORAGE_MEMORY_AUTH_PASSWORD", nil, "Password for Basic-Auth of /memfs", false, true)
d.val(newInt64Value(&d.Storage.Memory.Size, 0), "storage.memory.max_size_mbytes", "CORE_STORAGE_MEMORY_MAXSIZEMBYTES", nil, "Max. allowed megabytes for /memfs, 0 for unlimited", false, false) d.vars.Register(value.NewInt64(&d.Storage.Memory.Size, 0), "storage.memory.max_size_mbytes", "CORE_STORAGE_MEMORY_MAX_SIZE_MBYTES", []string{"CORE_STORAGE_MEMORY_MAXSIZEMBYTES"}, "Max. allowed megabytes for /memfs, 0 for unlimited", false, false)
d.val(newBoolValue(&d.Storage.Memory.Purge, false), "storage.memory.purge", "CORE_STORAGE_MEMORY_PURGE", nil, "Automatically remove the oldest files if /memfs is full", false, false) d.vars.Register(value.NewBool(&d.Storage.Memory.Purge, false), "storage.memory.purge", "CORE_STORAGE_MEMORY_PURGE", nil, "Automatically remove the oldest files if /memfs is full", false, false)
// Storage (S3)
d.vars.Register(value.NewS3StorageListValue(&d.Storage.S3, []value.S3Storage{}, "|"), "storage.s3", "CORE_STORAGE_S3", nil, "List of S3 storage URLS", false, false)
// Storage (CORS) // Storage (CORS)
d.val(newCORSOriginsValue(&d.Storage.CORS.Origins, []string{"*"}, ","), "storage.cors.origins", "CORE_STORAGE_CORS_ORIGINS", nil, "Allowed CORS origins for /memfs and /data", false, false) d.vars.Register(value.NewCORSOrigins(&d.Storage.CORS.Origins, []string{"*"}, ","), "storage.cors.origins", "CORE_STORAGE_CORS_ORIGINS", nil, "Allowed CORS origins for /memfs and /data", false, false)
// RTMP // RTMP
d.val(newBoolValue(&d.RTMP.Enable, false), "rtmp.enable", "CORE_RTMP_ENABLE", nil, "Enable RTMP server", false, false) d.vars.Register(value.NewBool(&d.RTMP.Enable, false), "rtmp.enable", "CORE_RTMP_ENABLE", nil, "Enable RTMP server", false, false)
d.val(newBoolValue(&d.RTMP.EnableTLS, false), "rtmp.enable_tls", "CORE_RTMP_ENABLE_TLS", nil, "Enable RTMPS server instead of RTMP", false, false) d.vars.Register(value.NewBool(&d.RTMP.EnableTLS, false), "rtmp.enable_tls", "CORE_RTMP_ENABLE_TLS", nil, "Enable RTMPS server instead of RTMP", false, false)
d.val(newAddressValue(&d.RTMP.Address, ":1935"), "rtmp.address", "CORE_RTMP_ADDRESS", nil, "RTMP server listen address", false, false) d.vars.Register(value.NewAddress(&d.RTMP.Address, ":1935"), "rtmp.address", "CORE_RTMP_ADDRESS", nil, "RTMP server listen address", false, false)
d.val(newAddressValue(&d.RTMP.AddressTLS, ":1936"), "rtmp.address_tls", "CORE_RTMP_ADDRESS_TLS", nil, "RTMPS server listen address", false, false) d.vars.Register(value.NewAddress(&d.RTMP.AddressTLS, ":1936"), "rtmp.address_tls", "CORE_RTMP_ADDRESS_TLS", nil, "RTMPS server listen address", false, false)
d.val(newAbsolutePathValue(&d.RTMP.App, "/"), "rtmp.app", "CORE_RTMP_APP", nil, "RTMP app for publishing", false, false) d.vars.Register(value.NewAbsolutePath(&d.RTMP.App, "/"), "rtmp.app", "CORE_RTMP_APP", nil, "RTMP app for publishing", false, false)
d.val(newStringValue(&d.RTMP.Token, ""), "rtmp.token", "CORE_RTMP_TOKEN", nil, "RTMP token for publishing and playing", false, true) d.vars.Register(value.NewString(&d.RTMP.Token, ""), "rtmp.token", "CORE_RTMP_TOKEN", nil, "RTMP token for publishing and playing", false, true)
// SRT // SRT
d.val(newBoolValue(&d.SRT.Enable, false), "srt.enable", "CORE_SRT_ENABLE", nil, "Enable SRT server", false, false) d.vars.Register(value.NewBool(&d.SRT.Enable, false), "srt.enable", "CORE_SRT_ENABLE", nil, "Enable SRT server", false, false)
d.val(newAddressValue(&d.SRT.Address, ":6000"), "srt.address", "CORE_SRT_ADDRESS", nil, "SRT server listen address", false, false) d.vars.Register(value.NewAddress(&d.SRT.Address, ":6000"), "srt.address", "CORE_SRT_ADDRESS", nil, "SRT server listen address", false, false)
d.val(newStringValue(&d.SRT.Passphrase, ""), "srt.passphrase", "CORE_SRT_PASSPHRASE", nil, "SRT encryption passphrase", false, true) d.vars.Register(value.NewString(&d.SRT.Passphrase, ""), "srt.passphrase", "CORE_SRT_PASSPHRASE", nil, "SRT encryption passphrase", false, true)
d.val(newStringValue(&d.SRT.Token, ""), "srt.token", "CORE_SRT_TOKEN", nil, "SRT token for publishing and playing", false, true) d.vars.Register(value.NewString(&d.SRT.Token, ""), "srt.token", "CORE_SRT_TOKEN", nil, "SRT token for publishing and playing", false, true)
d.val(newBoolValue(&d.SRT.Log.Enable, false), "srt.log.enable", "CORE_SRT_LOG_ENABLE", nil, "Enable SRT server logging", false, false) d.vars.Register(value.NewBool(&d.SRT.Log.Enable, false), "srt.log.enable", "CORE_SRT_LOG_ENABLE", nil, "Enable SRT server logging", false, false)
d.val(newStringListValue(&d.SRT.Log.Topics, []string{}, ","), "srt.log.topics", "CORE_SRT_LOG_TOPICS", nil, "List of topics to log", false, false) d.vars.Register(value.NewStringList(&d.SRT.Log.Topics, []string{}, ","), "srt.log.topics", "CORE_SRT_LOG_TOPICS", nil, "List of topics to log", false, false)
// FFmpeg // FFmpeg
d.val(newExecValue(&d.FFmpeg.Binary, "ffmpeg"), "ffmpeg.binary", "CORE_FFMPEG_BINARY", nil, "Path to ffmpeg binary", true, false) d.vars.Register(value.NewExec(&d.FFmpeg.Binary, "ffmpeg", d.fs), "ffmpeg.binary", "CORE_FFMPEG_BINARY", nil, "Path to ffmpeg binary", true, false)
d.val(newInt64Value(&d.FFmpeg.MaxProcesses, 0), "ffmpeg.max_processes", "CORE_FFMPEG_MAXPROCESSES", nil, "Max. allowed simultaneously running ffmpeg instances, 0 for unlimited", false, false) d.vars.Register(value.NewInt64(&d.FFmpeg.MaxProcesses, 0), "ffmpeg.max_processes", "CORE_FFMPEG_MAXPROCESSES", nil, "Max. allowed simultaneously running ffmpeg instances, 0 for unlimited", false, false)
d.val(newStringListValue(&d.FFmpeg.Access.Input.Allow, []string{}, " "), "ffmpeg.access.input.allow", "CORE_FFMPEG_ACCESS_INPUT_ALLOW", nil, "List of allowed expression to match against the input addresses", false, false) d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Input.Allow, []string{}, " "), "ffmpeg.access.input.allow", "CORE_FFMPEG_ACCESS_INPUT_ALLOW", nil, "List of allowed expression to match against the input addresses", false, false)
d.val(newStringListValue(&d.FFmpeg.Access.Input.Block, []string{}, " "), "ffmpeg.access.input.block", "CORE_FFMPEG_ACCESS_INPUT_BLOCK", nil, "List of blocked expression to match against the input addresses", false, false) d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Input.Block, []string{}, " "), "ffmpeg.access.input.block", "CORE_FFMPEG_ACCESS_INPUT_BLOCK", nil, "List of blocked expression to match against the input addresses", false, false)
d.val(newStringListValue(&d.FFmpeg.Access.Output.Allow, []string{}, " "), "ffmpeg.access.output.allow", "CORE_FFMPEG_ACCESS_OUTPUT_ALLOW", nil, "List of allowed expression to match against the output addresses", false, false) d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Output.Allow, []string{}, " "), "ffmpeg.access.output.allow", "CORE_FFMPEG_ACCESS_OUTPUT_ALLOW", nil, "List of allowed expression to match against the output addresses", false, false)
d.val(newStringListValue(&d.FFmpeg.Access.Output.Block, []string{}, " "), "ffmpeg.access.output.block", "CORE_FFMPEG_ACCESS_OUTPUT_BLOCK", nil, "List of blocked expression to match against the output addresses", false, false) d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Output.Block, []string{}, " "), "ffmpeg.access.output.block", "CORE_FFMPEG_ACCESS_OUTPUT_BLOCK", nil, "List of blocked expression to match against the output addresses", false, false)
d.val(newIntValue(&d.FFmpeg.Log.MaxLines, 50), "ffmpeg.log.max_lines", "CORE_FFMPEG_LOG_MAXLINES", nil, "Number of latest log lines to keep for each process", false, false) d.vars.Register(value.NewInt(&d.FFmpeg.Log.MaxLines, 50), "ffmpeg.log.max_lines", "CORE_FFMPEG_LOG_MAX_LINES", []string{"CORE_FFMPEG_LOG_MAXLINES"}, "Number of latest log lines to keep for each process", false, false)
d.val(newIntValue(&d.FFmpeg.Log.MaxHistory, 3), "ffmpeg.log.max_history", "CORE_FFMPEG_LOG_MAXHISTORY", nil, "Number of latest logs to keep for each process", false, false) d.vars.Register(value.NewInt(&d.FFmpeg.Log.MaxHistory, 3), "ffmpeg.log.max_history", "CORE_FFMPEG_LOG_MAX_HISTORY", []string{"CORE_FFMPEG_LOG_MAXHISTORY"}, "Number of latest logs to keep for each process", false, false)
// Playout // Playout
d.val(newBoolValue(&d.Playout.Enable, false), "playout.enable", "CORE_PLAYOUT_ENABLE", nil, "Enable playout proxy where available", false, false) d.vars.Register(value.NewBool(&d.Playout.Enable, false), "playout.enable", "CORE_PLAYOUT_ENABLE", nil, "Enable playout proxy where available", false, false)
d.val(newPortValue(&d.Playout.MinPort, 0), "playout.min_port", "CORE_PLAYOUT_MINPORT", nil, "Min. playout server port", false, false) d.vars.Register(value.NewPort(&d.Playout.MinPort, 0), "playout.min_port", "CORE_PLAYOUT_MIN_PORT", []string{"CORE_PLAYOUT_MINPORT"}, "Min. playout server port", false, false)
d.val(newPortValue(&d.Playout.MaxPort, 0), "playout.max_port", "CORE_PLAYOUT_MAXPORT", nil, "Max. playout server port", false, false) d.vars.Register(value.NewPort(&d.Playout.MaxPort, 0), "playout.max_port", "CORE_PLAYOUT_MAX_PORT", []string{"CORE_PLAYOUT_MAXPORT"}, "Max. playout server port", false, false)
// Debug // Debug
d.val(newBoolValue(&d.Debug.Profiling, false), "debug.profiling", "CORE_DEBUG_PROFILING", nil, "Enable profiling endpoint on /profiling", false, false) d.vars.Register(value.NewBool(&d.Debug.Profiling, false), "debug.profiling", "CORE_DEBUG_PROFILING", nil, "Enable profiling endpoint on /profiling", false, false)
d.val(newIntValue(&d.Debug.ForceGC, 0), "debug.force_gc", "CORE_DEBUG_FORCEGC", nil, "Number of seconds between forcing GC to return memory to the OS", false, false) d.vars.Register(value.NewInt(&d.Debug.ForceGC, 0), "debug.force_gc", "CORE_DEBUG_FORCE_GC", []string{"CORE_DEBUG_FORCEGC"}, "Number of seconds between forcing GC to return memory to the OS", false, false)
d.vars.Register(value.NewInt64(&d.Debug.MemoryLimit, 0), "debug.memory_limit_mbytes", "CORE_DEBUG_MEMORY_LIMIT_MBYTES", nil, "Impose a soft memory limit for the core, in megabytes", false, false)
// Metrics // Metrics
d.val(newBoolValue(&d.Metrics.Enable, false), "metrics.enable", "CORE_METRICS_ENABLE", nil, "Enable collecting historic metrics data", false, false) d.vars.Register(value.NewBool(&d.Metrics.Enable, false), "metrics.enable", "CORE_METRICS_ENABLE", nil, "Enable collecting historic metrics data", false, false)
d.val(newBoolValue(&d.Metrics.EnablePrometheus, false), "metrics.enable_prometheus", "CORE_METRICS_ENABLE_PROMETHEUS", nil, "Enable prometheus endpoint /metrics", false, false) d.vars.Register(value.NewBool(&d.Metrics.EnablePrometheus, false), "metrics.enable_prometheus", "CORE_METRICS_ENABLE_PROMETHEUS", nil, "Enable prometheus endpoint /metrics", false, false)
d.val(newInt64Value(&d.Metrics.Range, 300), "metrics.range_seconds", "CORE_METRICS_RANGE_SECONDS", nil, "Seconds to keep history data", false, false) d.vars.Register(value.NewInt64(&d.Metrics.Range, 300), "metrics.range_seconds", "CORE_METRICS_RANGE_SECONDS", nil, "Seconds to keep history data", false, false)
d.val(newInt64Value(&d.Metrics.Interval, 2), "metrics.interval_seconds", "CORE_METRICS_INTERVAL_SECONDS", nil, "Interval for collecting metrics", false, false) d.vars.Register(value.NewInt64(&d.Metrics.Interval, 2), "metrics.interval_seconds", "CORE_METRICS_INTERVAL_SECONDS", nil, "Interval for collecting metrics", false, false)
// Sessions // Sessions
d.val(newBoolValue(&d.Sessions.Enable, true), "sessions.enable", "CORE_SESSIONS_ENABLE", nil, "Enable collecting HLS session stats for /memfs", false, false) d.vars.Register(value.NewBool(&d.Sessions.Enable, true), "sessions.enable", "CORE_SESSIONS_ENABLE", nil, "Enable collecting HLS session stats for /memfs", false, false)
d.val(newCIDRListValue(&d.Sessions.IPIgnoreList, []string{"127.0.0.1/32", "::1/128"}, ","), "sessions.ip_ignorelist", "CORE_SESSIONS_IP_IGNORELIST", nil, "List of IP ranges in CIDR notation to ignore", false, false) d.vars.Register(value.NewCIDRList(&d.Sessions.IPIgnoreList, []string{"127.0.0.1/32", "::1/128"}, ","), "sessions.ip_ignorelist", "CORE_SESSIONS_IP_IGNORELIST", nil, "List of IP ranges in CIDR notation to ignore", false, false)
d.val(newIntValue(&d.Sessions.SessionTimeout, 30), "sessions.session_timeout_sec", "CORE_SESSIONS_SESSION_TIMEOUT_SEC", nil, "Timeout for an idle session", false, false) d.vars.Register(value.NewInt(&d.Sessions.SessionTimeout, 30), "sessions.session_timeout_sec", "CORE_SESSIONS_SESSION_TIMEOUT_SEC", nil, "Timeout for an idle session", false, false)
d.val(newBoolValue(&d.Sessions.Persist, false), "sessions.persist", "CORE_SESSIONS_PERSIST", nil, "Whether to persist session history. Will be stored as sessions.json in db.dir", false, false) d.vars.Register(value.NewBool(&d.Sessions.Persist, false), "sessions.persist", "CORE_SESSIONS_PERSIST", nil, "Whether to persist session history. Will be stored as sessions.json in db.dir", false, false)
d.val(newIntValue(&d.Sessions.PersistInterval, 300), "sessions.persist_interval_sec", "CORE_SESSIONS_PERSIST_INTERVAL_SEC", nil, "Interval in seconds in which to persist the current session history", false, false) d.vars.Register(value.NewInt(&d.Sessions.PersistInterval, 300), "sessions.persist_interval_sec", "CORE_SESSIONS_PERSIST_INTERVAL_SEC", nil, "Interval in seconds in which to persist the current session history", false, false)
d.val(newUint64Value(&d.Sessions.MaxBitrate, 0), "sessions.max_bitrate_mbit", "CORE_SESSIONS_MAXBITRATE_MBIT", nil, "Max. allowed outgoing bitrate in mbit/s, 0 for unlimited", false, false) d.vars.Register(value.NewUint64(&d.Sessions.MaxBitrate, 0), "sessions.max_bitrate_mbit", "CORE_SESSIONS_MAXBITRATE_MBIT", nil, "Max. allowed outgoing bitrate in mbit/s, 0 for unlimited", false, false)
d.val(newUint64Value(&d.Sessions.MaxSessions, 0), "sessions.max_sessions", "CORE_SESSIONS_MAXSESSIONS", nil, "Max. allowed number of simultaneous sessions, 0 for unlimited", false, false) d.vars.Register(value.NewUint64(&d.Sessions.MaxSessions, 0), "sessions.max_sessions", "CORE_SESSIONS_MAX_SESSIONS", []string{"CORE_SESSIONS_MAXSESSIONS"}, "Max. allowed number of simultaneous sessions, 0 for unlimited", false, false)
// Service // Service
d.val(newBoolValue(&d.Service.Enable, false), "service.enable", "CORE_SERVICE_ENABLE", nil, "Enable connecting to the Restreamer Service", false, false) d.vars.Register(value.NewBool(&d.Service.Enable, false), "service.enable", "CORE_SERVICE_ENABLE", nil, "Enable connecting to the Restreamer Service", false, false)
d.val(newStringValue(&d.Service.Token, ""), "service.token", "CORE_SERVICE_TOKEN", nil, "Restreamer Service account token", false, true) d.vars.Register(value.NewString(&d.Service.Token, ""), "service.token", "CORE_SERVICE_TOKEN", nil, "Restreamer Service account token", false, true)
d.val(newURLValue(&d.Service.URL, "https://service.datarhei.com"), "service.url", "CORE_SERVICE_URL", nil, "URL of the Restreamer Service", false, false) d.vars.Register(value.NewURL(&d.Service.URL, "https://service.datarhei.com"), "service.url", "CORE_SERVICE_URL", nil, "URL of the Restreamer Service", false, false)
// Router // Router
d.val(newStringListValue(&d.Router.BlockedPrefixes, []string{"/api"}, ","), "router.blocked_prefixes", "CORE_ROUTER_BLOCKED_PREFIXES", nil, "List of path prefixes that can't be routed", false, false) d.vars.Register(value.NewStringList(&d.Router.BlockedPrefixes, []string{"/api"}, ","), "router.blocked_prefixes", "CORE_ROUTER_BLOCKED_PREFIXES", nil, "List of path prefixes that can't be routed", false, false)
d.val(newStringMapStringValue(&d.Router.Routes, nil), "router.routes", "CORE_ROUTER_ROUTES", nil, "List of route mappings", false, false) d.vars.Register(value.NewStringMapString(&d.Router.Routes, nil), "router.routes", "CORE_ROUTER_ROUTES", nil, "List of route mappings", false, false)
d.val(newDirValue(&d.Router.UIPath, ""), "router.ui_path", "CORE_ROUTER_UI_PATH", nil, "Path to a directory holding UI files mounted as /ui", false, false) d.vars.Register(value.NewDir(&d.Router.UIPath, "", d.fs), "router.ui_path", "CORE_ROUTER_UI_PATH", nil, "Path to a directory holding UI files mounted as /ui", false, false)
}
func (d *Config) val(val value, name, envName string, envAltNames []string, description string, required, disguise bool) {
d.vars = append(d.vars, &variable{
value: val,
defVal: val.String(),
name: name,
envName: envName,
envAltNames: envAltNames,
description: description,
required: required,
disguise: disguise,
})
}
func (d *Config) log(level string, v *variable, format string, args ...interface{}) {
variable := Variable{
Value: v.value.String(),
Name: v.name,
EnvName: v.envName,
Description: v.description,
Merged: v.merged,
}
if v.disguise {
variable.Value = "***"
}
l := message{
message: fmt.Sprintf(format, args...),
variable: variable,
level: level,
}
d.logs = append(d.logs, l)
}
// Merge merges the values of the known environment variables into the configuration
func (d *Config) Merge() {
for _, v := range d.vars {
if len(v.envName) == 0 {
continue
}
var envval string
var ok bool
envval, ok = os.LookupEnv(v.envName)
if !ok {
foundAltName := false
for _, envName := range v.envAltNames {
envval, ok = os.LookupEnv(envName)
if ok {
foundAltName = true
d.log("warn", v, "deprecated name, please use %s", v.envName)
break
}
}
if !foundAltName {
continue
}
}
err := v.value.Set(envval)
if err != nil {
d.log("error", v, "%s", err.Error())
}
v.merged = true
}
} }
// Validate validates the current state of the Config for completeness and sanity. Errors are // Validate validates the current state of the Config for completeness and sanity. Errors are
// written to the log. Use resetLogs to indicate to reset the logs prior validation. // written to the log. Use resetLogs to indicate to reset the logs prior validation.
func (d *Config) Validate(resetLogs bool) { func (d *Config) Validate(resetLogs bool) {
if resetLogs { if resetLogs {
d.logs = nil d.vars.ResetLogs()
} }
if d.Version != version { if d.Version != version {
d.log("error", d.findVariable("version"), "unknown configuration layout version (found version %d, expecting version %d)", d.Version, version) d.vars.Log("error", "version", "unknown configuration layout version (found version %d, expecting version %d)", d.Version, version)
return return
} }
for _, v := range d.vars { d.vars.Validate()
d.log("info", v, "%s", "")
err := v.value.Validate()
if err != nil {
d.log("error", v, "%s", err.Error())
}
if v.required && v.value.IsEmpty() {
d.log("error", v, "a value is required")
}
}
// Individual sanity checks // Individual sanity checks
// If HTTP Auth is enabled, check that the username and password are set // If HTTP Auth is enabled, check that the username and password are set
if d.API.Auth.Enable { if d.API.Auth.Enable {
if len(d.API.Auth.Username) == 0 || len(d.API.Auth.Password) == 0 { if len(d.API.Auth.Username) == 0 || len(d.API.Auth.Password) == 0 {
d.log("error", d.findVariable("api.auth.enable"), "api.auth.username and api.auth.password must be set") d.vars.Log("error", "api.auth.enable", "api.auth.username and api.auth.password must be set")
} }
} }
// If Auth0 is enabled, check that domain, audience, and clientid are set // If Auth0 is enabled, check that domain, audience, and clientid are set
if d.API.Auth.Auth0.Enable { if d.API.Auth.Auth0.Enable {
if len(d.API.Auth.Auth0.Tenants) == 0 { if len(d.API.Auth.Auth0.Tenants) == 0 {
d.log("error", d.findVariable("api.auth.auth0.enable"), "at least one tenants must be set") d.vars.Log("error", "api.auth.auth0.enable", "at least one tenants must be set")
} }
for i, t := range d.API.Auth.Auth0.Tenants { for i, t := range d.API.Auth.Auth0.Tenants {
if len(t.Domain) == 0 || len(t.Audience) == 0 || len(t.ClientID) == 0 { if len(t.Domain) == 0 || len(t.Audience) == 0 || len(t.ClientID) == 0 {
d.log("error", d.findVariable("api.auth.auth0.tenants"), "domain, audience, and clientid must be set (tenant %d)", i) d.vars.Log("error", "api.auth.auth0.tenants", "domain, audience, and clientid must be set (tenant %d)", i)
} }
} }
} }
@@ -387,14 +313,14 @@ func (d *Config) Validate(resetLogs bool) {
// If TLS is enabled and Let's Encrypt is disabled, require certfile and keyfile // If TLS is enabled and Let's Encrypt is disabled, require certfile and keyfile
if d.TLS.Enable && !d.TLS.Auto { if d.TLS.Enable && !d.TLS.Auto {
if len(d.TLS.CertFile) == 0 || len(d.TLS.KeyFile) == 0 { if len(d.TLS.CertFile) == 0 || len(d.TLS.KeyFile) == 0 {
d.log("error", d.findVariable("tls.enable"), "tls.certfile and tls.keyfile must be set") d.vars.Log("error", "tls.enable", "tls.certfile and tls.keyfile must be set")
} }
} }
// If TLS and Let's Encrypt certificate is enabled, we require a public hostname // If TLS and Let's Encrypt certificate is enabled, we require a public hostname
if d.TLS.Enable && d.TLS.Auto { if d.TLS.Enable && d.TLS.Auto {
if len(d.Host.Name) == 0 { if len(d.Host.Name) == 0 {
d.log("error", d.findVariable("host.name"), "a hostname must be set in order to get an automatic TLS certificate") d.vars.Log("error", "host.name", "a hostname must be set in order to get an automatic TLS certificate")
} else { } else {
r := &net.Resolver{ r := &net.Resolver{
PreferGo: true, PreferGo: true,
@@ -404,7 +330,7 @@ func (d *Config) Validate(resetLogs bool) {
for _, host := range d.Host.Name { for _, host := range d.Host.Name {
// Don't lookup IP addresses // Don't lookup IP addresses
if ip := net.ParseIP(host); ip != nil { if ip := net.ParseIP(host); ip != nil {
d.log("error", d.findVariable("host.name"), "only host names are allowed if automatic TLS is enabled, but found IP address: %s", host) d.vars.Log("error", "host.name", "only host names are allowed if automatic TLS is enabled, but found IP address: %s", host)
} }
// Lookup host name with a timeout // Lookup host name with a timeout
@@ -412,7 +338,7 @@ func (d *Config) Validate(resetLogs bool) {
_, err := r.LookupHost(ctx, host) _, err := r.LookupHost(ctx, host)
if err != nil { if err != nil {
d.log("error", d.findVariable("host.name"), "the host '%s' can't be resolved and will not work with automatic TLS", host) d.vars.Log("error", "host.name", "the host '%s' can't be resolved and will not work with automatic TLS", host)
} }
cancel() cancel()
@@ -423,32 +349,31 @@ func (d *Config) Validate(resetLogs bool) {
// If TLS and Let's Encrypt certificate is enabled, we require a non-empty email address // If TLS and Let's Encrypt certificate is enabled, we require a non-empty email address
if d.TLS.Enable && d.TLS.Auto { if d.TLS.Enable && d.TLS.Auto {
if len(d.TLS.Email) == 0 { if len(d.TLS.Email) == 0 {
v := d.findVariable("tls.email") d.vars.SetDefault("tls.email")
v.value.Set(v.defVal)
} }
} }
// If TLS for RTMP is enabled, TLS must be enabled // If TLS for RTMP is enabled, TLS must be enabled
if d.RTMP.EnableTLS { if d.RTMP.EnableTLS {
if !d.RTMP.Enable { if !d.RTMP.Enable {
d.log("error", d.findVariable("rtmp.enable"), "RTMP server must be enabled if RTMPS server is enabled") d.vars.Log("error", "rtmp.enable", "RTMP server must be enabled if RTMPS server is enabled")
} }
if !d.TLS.Enable { if !d.TLS.Enable {
d.log("error", d.findVariable("rtmp.enable_tls"), "RTMPS server can only be enabled if TLS is enabled") d.vars.Log("error", "rtmp.enable_tls", "RTMPS server can only be enabled if TLS is enabled")
} }
if len(d.RTMP.AddressTLS) == 0 { if len(d.RTMP.AddressTLS) == 0 {
d.log("error", d.findVariable("rtmp.address_tls"), "RTMPS server address must be set") d.vars.Log("error", "rtmp.address_tls", "RTMPS server address must be set")
} }
if d.RTMP.Enable && d.RTMP.Address == d.RTMP.AddressTLS { if d.RTMP.Enable && d.RTMP.Address == d.RTMP.AddressTLS {
d.log("error", d.findVariable("rtmp.address"), "The RTMP and RTMPS server can't listen on the same address") d.vars.Log("error", "rtmp.address", "The RTMP and RTMPS server can't listen on the same address")
} }
} }
// If CORE_MEMFS_USERNAME and CORE_MEMFS_PASSWORD are set, automatically active/deactivate Basic-Auth for memfs // If CORE_MEMFS_USERNAME and CORE_MEMFS_PASSWORD are set, automatically active/deactivate Basic-Auth for memfs
if d.findVariable("storage.memory.auth.username").merged && d.findVariable("storage.memory.auth.password").merged { if d.vars.IsMerged("storage.memory.auth.username") && d.vars.IsMerged("storage.memory.auth.password") {
d.Storage.Memory.Auth.Enable = true d.Storage.Memory.Auth.Enable = true
if len(d.Storage.Memory.Auth.Username) == 0 && len(d.Storage.Memory.Auth.Password) == 0 { if len(d.Storage.Memory.Auth.Username) == 0 && len(d.Storage.Memory.Auth.Password) == 0 {
@@ -459,121 +384,91 @@ func (d *Config) Validate(resetLogs bool) {
// If Basic-Auth for memfs is enable, check that the username and password are set // If Basic-Auth for memfs is enable, check that the username and password are set
if d.Storage.Memory.Auth.Enable { if d.Storage.Memory.Auth.Enable {
if len(d.Storage.Memory.Auth.Username) == 0 || len(d.Storage.Memory.Auth.Password) == 0 { if len(d.Storage.Memory.Auth.Username) == 0 || len(d.Storage.Memory.Auth.Password) == 0 {
d.log("error", d.findVariable("storage.memory.auth.enable"), "storage.memory.auth.username and storage.memory.auth.password must be set") d.vars.Log("error", "storage.memory.auth.enable", "storage.memory.auth.username and storage.memory.auth.password must be set")
}
}
if len(d.Storage.S3) != 0 {
names := map[string]struct{}{
"disk": {},
"mem": {},
}
for _, s3 := range d.Storage.S3 {
if _, ok := names[s3.Name]; ok {
d.vars.Log("error", "storage.s3", "the name %s is already in use or reserved", s3.Name)
}
names[s3.Name] = struct{}{}
} }
} }
// If playout is enabled, check that the port range is sane // If playout is enabled, check that the port range is sane
if d.Playout.Enable { if d.Playout.Enable {
if d.Playout.MinPort >= d.Playout.MaxPort { if d.Playout.MinPort >= d.Playout.MaxPort {
d.log("error", d.findVariable("playout.min_port"), "must be bigger than playout.max_port") d.vars.Log("error", "playout.min_port", "must be bigger than playout.max_port")
} }
} }
// If cache is enabled, a valid TTL has to be set to a useful value // If cache is enabled, a valid TTL has to be set to a useful value
if d.Storage.Disk.Cache.Enable && d.Storage.Disk.Cache.TTL < 0 { if d.Storage.Disk.Cache.Enable && d.Storage.Disk.Cache.TTL < 0 {
d.log("error", d.findVariable("storage.disk.cache.ttl_seconds"), "must be equal or greater than 0") d.vars.Log("error", "storage.disk.cache.ttl_seconds", "must be equal or greater than 0")
} }
// If the stats are enabled, the session timeout has to be set to a useful value // If the stats are enabled, the session timeout has to be set to a useful value
if d.Sessions.Enable && d.Sessions.SessionTimeout < 1 { if d.Sessions.Enable && d.Sessions.SessionTimeout < 1 {
d.log("error", d.findVariable("stats.session_timeout_sec"), "must be equal or greater than 1") d.vars.Log("error", "stats.session_timeout_sec", "must be equal or greater than 1")
} }
// If the stats and their persistence are enabled, the persist interval has to be set to a useful value // If the stats and their persistence are enabled, the persist interval has to be set to a useful value
if d.Sessions.Enable && d.Sessions.PersistInterval < 0 { if d.Sessions.Enable && d.Sessions.PersistInterval < 0 {
d.log("error", d.findVariable("stats.persist_interval_sec"), "must be at equal or greater than 0") d.vars.Log("error", "stats.persist_interval_sec", "must be at equal or greater than 0")
} }
// If the service is enabled, the token and enpoint have to be defined // If the service is enabled, the token and enpoint have to be defined
if d.Service.Enable { if d.Service.Enable {
if len(d.Service.Token) == 0 { if len(d.Service.Token) == 0 {
d.log("error", d.findVariable("service.token"), "must be non-empty") d.vars.Log("error", "service.token", "must be non-empty")
} }
if len(d.Service.URL) == 0 { if len(d.Service.URL) == 0 {
d.log("error", d.findVariable("service.url"), "must be non-empty") d.vars.Log("error", "service.url", "must be non-empty")
} }
} }
// If historic metrics are enabled, the timerange and interval have to be valid // If historic metrics are enabled, the timerange and interval have to be valid
if d.Metrics.Enable { if d.Metrics.Enable {
if d.Metrics.Range <= 0 { if d.Metrics.Range <= 0 {
d.log("error", d.findVariable("metrics.range"), "must be greater 0") d.vars.Log("error", "metrics.range", "must be greater 0")
} }
if d.Metrics.Interval <= 0 { if d.Metrics.Interval <= 0 {
d.log("error", d.findVariable("metrics.interval"), "must be greater 0") d.vars.Log("error", "metrics.interval", "must be greater 0")
} }
if d.Metrics.Interval > d.Metrics.Range { if d.Metrics.Interval > d.Metrics.Range {
d.log("error", d.findVariable("metrics.interval"), "must be smaller than the range") d.vars.Log("error", "metrics.interval", "must be smaller than the range")
} }
} }
} }
func (d *Config) findVariable(name string) *variable { // Merge merges the values of the known environment variables into the configuration
for _, v := range d.vars { func (d *Config) Merge() {
if v.name == name { d.vars.Merge()
return v
}
}
return nil
} }
// Messages calls for each log entry the provided callback. The level has the values 'error', 'warn', or 'info'. // Messages calls for each log entry the provided callback. The level has the values 'error', 'warn', or 'info'.
// The name is the name of the configuration value, e.g. 'api.auth.enable'. The message is the log message. // The name is the name of the configuration value, e.g. 'api.auth.enable'. The message is the log message.
func (d *Config) Messages(logger func(level string, v Variable, message string)) { func (d *Config) Messages(logger func(level string, v vars.Variable, message string)) {
for _, l := range d.logs { d.vars.Messages(logger)
logger(l.level, l.variable, l.message)
}
} }
// HasErrors returns whether there are some error messages in the log. // HasErrors returns whether there are some error messages in the log.
func (d *Config) HasErrors() bool { func (d *Config) HasErrors() bool {
for _, l := range d.logs { return d.vars.HasErrors()
if l.level == "error" {
return true
}
}
return false
} }
// Overrides returns a list of configuration value names that have been overriden by an environment variable. // Overrides returns a list of configuration value names that have been overriden by an environment variable.
func (d *Config) Overrides() []string { func (d *Config) Overrides() []string {
overrides := []string{} return d.vars.Overrides()
for _, v := range d.vars {
if v.merged {
overrides = append(overrides, v.name)
}
}
return overrides
}
func copyStringSlice(src []string) []string {
dst := make([]string, len(src))
copy(dst, src)
return dst
}
func copyStringMap(src map[string]string) map[string]string {
dst := make(map[string]string)
for k, v := range src {
dst[k] = v
}
return dst
}
func copyTenantSlice(src []Auth0Tenant) []Auth0Tenant {
dst := make([]Auth0Tenant, len(src))
copy(dst, src)
return dst
} }

View File

@@ -1,55 +1,84 @@
package config package config
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/io/fs"
"github.com/stretchr/testify/require"
) )
func TestConfigCopy(t *testing.T) { func TestConfigCopy(t *testing.T) {
config1 := New() fs, _ := fs.NewMemFilesystem(fs.MemConfig{})
config1 := New(fs)
config1.Version = 42 config1.Version = 42
config1.DB.Dir = "foo" config1.DB.Dir = "foo"
val1 := config1.findVariable("version") val1, _ := config1.Get("version")
val2 := config1.findVariable("db.dir") val2, _ := config1.Get("db.dir")
val3 := config1.findVariable("host.name") val3, _ := config1.Get("host.name")
assert.Equal(t, "42", val1.value.String()) require.Equal(t, "42", val1)
assert.Equal(t, nil, val1.value.Validate()) require.Equal(t, "foo", val2)
assert.Equal(t, false, val1.value.IsEmpty()) require.Equal(t, "(empty)", val3)
assert.Equal(t, "foo", val2.value.String()) config1.Set("host.name", "foo.com")
assert.Equal(t, "(empty)", val3.value.String()) val3, _ = config1.Get("host.name")
require.Equal(t, "foo.com", val3)
val3.value.Set("foo.com") config2 := config1.Clone()
assert.Equal(t, "foo.com", val3.value.String()) require.Equal(t, int64(42), config2.Version)
require.Equal(t, "foo", config2.DB.Dir)
require.Equal(t, []string{"foo.com"}, config2.Host.Name)
config2 := NewConfigFrom(config1) config1.Set("version", "77")
assert.Equal(t, int64(42), config2.Version) require.Equal(t, int64(77), config1.Version)
assert.Equal(t, "foo", config2.DB.Dir) require.Equal(t, int64(42), config2.Version)
assert.Equal(t, []string{"foo.com"}, config2.Host.Name)
val1.value.Set("77") config1.Set("db.dir", "bar")
assert.Equal(t, int64(77), config1.Version) require.Equal(t, "bar", config1.DB.Dir)
assert.Equal(t, int64(42), config2.Version) require.Equal(t, "foo", config2.DB.Dir)
val2.value.Set("bar")
assert.Equal(t, "bar", config1.DB.Dir)
assert.Equal(t, "foo", config2.DB.Dir)
config2.DB.Dir = "baz" config2.DB.Dir = "baz"
assert.Equal(t, "bar", config1.DB.Dir) require.Equal(t, "bar", config1.DB.Dir)
assert.Equal(t, "baz", config2.DB.Dir) require.Equal(t, "baz", config2.DB.Dir)
config1.Host.Name[0] = "bar.com" config1.Host.Name[0] = "bar.com"
assert.Equal(t, []string{"bar.com"}, config1.Host.Name) require.Equal(t, []string{"bar.com"}, config1.Host.Name)
assert.Equal(t, []string{"foo.com"}, config2.Host.Name) require.Equal(t, []string{"foo.com"}, config2.Host.Name)
}
func TestValidateDefault(t *testing.T) {
fs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
size, fresh, err := fs.WriteFileReader("./mime.types", strings.NewReader("xxxxx"))
require.Equal(t, int64(5), size)
require.Equal(t, true, fresh)
require.NoError(t, err)
_, _, err = fs.WriteFileReader("/bin/ffmpeg", strings.NewReader("xxxxx"))
require.NoError(t, err)
cfg := New(fs)
cfg.Validate(true)
errors := []string{}
cfg.Messages(func(level string, v vars.Variable, message string) {
if level == "error" {
errors = append(errors, message)
}
})
require.Equal(t, 0, len(cfg.Overrides()))
require.Equal(t, false, cfg.HasErrors(), errors)
} }

30
config/copy/copy.go Normal file
View File

@@ -0,0 +1,30 @@
package copy
import "github.com/datarhei/core/v16/config/value"
func StringMap(src map[string]string) map[string]string {
dst := make(map[string]string)
for k, v := range src {
dst[k] = v
}
return dst
}
func TenantSlice(src []value.Auth0Tenant) []value.Auth0Tenant {
dst := Slice(src)
for i, t := range src {
dst[i].Users = Slice(t.Users)
}
return dst
}
func Slice[T any](src []T) []T {
dst := make([]T, len(src))
copy(dst, src)
return dst
}

View File

@@ -1,13 +1,20 @@
package config package config
import "time" import (
"time"
"github.com/datarhei/core/v16/config/copy"
v2 "github.com/datarhei/core/v16/config/v2"
"github.com/datarhei/core/v16/config/value"
"github.com/datarhei/core/v16/io/fs"
)
// Data is the actual configuration data for the app // Data is the actual configuration data for the app
type Data struct { type Data struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"` // When this config has been persisted
LoadedAt time.Time `json:"-"` LoadedAt time.Time `json:"-"` // When this config has been actually used
UpdatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` // Irrelevant
Version int64 `json:"version" jsonschema:"minimum=3,maximum=3"` Version int64 `json:"version" jsonschema:"minimum=3,maximum=3" format:"int64"`
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Address string `json:"address"` Address string `json:"address"`
@@ -15,7 +22,7 @@ type Data struct {
Log struct { Log struct {
Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"` Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"`
Topics []string `json:"topics"` Topics []string `json:"topics"`
MaxLines int `json:"max_lines"` MaxLines int `json:"max_lines" format:"int"`
} `json:"log"` } `json:"log"`
DB struct { DB struct {
Dir string `json:"dir"` Dir string `json:"dir"`
@@ -46,7 +53,7 @@ type Data struct {
} `json:"jwt"` } `json:"jwt"`
Auth0 struct { Auth0 struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Tenants []Auth0Tenant `json:"tenants"` Tenants []value.Auth0Tenant `json:"tenants"`
} `json:"auth0"` } `json:"auth0"`
} `json:"auth"` } `json:"auth"`
} `json:"api"` } `json:"api"`
@@ -61,12 +68,12 @@ type Data struct {
Storage struct { Storage struct {
Disk struct { Disk struct {
Dir string `json:"dir"` Dir string `json:"dir"`
Size int64 `json:"max_size_mbytes"` Size int64 `json:"max_size_mbytes" format:"int64"`
Cache struct { Cache struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Size uint64 `json:"max_size_mbytes"` Size uint64 `json:"max_size_mbytes" format:"uint64"`
TTL int64 `json:"ttl_seconds"` TTL int64 `json:"ttl_seconds" format:"int64"`
FileSize uint64 `json:"max_file_size_mbytes"` FileSize uint64 `json:"max_file_size_mbytes" format:"uint64"`
Types struct { Types struct {
Allow []string `json:"allow"` Allow []string `json:"allow"`
Block []string `json:"block"` Block []string `json:"block"`
@@ -79,9 +86,10 @@ type Data struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} `json:"auth"` } `json:"auth"`
Size int64 `json:"max_size_mbytes"` Size int64 `json:"max_size_mbytes" format:"int64"`
Purge bool `json:"purge"` Purge bool `json:"purge"`
} `json:"memory"` } `json:"memory"`
S3 []value.S3Storage `json:"s3"`
CORS struct { CORS struct {
Origins []string `json:"origins"` Origins []string `json:"origins"`
} `json:"cors"` } `json:"cors"`
@@ -107,7 +115,7 @@ type Data struct {
} `json:"srt"` } `json:"srt"`
FFmpeg struct { FFmpeg struct {
Binary string `json:"binary"` Binary string `json:"binary"`
MaxProcesses int64 `json:"max_processes"` MaxProcesses int64 `json:"max_processes" format:"int64"`
Access struct { Access struct {
Input struct { Input struct {
Allow []string `json:"allow"` Allow []string `json:"allow"`
@@ -119,33 +127,34 @@ type Data struct {
} `json:"output"` } `json:"output"`
} `json:"access"` } `json:"access"`
Log struct { Log struct {
MaxLines int `json:"max_lines"` MaxLines int `json:"max_lines" format:"int"`
MaxHistory int `json:"max_history"` MaxHistory int `json:"max_history" format:"int"`
} `json:"log"` } `json:"log"`
} `json:"ffmpeg"` } `json:"ffmpeg"`
Playout struct { Playout struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
MinPort int `json:"min_port"` MinPort int `json:"min_port" format:"int"`
MaxPort int `json:"max_port"` MaxPort int `json:"max_port" format:"int"`
} `json:"playout"` } `json:"playout"`
Debug struct { Debug struct {
Profiling bool `json:"profiling"` Profiling bool `json:"profiling"`
ForceGC int `json:"force_gc"` ForceGC int `json:"force_gc" format:"int"`
MemoryLimit int64 `json:"memory_limit_mbytes" format:"int64"`
} `json:"debug"` } `json:"debug"`
Metrics struct { Metrics struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
EnablePrometheus bool `json:"enable_prometheus"` EnablePrometheus bool `json:"enable_prometheus"`
Range int64 `json:"range_sec"` // seconds Range int64 `json:"range_sec" format:"int64"` // seconds
Interval int64 `json:"interval_sec"` // seconds Interval int64 `json:"interval_sec" format:"int64"` // seconds
} `json:"metrics"` } `json:"metrics"`
Sessions struct { Sessions struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
IPIgnoreList []string `json:"ip_ignorelist"` IPIgnoreList []string `json:"ip_ignorelist"`
SessionTimeout int `json:"session_timeout_sec"` SessionTimeout int `json:"session_timeout_sec" format:"int"`
Persist bool `json:"persist"` Persist bool `json:"persist"`
PersistInterval int `json:"persist_interval_sec"` PersistInterval int `json:"persist_interval_sec" format:"int"`
MaxBitrate uint64 `json:"max_bitrate_mbit"` MaxBitrate uint64 `json:"max_bitrate_mbit" format:"uint64"`
MaxSessions uint64 `json:"max_sessions"` MaxSessions uint64 `json:"max_sessions" format:"uint64"`
} `json:"sessions"` } `json:"sessions"`
Service struct { Service struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
@@ -159,8 +168,95 @@ type Data struct {
} `json:"router"` } `json:"router"`
} }
func NewV3FromV2(d *dataV2) (*Data, error) { func UpgradeV2ToV3(d *v2.Data, fs fs.Filesystem) (*Data, error) {
data := &Data{} cfg := New(fs)
return MergeV2toV3(&cfg.Data, d)
}
func MergeV2toV3(data *Data, d *v2.Data) (*Data, error) {
data.CreatedAt = d.CreatedAt
data.LoadedAt = d.LoadedAt
data.UpdatedAt = d.UpdatedAt
data.ID = d.ID
data.Name = d.Name
data.Address = d.Address
data.CheckForUpdates = d.CheckForUpdates
data.Log = d.Log
data.DB = d.DB
data.Host = d.Host
data.API = d.API
data.RTMP = d.RTMP
data.SRT = d.SRT
data.FFmpeg = d.FFmpeg
data.Playout = d.Playout
data.Metrics = d.Metrics
data.Sessions = d.Sessions
data.Service = d.Service
data.Router = d.Router
data.Log.Topics = copy.Slice(d.Log.Topics)
data.Host.Name = copy.Slice(d.Host.Name)
data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow)
data.API.Access.HTTP.Block = copy.Slice(d.API.Access.HTTP.Block)
data.API.Access.HTTPS.Allow = copy.Slice(d.API.Access.HTTPS.Allow)
data.API.Access.HTTPS.Block = copy.Slice(d.API.Access.HTTPS.Block)
data.API.Auth.Auth0.Tenants = copy.TenantSlice(d.API.Auth.Auth0.Tenants)
data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.FFmpeg.Access.Input.Allow = copy.Slice(d.FFmpeg.Access.Input.Allow)
data.FFmpeg.Access.Input.Block = copy.Slice(d.FFmpeg.Access.Input.Block)
data.FFmpeg.Access.Output.Allow = copy.Slice(d.FFmpeg.Access.Output.Allow)
data.FFmpeg.Access.Output.Block = copy.Slice(d.FFmpeg.Access.Output.Block)
data.Sessions.IPIgnoreList = copy.Slice(d.Sessions.IPIgnoreList)
data.SRT.Log.Topics = copy.Slice(d.SRT.Log.Topics)
data.Router.BlockedPrefixes = copy.Slice(d.Router.BlockedPrefixes)
data.Router.Routes = copy.StringMap(d.Router.Routes)
data.Storage.MimeTypes = d.Storage.MimeTypes
data.Storage.CORS = d.Storage.CORS
data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.Storage.Memory = d.Storage.Memory
// Actual changes
data.Debug.Profiling = d.Debug.Profiling
data.Debug.ForceGC = d.Debug.ForceGC
data.Debug.MemoryLimit = 0
data.TLS.Enable = d.TLS.Enable
data.TLS.Address = d.TLS.Address
data.TLS.Auto = d.TLS.Auto
data.TLS.CertFile = d.TLS.CertFile
data.TLS.KeyFile = d.TLS.KeyFile
data.Storage.Disk.Dir = d.Storage.Disk.Dir
data.Storage.Disk.Size = d.Storage.Disk.Size
data.Storage.Disk.Cache.Enable = d.Storage.Disk.Cache.Enable
data.Storage.Disk.Cache.Size = d.Storage.Disk.Cache.Size
data.Storage.Disk.Cache.FileSize = d.Storage.Disk.Cache.FileSize
data.Storage.Disk.Cache.TTL = d.Storage.Disk.Cache.TTL
data.Storage.Disk.Cache.Types.Allow = copy.Slice(d.Storage.Disk.Cache.Types)
data.Storage.S3 = []value.S3Storage{}
data.Version = 3
return data, nil
}
func DowngradeV3toV2(d *Data) (*v2.Data, error) {
data := &v2.Data{}
data.CreatedAt = d.CreatedAt data.CreatedAt = d.CreatedAt
data.LoadedAt = d.LoadedAt data.LoadedAt = d.LoadedAt
@@ -179,49 +275,50 @@ func NewV3FromV2(d *dataV2) (*Data, error) {
data.SRT = d.SRT data.SRT = d.SRT
data.FFmpeg = d.FFmpeg data.FFmpeg = d.FFmpeg
data.Playout = d.Playout data.Playout = d.Playout
data.Debug = d.Debug
data.Metrics = d.Metrics data.Metrics = d.Metrics
data.Sessions = d.Sessions data.Sessions = d.Sessions
data.Service = d.Service data.Service = d.Service
data.Router = d.Router data.Router = d.Router
data.Log.Topics = copyStringSlice(d.Log.Topics) data.Log.Topics = copy.Slice(d.Log.Topics)
data.Host.Name = copyStringSlice(d.Host.Name) data.Host.Name = copy.Slice(d.Host.Name)
data.API.Access.HTTP.Allow = copyStringSlice(d.API.Access.HTTP.Allow) data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow)
data.API.Access.HTTP.Block = copyStringSlice(d.API.Access.HTTP.Block) data.API.Access.HTTP.Block = copy.Slice(d.API.Access.HTTP.Block)
data.API.Access.HTTPS.Allow = copyStringSlice(d.API.Access.HTTPS.Allow) data.API.Access.HTTPS.Allow = copy.Slice(d.API.Access.HTTPS.Allow)
data.API.Access.HTTPS.Block = copyStringSlice(d.API.Access.HTTPS.Block) data.API.Access.HTTPS.Block = copy.Slice(d.API.Access.HTTPS.Block)
data.API.Auth.Auth0.Tenants = copyTenantSlice(d.API.Auth.Auth0.Tenants) data.API.Auth.Auth0.Tenants = copy.TenantSlice(d.API.Auth.Auth0.Tenants)
data.Storage.CORS.Origins = copyStringSlice(d.Storage.CORS.Origins) data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.FFmpeg.Access.Input.Allow = copyStringSlice(d.FFmpeg.Access.Input.Allow) data.FFmpeg.Access.Input.Allow = copy.Slice(d.FFmpeg.Access.Input.Allow)
data.FFmpeg.Access.Input.Block = copyStringSlice(d.FFmpeg.Access.Input.Block) data.FFmpeg.Access.Input.Block = copy.Slice(d.FFmpeg.Access.Input.Block)
data.FFmpeg.Access.Output.Allow = copyStringSlice(d.FFmpeg.Access.Output.Allow) data.FFmpeg.Access.Output.Allow = copy.Slice(d.FFmpeg.Access.Output.Allow)
data.FFmpeg.Access.Output.Block = copyStringSlice(d.FFmpeg.Access.Output.Block) data.FFmpeg.Access.Output.Block = copy.Slice(d.FFmpeg.Access.Output.Block)
data.Sessions.IPIgnoreList = copyStringSlice(d.Sessions.IPIgnoreList) data.Sessions.IPIgnoreList = copy.Slice(d.Sessions.IPIgnoreList)
data.SRT.Log.Topics = copyStringSlice(d.SRT.Log.Topics) data.SRT.Log.Topics = copy.Slice(d.SRT.Log.Topics)
data.Router.BlockedPrefixes = copyStringSlice(d.Router.BlockedPrefixes) data.Router.BlockedPrefixes = copy.Slice(d.Router.BlockedPrefixes)
data.Router.Routes = copyStringMap(d.Router.Routes) data.Router.Routes = copy.StringMap(d.Router.Routes)
// Actual changes // Actual changes
data.Debug.Profiling = d.Debug.Profiling
data.Debug.ForceGC = d.Debug.ForceGC
data.TLS.Enable = d.TLS.Enable data.TLS.Enable = d.TLS.Enable
data.TLS.Address = d.TLS.Address data.TLS.Address = d.TLS.Address
data.TLS.Auto = d.TLS.Auto data.TLS.Auto = d.TLS.Auto
data.TLS.CertFile = d.TLS.CertFile data.TLS.CertFile = d.TLS.CertFile
data.TLS.KeyFile = d.TLS.KeyFile data.TLS.KeyFile = d.TLS.KeyFile
data.TLS.Email = "cert@datarhei.com"
data.Storage.MimeTypes = d.Storage.MimeTypes data.Storage.MimeTypes = d.Storage.MimeTypes
data.Storage.CORS = d.Storage.CORS data.Storage.CORS = d.Storage.CORS
data.Storage.CORS.Origins = copyStringSlice(d.Storage.CORS.Origins) data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.Storage.Memory = d.Storage.Memory data.Storage.Memory = d.Storage.Memory
@@ -231,10 +328,9 @@ func NewV3FromV2(d *dataV2) (*Data, error) {
data.Storage.Disk.Cache.Size = d.Storage.Disk.Cache.Size data.Storage.Disk.Cache.Size = d.Storage.Disk.Cache.Size
data.Storage.Disk.Cache.FileSize = d.Storage.Disk.Cache.FileSize data.Storage.Disk.Cache.FileSize = d.Storage.Disk.Cache.FileSize
data.Storage.Disk.Cache.TTL = d.Storage.Disk.Cache.TTL data.Storage.Disk.Cache.TTL = d.Storage.Disk.Cache.TTL
data.Storage.Disk.Cache.Types.Allow = copyStringSlice(d.Storage.Disk.Cache.Types) data.Storage.Disk.Cache.Types = copy.Slice(d.Storage.Disk.Cache.Types.Allow)
data.Storage.Disk.Cache.Types.Block = []string{}
data.Version = 3 data.Version = 2
return data, nil return data, nil
} }

36
config/data_test.go Normal file
View File

@@ -0,0 +1,36 @@
package config
import (
"testing"
v2 "github.com/datarhei/core/v16/config/v2"
"github.com/datarhei/core/v16/io/fs"
"github.com/stretchr/testify/require"
)
func TestUpgrade(t *testing.T) {
fs, _ := fs.NewMemFilesystem(fs.MemConfig{})
v2cfg := v2.New(fs)
v2cfg.Storage.Disk.Cache.Types = []string{".foo", ".bar"}
v3cfg, err := UpgradeV2ToV3(&v2cfg.Data, fs)
require.NoError(t, err)
require.Equal(t, int64(3), v3cfg.Version)
require.ElementsMatch(t, []string{".foo", ".bar"}, v3cfg.Storage.Disk.Cache.Types.Allow)
require.ElementsMatch(t, []string{".m3u8", ".mpd"}, v3cfg.Storage.Disk.Cache.Types.Block)
}
func TestDowngrade(t *testing.T) {
fs, _ := fs.NewMemFilesystem(fs.MemConfig{})
v3cfg := New(fs)
v3cfg.Storage.Disk.Cache.Types.Allow = []string{".foo", ".bar"}
v2cfg, err := DowngradeV3toV2(&v3cfg.Data)
require.NoError(t, err)
require.Equal(t, int64(2), v2cfg.Version)
require.ElementsMatch(t, []string{".foo", ".bar"}, v2cfg.Storage.Disk.Cache.Types)
}

View File

@@ -1,83 +0,0 @@
package config
import "fmt"
type dummyStore struct {
current *Config
active *Config
}
// NewDummyStore returns a store that returns the default config
func NewDummyStore() Store {
s := &dummyStore{}
cfg := New()
cfg.DB.Dir = "."
cfg.FFmpeg.Binary = "true"
cfg.Storage.Disk.Dir = "."
cfg.Storage.MimeTypes = ""
s.current = cfg
cfg = New()
cfg.DB.Dir = "."
cfg.FFmpeg.Binary = "true"
cfg.Storage.Disk.Dir = "."
cfg.Storage.MimeTypes = ""
s.active = cfg
return s
}
func (c *dummyStore) Get() *Config {
cfg := New()
cfg.DB.Dir = "."
cfg.FFmpeg.Binary = "true"
cfg.Storage.Disk.Dir = "."
cfg.Storage.MimeTypes = ""
return cfg
}
func (c *dummyStore) Set(d *Config) error {
d.Validate(true)
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
c.current = NewConfigFrom(d)
return nil
}
func (c *dummyStore) GetActive() *Config {
cfg := New()
cfg.DB.Dir = "."
cfg.FFmpeg.Binary = "true"
cfg.Storage.Disk.Dir = "."
cfg.Storage.MimeTypes = ""
return cfg
}
func (c *dummyStore) SetActive(d *Config) error {
d.Validate(true)
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
c.active = NewConfigFrom(d)
return nil
}
func (c *dummyStore) Reload() error {
return nil
}

View File

@@ -1,71 +0,0 @@
package config
import (
"io"
"net/http"
"sync"
"time"
)
// SetPublicIPs will try to figure out the public IPs (v4 and v6)
// we're running on. There's a timeout of max. 5 seconds to do it.
// If it fails, the IPs will simply not be set.
func (d *Config) SetPublicIPs() {
var wg sync.WaitGroup
ipv4 := ""
ipv6 := ""
wg.Add(2)
go func() {
defer wg.Done()
ipv4 = doRequest("https://api.ipify.org")
}()
go func() {
defer wg.Done()
ipv6 = doRequest("https://api6.ipify.org")
}()
wg.Wait()
if len(ipv4) != 0 {
d.Host.Name = append(d.Host.Name, ipv4)
}
if len(ipv6) != 0 && ipv4 != ipv6 {
d.Host.Name = append(d.Host.Name, ipv6)
}
}
func doRequest(url string) string {
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return ""
}
resp, err := client.Do(req)
if err != nil {
return ""
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return ""
}
if resp.StatusCode != 200 {
return ""
}
return string(body)
}

View File

@@ -1,201 +0,0 @@
package config
import (
gojson "encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/io/file"
)
type jsonStore struct {
path string
data map[string]*Config
reloadFn func()
}
// NewJSONStore will read a JSON config file from the given path. After successfully reading it in, it will be written
// back to the path. The returned error will be nil if everything went fine.
// If the path doesn't exist, a default JSON config file will be written to that path.
// The returned ConfigStore can be used to retrieve or write the config.
func NewJSONStore(path string, reloadFn func()) (Store, error) {
c := &jsonStore{
path: path,
data: make(map[string]*Config),
reloadFn: reloadFn,
}
c.data["base"] = New()
if err := c.load(c.data["base"]); err != nil {
return nil, fmt.Errorf("failed to read JSON from '%s': %w", path, err)
}
if err := c.store(c.data["base"]); err != nil {
return nil, fmt.Errorf("failed to write JSON to '%s': %w", path, err)
}
return c, nil
}
func (c *jsonStore) Get() *Config {
return NewConfigFrom(c.data["base"])
}
func (c *jsonStore) Set(d *Config) error {
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
data := NewConfigFrom(d)
data.CreatedAt = time.Now()
if err := c.store(data); err != nil {
return fmt.Errorf("failed to write JSON to '%s': %w", c.path, err)
}
data.UpdatedAt = time.Now()
c.data["base"] = data
return nil
}
func (c *jsonStore) GetActive() *Config {
if x, ok := c.data["merged"]; ok {
return NewConfigFrom(x)
}
if x, ok := c.data["base"]; ok {
return NewConfigFrom(x)
}
return nil
}
func (c *jsonStore) SetActive(d *Config) error {
d.Validate(true)
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
c.data["merged"] = NewConfigFrom(d)
return nil
}
func (c *jsonStore) Reload() error {
if c.reloadFn == nil {
return nil
}
c.reloadFn()
return nil
}
func (c *jsonStore) load(config *Config) error {
if len(c.path) == 0 {
return nil
}
if _, err := os.Stat(c.path); os.IsNotExist(err) {
return nil
}
jsondata, err := os.ReadFile(c.path)
if err != nil {
return err
}
dataV3 := &Data{}
version := DataVersion{}
if err = gojson.Unmarshal(jsondata, &version); err != nil {
return json.FormatError(jsondata, err)
}
if version.Version == 1 {
dataV1 := &dataV1{}
if err = gojson.Unmarshal(jsondata, dataV1); err != nil {
return json.FormatError(jsondata, err)
}
dataV2, err := NewV2FromV1(dataV1)
if err != nil {
return err
}
dataV3, err = NewV3FromV2(dataV2)
if err != nil {
return err
}
} else if version.Version == 2 {
dataV2 := &dataV2{}
if err = gojson.Unmarshal(jsondata, dataV2); err != nil {
return json.FormatError(jsondata, err)
}
dataV3, err = NewV3FromV2(dataV2)
if err != nil {
return err
}
} else if version.Version == 3 {
if err = gojson.Unmarshal(jsondata, dataV3); err != nil {
return json.FormatError(jsondata, err)
}
}
config.Data = *dataV3
config.LoadedAt = time.Now()
config.UpdatedAt = config.LoadedAt
return nil
}
func (c *jsonStore) store(data *Config) error {
data.CreatedAt = time.Now()
if len(c.path) == 0 {
return nil
}
jsondata, err := gojson.MarshalIndent(data, "", " ")
if err != nil {
return err
}
dir, filename := filepath.Split(c.path)
tmpfile, err := os.CreateTemp(dir, filename)
if err != nil {
return err
}
defer os.Remove(tmpfile.Name())
if _, err := tmpfile.Write(jsondata); err != nil {
return err
}
if err := tmpfile.Close(); err != nil {
return err
}
if err := file.Rename(tmpfile.Name(), c.path); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,138 @@
{
"created_at": "2022-11-08T12:01:22.533279+01:00",
"version": 1,
"id": "c5ea4473-2f84-417c-a0c6-35746bfc9fc9",
"name": "cool-breeze-4646",
"address": ":8080",
"update_check": true,
"log": {
"level": "info",
"topics": [],
"max_lines": 1000
},
"db": {
"dir": "./config"
},
"host": {
"name": [],
"auto": true
},
"api": {
"read_only": false,
"access": {
"http": {
"allow": [],
"block": []
},
"https": {
"allow": [],
"block": []
}
},
"auth": {
"enable": false,
"disable_localhost": false,
"username": "",
"password": "",
"jwt": {
"secret": "L(*C[:uuHzL.]Fzpk$q=fa@PO=Z;j;56"
},
"auth0": {
"enable": false,
"tenants": []
}
}
},
"tls": {
"address": ":8181",
"enable": false,
"auto": false,
"cert_file": "",
"key_file": ""
},
"storage": {
"disk": {
"dir": "./data",
"max_size_mbytes": 0,
"cache": {
"enable": true,
"max_size_mbytes": 0,
"ttl_seconds": 300,
"max_file_size_mbytes": 1,
"types": []
}
},
"memory": {
"auth": {
"enable": true,
"username": "admin",
"password": "dcFsZVGwVFkv1bE8Rl"
},
"max_size_mbytes": 0,
"purge": false
},
"cors": {
"origins": [
"*"
]
},
"mimetypes_file": "./mime.types"
},
"ffmpeg": {
"binary": "ffmpeg",
"max_processes": 0,
"access": {
"input": {
"allow": [],
"block": []
},
"output": {
"allow": [],
"block": []
}
},
"log": {
"max_lines": 50,
"max_history": 3
}
},
"playout": {
"enable": false,
"min_port": 0,
"max_port": 0
},
"debug": {
"profiling": false,
"force_gc": 0
},
"metrics": {
"enable": false,
"enable_prometheus": false,
"range_sec": 300,
"interval_sec": 2
},
"sessions": {
"enable": true,
"ip_ignorelist": [
"127.0.0.1/32",
"::1/128"
],
"session_timeout_sec": 30,
"persist": false,
"persist_interval_sec": 300,
"max_bitrate_mbit": 0,
"max_sessions": 0
},
"service": {
"enable": false,
"token": "",
"url": "https://service.datarhei.com"
},
"router": {
"blocked_prefixes": [
"/api"
],
"routes": {},
"ui_path": ""
}
}

View File

@@ -0,0 +1,163 @@
{
"created_at": "2022-11-08T13:34:47.498911+01:00",
"version": 3,
"id": "c5ea4473-2f84-417c-a0c6-35746bfc9fc9",
"name": "cool-breeze-4646",
"address": ":8080",
"update_check": true,
"log": {
"level": "info",
"topics": [],
"max_lines": 1000
},
"db": {
"dir": "./config"
},
"host": {
"name": [],
"auto": true
},
"api": {
"read_only": false,
"access": {
"http": {
"allow": [],
"block": []
},
"https": {
"allow": [],
"block": []
}
},
"auth": {
"enable": false,
"disable_localhost": false,
"username": "",
"password": "",
"jwt": {
"secret": "L(*C[:uuHzL.]Fzpk$q=fa@PO=Z;j;56"
},
"auth0": {
"enable": false,
"tenants": []
}
}
},
"tls": {
"address": ":8181",
"enable": false,
"auto": false,
"email": "cert@datarhei.com",
"cert_file": "",
"key_file": ""
},
"storage": {
"disk": {
"dir": "./data",
"max_size_mbytes": 0,
"cache": {
"enable": true,
"max_size_mbytes": 0,
"ttl_seconds": 300,
"max_file_size_mbytes": 1,
"types": {
"allow": [],
"block": [
".m3u8",
".mpd"
]
}
}
},
"memory": {
"auth": {
"enable": true,
"username": "admin",
"password": "dcFsZVGwVFkv1bE8Rl"
},
"max_size_mbytes": 0,
"purge": false
},
"cors": {
"origins": [
"*"
]
},
"mimetypes_file": "./mime.types"
},
"rtmp": {
"enable": false,
"enable_tls": false,
"address": ":1935",
"address_tls": ":1936",
"app": "/",
"token": ""
},
"srt": {
"enable": false,
"address": ":6000",
"passphrase": "",
"token": "",
"log": {
"enable": false,
"topics": []
}
},
"ffmpeg": {
"binary": "ffmpeg",
"max_processes": 0,
"access": {
"input": {
"allow": [],
"block": []
},
"output": {
"allow": [],
"block": []
}
},
"log": {
"max_lines": 50,
"max_history": 3
}
},
"playout": {
"enable": false,
"min_port": 0,
"max_port": 0
},
"debug": {
"profiling": false,
"force_gc": 0
},
"metrics": {
"enable": false,
"enable_prometheus": false,
"range_sec": 300,
"interval_sec": 2
},
"sessions": {
"enable": true,
"ip_ignorelist": [
"127.0.0.1/32",
"::1/128"
],
"session_timeout_sec": 30,
"persist": false,
"persist_interval_sec": 300,
"max_bitrate_mbit": 0,
"max_sessions": 0
},
"service": {
"enable": false,
"token": "",
"url": "https://service.datarhei.com"
},
"router": {
"blocked_prefixes": [
"/api"
],
"routes": {},
"ui_path": ""
}
}

View File

@@ -0,0 +1,140 @@
{
"created_at": "2022-11-08T11:54:44.224213+01:00",
"version": 2,
"id": "3bddc061-e534-4315-ab56-95b48c050ec9",
"name": "super-frog-1715",
"address": ":8080",
"update_check": true,
"log": {
"level": "info",
"topics": [],
"max_lines": 1000
},
"db": {
"dir": "./config"
},
"host": {
"name": [],
"auto": true
},
"api": {
"read_only": false,
"access": {
"http": {
"allow": [],
"block": []
},
"https": {
"allow": [],
"block": []
}
},
"auth": {
"enable": false,
"disable_localhost": false,
"username": "",
"password": "",
"jwt": {
"secret": "u4+N,UDq]jGxGbbQLQN[!jcMsa\u0026weIJW"
},
"auth0": {
"enable": false,
"tenants": []
}
}
},
"tls": {
"address": ":8181",
"enable": false,
"auto": false,
"cert_file": "",
"key_file": ""
},
"storage": {
"disk": {
"dir": "./data",
"max_size_mbytes": 0,
"cache": {
"enable": true,
"max_size_mbytes": 0,
"ttl_seconds": 300,
"max_file_size_mbytes": 1,
"types": [
".ts"
]
}
},
"memory": {
"auth": {
"enable": true,
"username": "admin",
"password": "DsAKRUg9wmOk4qpvvy"
},
"max_size_mbytes": 0,
"purge": false
},
"cors": {
"origins": [
"*"
]
},
"mimetypes_file": "./mime.types"
},
"ffmpeg": {
"binary": "ffmpeg",
"max_processes": 0,
"access": {
"input": {
"allow": [],
"block": []
},
"output": {
"allow": [],
"block": []
}
},
"log": {
"max_lines": 50,
"max_history": 3
}
},
"playout": {
"enable": false,
"min_port": 0,
"max_port": 0
},
"debug": {
"profiling": false,
"force_gc": 0
},
"metrics": {
"enable": false,
"enable_prometheus": false,
"range_sec": 300,
"interval_sec": 2
},
"sessions": {
"enable": true,
"ip_ignorelist": [
"127.0.0.1/32",
"::1/128"
],
"session_timeout_sec": 30,
"persist": false,
"persist_interval_sec": 300,
"max_bitrate_mbit": 0,
"max_sessions": 0
},
"service": {
"enable": false,
"token": "",
"url": "https://service.datarhei.com"
},
"router": {
"blocked_prefixes": [
"/api"
],
"routes": {},
"ui_path": ""
}
}

View File

@@ -0,0 +1,165 @@
{
"created_at": "2022-11-08T11:54:44.224213+01:00",
"version": 3,
"id": "3bddc061-e534-4315-ab56-95b48c050ec9",
"name": "super-frog-1715",
"address": ":8080",
"update_check": true,
"log": {
"level": "info",
"topics": [],
"max_lines": 1000
},
"db": {
"dir": "./config"
},
"host": {
"name": [],
"auto": true
},
"api": {
"read_only": false,
"access": {
"http": {
"allow": [],
"block": []
},
"https": {
"allow": [],
"block": []
}
},
"auth": {
"enable": false,
"disable_localhost": false,
"username": "",
"password": "",
"jwt": {
"secret": "u4+N,UDq]jGxGbbQLQN[!jcMsa\u0026weIJW"
},
"auth0": {
"enable": false,
"tenants": []
}
}
},
"tls": {
"address": ":8181",
"enable": false,
"auto": false,
"cert_file": "",
"key_file": "",
"email": "cert@datarhei.com"
},
"storage": {
"disk": {
"dir": "./data",
"max_size_mbytes": 0,
"cache": {
"enable": true,
"max_size_mbytes": 0,
"ttl_seconds": 300,
"max_file_size_mbytes": 1,
"types": {
"allow": [
".ts"
],
"block": [
".m3u8",
".mpd"
]
}
}
},
"memory": {
"auth": {
"enable": true,
"username": "admin",
"password": "DsAKRUg9wmOk4qpvvy"
},
"max_size_mbytes": 0,
"purge": false
},
"cors": {
"origins": [
"*"
]
},
"mimetypes_file": "./mime.types"
},
"rtmp": {
"enable": false,
"enable_tls": false,
"address": ":1935",
"address_tls": ":1936",
"app": "/",
"token": ""
},
"srt": {
"enable": false,
"address": ":6000",
"passphrase": "",
"token": "",
"log": {
"enable": false,
"topics": []
}
},
"ffmpeg": {
"binary": "ffmpeg",
"max_processes": 0,
"access": {
"input": {
"allow": [],
"block": []
},
"output": {
"allow": [],
"block": []
}
},
"log": {
"max_lines": 50,
"max_history": 3
}
},
"playout": {
"enable": false,
"min_port": 0,
"max_port": 0
},
"debug": {
"profiling": false,
"force_gc": 0
},
"metrics": {
"enable": false,
"enable_prometheus": false,
"range_sec": 300,
"interval_sec": 2
},
"sessions": {
"enable": true,
"ip_ignorelist": [
"127.0.0.1/32",
"::1/128"
],
"session_timeout_sec": 30,
"persist": false,
"persist_interval_sec": 300,
"max_bitrate_mbit": 0,
"max_sessions": 0
},
"service": {
"enable": false,
"token": "",
"url": "https://service.datarhei.com"
},
"router": {
"blocked_prefixes": [
"/api"
],
"routes": {},
"ui_path": ""
}
}

214
config/store/json.go Normal file
View File

@@ -0,0 +1,214 @@
package store
import (
gojson "encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/datarhei/core/v16/config"
v1 "github.com/datarhei/core/v16/config/v1"
v2 "github.com/datarhei/core/v16/config/v2"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/io/fs"
)
type jsonStore struct {
fs fs.Filesystem
path string
data map[string]*config.Config
reloadFn func()
}
// NewJSONStore will read the JSON config file from the given path. After successfully reading it in, it will be written
// back to the path. The returned error will be nil if everything went fine. If the path doesn't exist, a default JSON
// config file will be written to that path. The returned ConfigStore can be used to retrieve or write the config.
func NewJSON(f fs.Filesystem, path string, reloadFn func()) (Store, error) {
c := &jsonStore{
fs: f,
data: make(map[string]*config.Config),
reloadFn: reloadFn,
}
path, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("failed to determine absolute path of '%s': %w", path, err)
}
c.path = path
if len(c.path) == 0 {
c.path = "/config.json"
}
if c.fs == nil {
return nil, fmt.Errorf("no valid filesystem provided")
}
c.data["base"] = config.New(f)
if err := c.load(c.data["base"]); err != nil {
return nil, fmt.Errorf("failed to read JSON from '%s': %w", path, err)
}
if err := c.store(c.data["base"]); err != nil {
return nil, fmt.Errorf("failed to write JSON to '%s': %w", path, err)
}
return c, nil
}
func (c *jsonStore) Get() *config.Config {
return c.data["base"].Clone()
}
func (c *jsonStore) Set(d *config.Config) error {
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
data := d.Clone()
if err := c.store(data); err != nil {
return fmt.Errorf("failed to write JSON to '%s': %w", c.path, err)
}
c.data["base"] = data
return nil
}
func (c *jsonStore) GetActive() *config.Config {
if x, ok := c.data["merged"]; ok {
return x.Clone()
}
if x, ok := c.data["base"]; ok {
return x.Clone()
}
return nil
}
func (c *jsonStore) SetActive(d *config.Config) error {
d.Validate(true)
if d.HasErrors() {
return fmt.Errorf("configuration data has errors after validation")
}
data := d.Clone()
c.data["merged"] = data
return nil
}
func (c *jsonStore) Reload() error {
if c.reloadFn == nil {
return nil
}
c.reloadFn()
return nil
}
func (c *jsonStore) load(cfg *config.Config) error {
if len(c.path) == 0 {
return nil
}
if _, err := c.fs.Stat(c.path); os.IsNotExist(err) {
return nil
}
jsondata, err := c.fs.ReadFile(c.path)
if err != nil {
return err
}
if len(jsondata) == 0 {
return nil
}
data, err := migrate(jsondata)
if err != nil {
return err
}
cfg.Data = *data
cfg.UpdatedAt = cfg.CreatedAt
return nil
}
func (c *jsonStore) store(data *config.Config) error {
if len(c.path) == 0 {
return nil
}
jsondata, err := gojson.MarshalIndent(data, "", " ")
if err != nil {
return err
}
_, _, err = c.fs.WriteFileSafe(c.path, jsondata)
return err
}
func migrate(jsondata []byte) (*config.Data, error) {
data := &config.Data{}
version := DataVersion{}
if err := gojson.Unmarshal(jsondata, &version); err != nil {
return nil, json.FormatError(jsondata, err)
}
if version.Version == 1 {
dataV1 := &v1.New(nil).Data
if err := gojson.Unmarshal(jsondata, dataV1); err != nil {
return nil, json.FormatError(jsondata, err)
}
dataV2, err := v2.UpgradeV1ToV2(dataV1, nil)
if err != nil {
return nil, err
}
dataV3, err := config.UpgradeV2ToV3(dataV2, nil)
if err != nil {
return nil, err
}
data = dataV3
} else if version.Version == 2 {
dataV2 := &v2.New(nil).Data
if err := gojson.Unmarshal(jsondata, dataV2); err != nil {
return nil, json.FormatError(jsondata, err)
}
dataV3, err := config.UpgradeV2ToV3(dataV2, nil)
if err != nil {
return nil, err
}
data = dataV3
} else if version.Version == 3 {
dataV3 := &config.New(nil).Data
if err := gojson.Unmarshal(jsondata, dataV3); err != nil {
return nil, json.FormatError(jsondata, err)
}
data = dataV3
}
return data, nil
}

50
config/store/json_test.go Normal file
View File

@@ -0,0 +1,50 @@
package store
import (
"encoding/json"
"os"
"testing"
"time"
"github.com/datarhei/core/v16/config"
"github.com/stretchr/testify/require"
)
func TestMigrationV1ToV3(t *testing.T) {
jsondatav1, err := os.ReadFile("./fixtures/config_v1.json")
require.NoError(t, err)
jsondatav3, err := os.ReadFile("./fixtures/config_v1_v3.json")
require.NoError(t, err)
datav3 := config.New(nil)
json.Unmarshal(jsondatav3, datav3)
data, err := migrate(jsondatav1)
require.NoError(t, err)
datav3.Data.CreatedAt = time.Time{}
data.CreatedAt = time.Time{}
require.Equal(t, datav3.Data, *data)
}
func TestMigrationV2ToV3(t *testing.T) {
jsondatav2, err := os.ReadFile("./fixtures/config_v2.json")
require.NoError(t, err)
jsondatav3, err := os.ReadFile("./fixtures/config_v2_v3.json")
require.NoError(t, err)
datav3 := config.New(nil)
json.Unmarshal(jsondatav3, datav3)
data, err := migrate(jsondatav2)
require.NoError(t, err)
datav3.Data.CreatedAt = time.Time{}
data.CreatedAt = time.Time{}
require.Equal(t, datav3.Data, *data)
}

53
config/store/location.go Normal file
View File

@@ -0,0 +1,53 @@
package store
import (
"os"
"path"
)
// Location returns the path to the config file. If no path is provided,
// different standard location will be probed:
// - os.UserConfigDir() + /datarhei-core/config.js
// - os.UserHomeDir() + /.config/datarhei-core/config.js
// - ./config/config.js
// If the config doesn't exist in none of these locations, it will be assumed
// at ./config/config.js
func Location(filepath string) string {
configfile := filepath
if len(configfile) != 0 {
return configfile
}
locations := []string{}
if dir, err := os.UserConfigDir(); err == nil {
locations = append(locations, dir+"/datarhei-core/config.js")
}
if dir, err := os.UserHomeDir(); err == nil {
locations = append(locations, dir+"/.config/datarhei-core/config.js")
}
locations = append(locations, "./config/config.js")
for _, path := range locations {
info, err := os.Stat(path)
if err != nil {
continue
}
if info.IsDir() {
continue
}
configfile = path
}
if len(configfile) == 0 {
configfile = "./config/config.js"
}
os.MkdirAll(path.Dir(configfile), 0740)
return configfile
}

View File

@@ -1,23 +1,29 @@
package config package store
import "github.com/datarhei/core/v16/config"
// Store is a store for the configuration data. // Store is a store for the configuration data.
type Store interface { type Store interface {
// Get the current configuration. // Get the current configuration.
Get() *Config Get() *config.Config
// Set a new configuration for persistence. // Set a new configuration for persistence.
Set(data *Config) error Set(data *config.Config) error
// GetActive returns the configuration that has been set as // GetActive returns the configuration that has been set as
// active before, otherwise it return nil. // active before, otherwise it return nil.
GetActive() *Config GetActive() *config.Config
// SetActive will keep the given configuration // SetActive will keep the given configuration
// as active in memory. It can be retrieved later with GetActive() // as active in memory. It can be retrieved later with GetActive()
SetActive(data *Config) error SetActive(data *config.Config) error
// Reload will reload the stored configuration. It has to make sure // Reload will reload the stored configuration. It has to make sure
// that all affected components will receiver their potentially // that all affected components will receiver their potentially
// changed configuration. // changed configuration.
Reload() error Reload() error
} }
type DataVersion struct {
Version int64 `json:"version"`
}

View File

@@ -1,844 +0,0 @@
package config
import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/mail"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/datarhei/core/v16/http/cors"
)
type value interface {
// String returns a string representation of the value.
String() string
// Set a new value for the value. Returns an
// error if the given string representation can't
// be transformed to the value. Returns nil
// if the new value has been set.
Set(string) error
// Validate the value. The returned error will
// indicate what is wrong with the current value.
// Returns nil if the value is OK.
Validate() error
// IsEmpty returns whether the value represents an empty
// representation for that value.
IsEmpty() bool
}
// string
type stringValue string
func newStringValue(p *string, val string) *stringValue {
*p = val
return (*stringValue)(p)
}
func (s *stringValue) Set(val string) error {
*s = stringValue(val)
return nil
}
func (s *stringValue) String() string {
return string(*s)
}
func (s *stringValue) Validate() error {
return nil
}
func (s *stringValue) IsEmpty() bool {
return len(string(*s)) == 0
}
// address (host?:port)
type addressValue string
func newAddressValue(p *string, val string) *addressValue {
*p = val
return (*addressValue)(p)
}
func (s *addressValue) Set(val string) error {
// Check if the new value is only a port number
re := regexp.MustCompile("^[0-9]+$")
if re.MatchString(val) {
val = ":" + val
}
*s = addressValue(val)
return nil
}
func (s *addressValue) String() string {
return string(*s)
}
func (s *addressValue) Validate() error {
_, port, err := net.SplitHostPort(string(*s))
if err != nil {
return err
}
re := regexp.MustCompile("^[0-9]+$")
if !re.MatchString(port) {
return fmt.Errorf("the port must be numerical")
}
return nil
}
func (s *addressValue) IsEmpty() bool {
return s.Validate() != nil
}
// array of strings
type stringListValue struct {
p *[]string
separator string
}
func newStringListValue(p *[]string, val []string, separator string) *stringListValue {
v := &stringListValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *stringListValue) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *stringListValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *stringListValue) Validate() error {
return nil
}
func (s *stringListValue) IsEmpty() bool {
return len(*s.p) == 0
}
// array of auth0 tenants
type tenantListValue struct {
p *[]Auth0Tenant
separator string
}
func newTenantListValue(p *[]Auth0Tenant, val []Auth0Tenant, separator string) *tenantListValue {
v := &tenantListValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *tenantListValue) Set(val string) error {
list := []Auth0Tenant{}
for i, elm := range strings.Split(val, s.separator) {
data, err := base64.StdEncoding.DecodeString(elm)
if err != nil {
return fmt.Errorf("invalid base64 encoding of tenant %d: %w", i, err)
}
t := Auth0Tenant{}
if err := json.Unmarshal(data, &t); err != nil {
return fmt.Errorf("invalid JSON in tenant %d: %w", i, err)
}
list = append(list, t)
}
*s.p = list
return nil
}
func (s *tenantListValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
list := []string{}
for _, t := range *s.p {
list = append(list, fmt.Sprintf("%s (%d users)", t.Domain, len(t.Users)))
}
return strings.Join(list, ",")
}
func (s *tenantListValue) Validate() error {
for i, t := range *s.p {
if len(t.Domain) == 0 {
return fmt.Errorf("the domain for tenant %d is missing", i)
}
if len(t.Audience) == 0 {
return fmt.Errorf("the audience for tenant %d is missing", i)
}
}
return nil
}
func (s *tenantListValue) IsEmpty() bool {
return len(*s.p) == 0
}
// map of strings to strings
type stringMapStringValue struct {
p *map[string]string
}
func newStringMapStringValue(p *map[string]string, val map[string]string) *stringMapStringValue {
v := &stringMapStringValue{
p: p,
}
if *p == nil {
*p = make(map[string]string)
}
if val != nil {
*p = val
}
return v
}
func (s *stringMapStringValue) Set(val string) error {
mappings := make(map[string]string)
for _, elm := range strings.Split(val, " ") {
elm = strings.TrimSpace(elm)
if len(elm) == 0 {
continue
}
mapping := strings.SplitN(elm, ":", 2)
mappings[mapping[0]] = mapping[1]
}
*s.p = mappings
return nil
}
func (s *stringMapStringValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
mappings := make([]string, len(*s.p))
i := 0
for k, v := range *s.p {
mappings[i] = k + ":" + v
i++
}
return strings.Join(mappings, " ")
}
func (s *stringMapStringValue) Validate() error {
return nil
}
func (s *stringMapStringValue) IsEmpty() bool {
return len(*s.p) == 0
}
// array of CIDR notation IP adresses
type cidrListValue struct {
p *[]string
separator string
}
func newCIDRListValue(p *[]string, val []string, separator string) *cidrListValue {
v := &cidrListValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *cidrListValue) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *cidrListValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *cidrListValue) Validate() error {
for _, cidr := range *s.p {
_, _, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
}
return nil
}
func (s *cidrListValue) IsEmpty() bool {
return len(*s.p) == 0
}
// array of origins for CORS
type corsOriginsValue struct {
p *[]string
separator string
}
func newCORSOriginsValue(p *[]string, val []string, separator string) *corsOriginsValue {
v := &corsOriginsValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *corsOriginsValue) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *corsOriginsValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *corsOriginsValue) Validate() error {
return cors.Validate(*s.p)
}
func (s *corsOriginsValue) IsEmpty() bool {
return len(*s.p) == 0
}
// boolean
type boolValue bool
func newBoolValue(p *bool, val bool) *boolValue {
*p = val
return (*boolValue)(p)
}
func (b *boolValue) Set(val string) error {
v, err := strconv.ParseBool(val)
if err != nil {
return err
}
*b = boolValue(v)
return nil
}
func (b *boolValue) String() string {
return strconv.FormatBool(bool(*b))
}
func (b *boolValue) Validate() error {
return nil
}
func (b *boolValue) IsEmpty() bool {
return !bool(*b)
}
// int
type intValue int
func newIntValue(p *int, val int) *intValue {
*p = val
return (*intValue)(p)
}
func (i *intValue) Set(val string) error {
v, err := strconv.Atoi(val)
if err != nil {
return err
}
*i = intValue(v)
return nil
}
func (i *intValue) String() string {
return strconv.Itoa(int(*i))
}
func (i *intValue) Validate() error {
return nil
}
func (i *intValue) IsEmpty() bool {
return int(*i) == 0
}
// int64
type int64Value int64
func newInt64Value(p *int64, val int64) *int64Value {
*p = val
return (*int64Value)(p)
}
func (u *int64Value) Set(val string) error {
v, err := strconv.ParseInt(val, 0, 64)
if err != nil {
return err
}
*u = int64Value(v)
return nil
}
func (u *int64Value) String() string {
return strconv.FormatInt(int64(*u), 10)
}
func (u *int64Value) Validate() error {
return nil
}
func (u *int64Value) IsEmpty() bool {
return int64(*u) == 0
}
// uint64
type uint64Value uint64
func newUint64Value(p *uint64, val uint64) *uint64Value {
*p = val
return (*uint64Value)(p)
}
func (u *uint64Value) Set(val string) error {
v, err := strconv.ParseUint(val, 0, 64)
if err != nil {
return err
}
*u = uint64Value(v)
return nil
}
func (u *uint64Value) String() string {
return strconv.FormatUint(uint64(*u), 10)
}
func (u *uint64Value) Validate() error {
return nil
}
func (u *uint64Value) IsEmpty() bool {
return uint64(*u) == 0
}
// network port
type portValue int
func newPortValue(p *int, val int) *portValue {
*p = val
return (*portValue)(p)
}
func (i *portValue) Set(val string) error {
v, err := strconv.Atoi(val)
if err != nil {
return err
}
*i = portValue(v)
return nil
}
func (i *portValue) String() string {
return strconv.Itoa(int(*i))
}
func (i *portValue) Validate() error {
val := int(*i)
if val < 0 || val >= (1<<16) {
return fmt.Errorf("%d is not in the range of [0, %d]", val, 1<<16-1)
}
return nil
}
func (i *portValue) IsEmpty() bool {
return int(*i) == 0
}
// must directory
type mustDirValue string
func newMustDirValue(p *string, val string) *mustDirValue {
*p = val
return (*mustDirValue)(p)
}
func (u *mustDirValue) Set(val string) error {
*u = mustDirValue(val)
return nil
}
func (u *mustDirValue) String() string {
return string(*u)
}
func (u *mustDirValue) Validate() error {
val := string(*u)
if len(strings.TrimSpace(val)) == 0 {
return fmt.Errorf("path name must not be empty")
}
finfo, err := os.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.IsDir() {
return fmt.Errorf("%s is not a directory", val)
}
return nil
}
func (u *mustDirValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// directory
type dirValue string
func newDirValue(p *string, val string) *dirValue {
*p = val
return (*dirValue)(p)
}
func (u *dirValue) Set(val string) error {
*u = dirValue(val)
return nil
}
func (u *dirValue) String() string {
return string(*u)
}
func (u *dirValue) Validate() error {
val := string(*u)
if len(strings.TrimSpace(val)) == 0 {
return nil
}
finfo, err := os.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.IsDir() {
return fmt.Errorf("%s is not a directory", val)
}
return nil
}
func (u *dirValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// executable
type execValue string
func newExecValue(p *string, val string) *execValue {
*p = val
return (*execValue)(p)
}
func (u *execValue) Set(val string) error {
*u = execValue(val)
return nil
}
func (u *execValue) String() string {
return string(*u)
}
func (u *execValue) Validate() error {
val := string(*u)
_, err := exec.LookPath(val)
if err != nil {
return fmt.Errorf("%s not found or is not executable", val)
}
return nil
}
func (u *execValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// regular file
type fileValue string
func newFileValue(p *string, val string) *fileValue {
*p = val
return (*fileValue)(p)
}
func (u *fileValue) Set(val string) error {
*u = fileValue(val)
return nil
}
func (u *fileValue) String() string {
return string(*u)
}
func (u *fileValue) Validate() error {
val := string(*u)
if len(val) == 0 {
return nil
}
finfo, err := os.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", val)
}
return nil
}
func (u *fileValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// time
type timeValue time.Time
func newTimeValue(p *time.Time, val time.Time) *timeValue {
*p = val
return (*timeValue)(p)
}
func (u *timeValue) Set(val string) error {
v, err := time.Parse(time.RFC3339, val)
if err != nil {
return err
}
*u = timeValue(v)
return nil
}
func (u *timeValue) String() string {
v := time.Time(*u)
return v.Format(time.RFC3339)
}
func (u *timeValue) Validate() error {
return nil
}
func (u *timeValue) IsEmpty() bool {
v := time.Time(*u)
return v.IsZero()
}
// url
type urlValue string
func newURLValue(p *string, val string) *urlValue {
*p = val
return (*urlValue)(p)
}
func (u *urlValue) Set(val string) error {
*u = urlValue(val)
return nil
}
func (u *urlValue) String() string {
return string(*u)
}
func (u *urlValue) Validate() error {
val := string(*u)
if len(val) == 0 {
return nil
}
URL, err := url.Parse(val)
if err != nil {
return fmt.Errorf("%s is not a valid URL", val)
}
if len(URL.Scheme) == 0 || len(URL.Host) == 0 {
return fmt.Errorf("%s is not a valid URL", val)
}
return nil
}
func (u *urlValue) IsEmpty() bool {
return len(string(*u)) == 0
}
// absolute path
type absolutePathValue string
func newAbsolutePathValue(p *string, val string) *absolutePathValue {
*p = filepath.Clean(val)
return (*absolutePathValue)(p)
}
func (s *absolutePathValue) Set(val string) error {
*s = absolutePathValue(filepath.Clean(val))
return nil
}
func (s *absolutePathValue) String() string {
return string(*s)
}
func (s *absolutePathValue) Validate() error {
path := string(*s)
if !filepath.IsAbs(path) {
return fmt.Errorf("%s is not an absolute path", path)
}
return nil
}
func (s *absolutePathValue) IsEmpty() bool {
return len(string(*s)) == 0
}
// email address
type emailValue string
func newEmailValue(p *string, val string) *emailValue {
*p = val
return (*emailValue)(p)
}
func (s *emailValue) Set(val string) error {
addr, err := mail.ParseAddress(val)
if err != nil {
return err
}
*s = emailValue(addr.Address)
return nil
}
func (s *emailValue) String() string {
return string(*s)
}
func (s *emailValue) Validate() error {
if len(s.String()) == 0 {
return nil
}
_, err := mail.ParseAddress(s.String())
return err
}
func (s *emailValue) IsEmpty() bool {
return len(string(*s)) == 0
}

View File

@@ -1,58 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIntValue(t *testing.T) {
var i int
ivar := newIntValue(&i, 11)
assert.Equal(t, "11", ivar.String())
assert.Equal(t, nil, ivar.Validate())
assert.Equal(t, false, ivar.IsEmpty())
i = 42
assert.Equal(t, "42", ivar.String())
assert.Equal(t, nil, ivar.Validate())
assert.Equal(t, false, ivar.IsEmpty())
ivar.Set("77")
assert.Equal(t, int(77), i)
}
type testdata struct {
value1 int
value2 int
}
func TestCopyStruct(t *testing.T) {
data1 := testdata{}
newIntValue(&data1.value1, 1)
newIntValue(&data1.value2, 2)
assert.Equal(t, int(1), data1.value1)
assert.Equal(t, int(2), data1.value2)
data2 := testdata{}
val21 := newIntValue(&data2.value1, 3)
val22 := newIntValue(&data2.value2, 4)
assert.Equal(t, int(3), data2.value1)
assert.Equal(t, int(4), data2.value2)
data2 = data1
assert.Equal(t, int(1), data2.value1)
assert.Equal(t, int(2), data2.value2)
assert.Equal(t, "1", val21.String())
assert.Equal(t, "2", val22.String())
}

405
config/v1/config.go Normal file
View File

@@ -0,0 +1,405 @@
package v1
import (
"context"
"net"
"time"
"github.com/datarhei/core/v16/config/copy"
"github.com/datarhei/core/v16/config/value"
"github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/math/rand"
haikunator "github.com/atrox/haikunatorgo/v2"
"github.com/google/uuid"
)
const version int64 = 1
// Make sure that the config.Config interface is satisfied
//var _ config.Config = &Config{}
// Config is a wrapper for Data
type Config struct {
fs fs.Filesystem
vars vars.Variables
Data
}
// New returns a Config which is initialized with its default values
func New(f fs.Filesystem) *Config {
cfg := &Config{
fs: f,
}
if cfg.fs == nil {
cfg.fs, _ = fs.NewMemFilesystem(fs.MemConfig{})
}
cfg.init()
return cfg
}
func (d *Config) Get(name string) (string, error) {
return d.vars.Get(name)
}
func (d *Config) Set(name, val string) error {
return d.vars.Set(name, val)
}
// NewConfigFrom returns a clone of a Config
func (d *Config) Clone() *Config {
data := New(d.fs)
data.CreatedAt = d.CreatedAt
data.LoadedAt = d.LoadedAt
data.UpdatedAt = d.UpdatedAt
data.Version = d.Version
data.ID = d.ID
data.Name = d.Name
data.Address = d.Address
data.CheckForUpdates = d.CheckForUpdates
data.Log = d.Log
data.DB = d.DB
data.Host = d.Host
data.API = d.API
data.TLS = d.TLS
data.Storage = d.Storage
data.RTMP = d.RTMP
data.SRT = d.SRT
data.FFmpeg = d.FFmpeg
data.Playout = d.Playout
data.Debug = d.Debug
data.Metrics = d.Metrics
data.Sessions = d.Sessions
data.Service = d.Service
data.Router = d.Router
data.Log.Topics = copy.Slice(d.Log.Topics)
data.Host.Name = copy.Slice(d.Host.Name)
data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow)
data.API.Access.HTTP.Block = copy.Slice(d.API.Access.HTTP.Block)
data.API.Access.HTTPS.Allow = copy.Slice(d.API.Access.HTTPS.Allow)
data.API.Access.HTTPS.Block = copy.Slice(d.API.Access.HTTPS.Block)
data.API.Auth.Auth0.Tenants = copy.TenantSlice(d.API.Auth.Auth0.Tenants)
data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.Storage.Disk.Cache.Types = copy.Slice(d.Storage.Disk.Cache.Types)
data.FFmpeg.Access.Input.Allow = copy.Slice(d.FFmpeg.Access.Input.Allow)
data.FFmpeg.Access.Input.Block = copy.Slice(d.FFmpeg.Access.Input.Block)
data.FFmpeg.Access.Output.Allow = copy.Slice(d.FFmpeg.Access.Output.Allow)
data.FFmpeg.Access.Output.Block = copy.Slice(d.FFmpeg.Access.Output.Block)
data.Sessions.IPIgnoreList = copy.Slice(d.Sessions.IPIgnoreList)
data.SRT.Log.Topics = copy.Slice(d.SRT.Log.Topics)
data.Router.BlockedPrefixes = copy.Slice(d.Router.BlockedPrefixes)
data.Router.Routes = copy.StringMap(d.Router.Routes)
data.vars.Transfer(&d.vars)
return data
}
func (d *Config) init() {
d.vars.Register(value.NewInt64(&d.Version, version), "version", "", nil, "Configuration file layout version", true, false)
d.vars.Register(value.NewTime(&d.CreatedAt, time.Now()), "created_at", "", nil, "Configuration file creation time", false, false)
d.vars.Register(value.NewString(&d.ID, uuid.New().String()), "id", "CORE_ID", nil, "ID for this instance", true, false)
d.vars.Register(value.NewString(&d.Name, haikunator.New().Haikunate()), "name", "CORE_NAME", nil, "A human readable name for this instance", false, false)
d.vars.Register(value.NewAddress(&d.Address, ":8080"), "address", "CORE_ADDRESS", nil, "HTTP listening address", false, false)
d.vars.Register(value.NewBool(&d.CheckForUpdates, true), "update_check", "CORE_UPDATE_CHECK", nil, "Check for updates and send anonymized data", false, false)
// Log
d.vars.Register(value.NewString(&d.Log.Level, "info"), "log.level", "CORE_LOG_LEVEL", nil, "Loglevel: silent, error, warn, info, debug", false, false)
d.vars.Register(value.NewStringList(&d.Log.Topics, []string{}, ","), "log.topics", "CORE_LOG_TOPICS", nil, "Show only selected log topics", false, false)
d.vars.Register(value.NewInt(&d.Log.MaxLines, 1000), "log.max_lines", "CORE_LOG_MAXLINES", nil, "Number of latest log lines to keep in memory", false, false)
// DB
d.vars.Register(value.NewMustDir(&d.DB.Dir, "./config", d.fs), "db.dir", "CORE_DB_DIR", nil, "Directory for holding the operational data", false, false)
// Host
d.vars.Register(value.NewStringList(&d.Host.Name, []string{}, ","), "host.name", "CORE_HOST_NAME", nil, "Comma separated list of public host/domain names or IPs", false, false)
d.vars.Register(value.NewBool(&d.Host.Auto, true), "host.auto", "CORE_HOST_AUTO", nil, "Enable detection of public IP addresses", false, false)
// API
d.vars.Register(value.NewBool(&d.API.ReadOnly, false), "api.read_only", "CORE_API_READ_ONLY", nil, "Allow only ready only access to the API", false, false)
d.vars.Register(value.NewCIDRList(&d.API.Access.HTTP.Allow, []string{}, ","), "api.access.http.allow", "CORE_API_ACCESS_HTTP_ALLOW", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false)
d.vars.Register(value.NewCIDRList(&d.API.Access.HTTP.Block, []string{}, ","), "api.access.http.block", "CORE_API_ACCESS_HTTP_BLOCK", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false)
d.vars.Register(value.NewCIDRList(&d.API.Access.HTTPS.Allow, []string{}, ","), "api.access.https.allow", "CORE_API_ACCESS_HTTPS_ALLOW", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false)
d.vars.Register(value.NewCIDRList(&d.API.Access.HTTPS.Block, []string{}, ","), "api.access.https.block", "CORE_API_ACCESS_HTTPS_BLOCK", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false)
d.vars.Register(value.NewBool(&d.API.Auth.Enable, false), "api.auth.enable", "CORE_API_AUTH_ENABLE", nil, "Enable authentication for all clients", false, false)
d.vars.Register(value.NewBool(&d.API.Auth.DisableLocalhost, false), "api.auth.disable_localhost", "CORE_API_AUTH_DISABLE_LOCALHOST", nil, "Disable authentication for clients from localhost", false, false)
d.vars.Register(value.NewString(&d.API.Auth.Username, ""), "api.auth.username", "CORE_API_AUTH_USERNAME", []string{"RS_USERNAME"}, "Username", false, false)
d.vars.Register(value.NewString(&d.API.Auth.Password, ""), "api.auth.password", "CORE_API_AUTH_PASSWORD", []string{"RS_PASSWORD"}, "Password", false, true)
// Auth JWT
d.vars.Register(value.NewString(&d.API.Auth.JWT.Secret, rand.String(32)), "api.auth.jwt.secret", "CORE_API_AUTH_JWT_SECRET", nil, "JWT secret, leave empty for generating a random value", false, true)
// Auth Auth0
d.vars.Register(value.NewBool(&d.API.Auth.Auth0.Enable, false), "api.auth.auth0.enable", "CORE_API_AUTH_AUTH0_ENABLE", nil, "Enable Auth0", false, false)
d.vars.Register(value.NewTenantList(&d.API.Auth.Auth0.Tenants, []value.Auth0Tenant{}, ","), "api.auth.auth0.tenants", "CORE_API_AUTH_AUTH0_TENANTS", nil, "List of Auth0 tenants", false, false)
// TLS
d.vars.Register(value.NewAddress(&d.TLS.Address, ":8181"), "tls.address", "CORE_TLS_ADDRESS", nil, "HTTPS listening address", false, false)
d.vars.Register(value.NewBool(&d.TLS.Enable, false), "tls.enable", "CORE_TLS_ENABLE", nil, "Enable HTTPS", false, false)
d.vars.Register(value.NewBool(&d.TLS.Auto, false), "tls.auto", "CORE_TLS_AUTO", nil, "Enable Let's Encrypt certificate", false, false)
d.vars.Register(value.NewFile(&d.TLS.CertFile, "", d.fs), "tls.cert_file", "CORE_TLS_CERTFILE", nil, "Path to certificate file in PEM format", false, false)
d.vars.Register(value.NewFile(&d.TLS.KeyFile, "", d.fs), "tls.key_file", "CORE_TLS_KEYFILE", nil, "Path to key file in PEM format", false, false)
// Storage
d.vars.Register(value.NewFile(&d.Storage.MimeTypes, "./mime.types", d.fs), "storage.mimetypes_file", "CORE_STORAGE_MIMETYPES_FILE", []string{"CORE_MIMETYPES_FILE"}, "Path to file with mime-types", false, false)
// Storage (Disk)
d.vars.Register(value.NewMustDir(&d.Storage.Disk.Dir, "./data", d.fs), "storage.disk.dir", "CORE_STORAGE_DISK_DIR", nil, "Directory on disk, exposed on /", false, false)
d.vars.Register(value.NewInt64(&d.Storage.Disk.Size, 0), "storage.disk.max_size_mbytes", "CORE_STORAGE_DISK_MAXSIZEMBYTES", nil, "Max. allowed megabytes for storage.disk.dir, 0 for unlimited", false, false)
d.vars.Register(value.NewBool(&d.Storage.Disk.Cache.Enable, true), "storage.disk.cache.enable", "CORE_STORAGE_DISK_CACHE_ENABLE", nil, "Enable cache for /", false, false)
d.vars.Register(value.NewUint64(&d.Storage.Disk.Cache.Size, 0), "storage.disk.cache.max_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXSIZEMBYTES", nil, "Max. allowed cache size, 0 for unlimited", false, false)
d.vars.Register(value.NewInt64(&d.Storage.Disk.Cache.TTL, 300), "storage.disk.cache.ttl_seconds", "CORE_STORAGE_DISK_CACHE_TTLSECONDS", nil, "Seconds to keep files in cache", false, false)
d.vars.Register(value.NewUint64(&d.Storage.Disk.Cache.FileSize, 1), "storage.disk.cache.max_file_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXFILESIZEMBYTES", nil, "Max. file size to put in cache", false, false)
d.vars.Register(value.NewStringList(&d.Storage.Disk.Cache.Types, []string{}, " "), "storage.disk.cache.types", "CORE_STORAGE_DISK_CACHE_TYPES_ALLOW", []string{"CORE_STORAGE_DISK_CACHE_TYPES"}, "File extensions to cache, empty for all", false, false)
// Storage (Memory)
d.vars.Register(value.NewBool(&d.Storage.Memory.Auth.Enable, true), "storage.memory.auth.enable", "CORE_STORAGE_MEMORY_AUTH_ENABLE", nil, "Enable basic auth for PUT,POST, and DELETE on /memfs", false, false)
d.vars.Register(value.NewString(&d.Storage.Memory.Auth.Username, "admin"), "storage.memory.auth.username", "CORE_STORAGE_MEMORY_AUTH_USERNAME", nil, "Username for Basic-Auth of /memfs", false, false)
d.vars.Register(value.NewString(&d.Storage.Memory.Auth.Password, rand.StringAlphanumeric(18)), "storage.memory.auth.password", "CORE_STORAGE_MEMORY_AUTH_PASSWORD", nil, "Password for Basic-Auth of /memfs", false, true)
d.vars.Register(value.NewInt64(&d.Storage.Memory.Size, 0), "storage.memory.max_size_mbytes", "CORE_STORAGE_MEMORY_MAXSIZEMBYTES", nil, "Max. allowed megabytes for /memfs, 0 for unlimited", false, false)
d.vars.Register(value.NewBool(&d.Storage.Memory.Purge, false), "storage.memory.purge", "CORE_STORAGE_MEMORY_PURGE", nil, "Automatically remove the oldest files if /memfs is full", false, false)
// Storage (CORS)
d.vars.Register(value.NewCORSOrigins(&d.Storage.CORS.Origins, []string{"*"}, ","), "storage.cors.origins", "CORE_STORAGE_CORS_ORIGINS", nil, "Allowed CORS origins for /memfs and /data", false, false)
// RTMP
d.vars.Register(value.NewBool(&d.RTMP.Enable, false), "rtmp.enable", "CORE_RTMP_ENABLE", nil, "Enable RTMP server", false, false)
d.vars.Register(value.NewBool(&d.RTMP.EnableTLS, false), "rtmp.enable_tls", "CORE_RTMP_ENABLE_TLS", nil, "Enable RTMPS server instead of RTMP", false, false)
d.vars.Register(value.NewAddress(&d.RTMP.Address, ":1935"), "rtmp.address", "CORE_RTMP_ADDRESS", nil, "RTMP server listen address", false, false)
d.vars.Register(value.NewAbsolutePath(&d.RTMP.App, "/"), "rtmp.app", "CORE_RTMP_APP", nil, "RTMP app for publishing", false, false)
d.vars.Register(value.NewString(&d.RTMP.Token, ""), "rtmp.token", "CORE_RTMP_TOKEN", nil, "RTMP token for publishing and playing", false, true)
// SRT
d.vars.Register(value.NewBool(&d.SRT.Enable, false), "srt.enable", "CORE_SRT_ENABLE", nil, "Enable SRT server", false, false)
d.vars.Register(value.NewAddress(&d.SRT.Address, ":6000"), "srt.address", "CORE_SRT_ADDRESS", nil, "SRT server listen address", false, false)
d.vars.Register(value.NewString(&d.SRT.Passphrase, ""), "srt.passphrase", "CORE_SRT_PASSPHRASE", nil, "SRT encryption passphrase", false, true)
d.vars.Register(value.NewString(&d.SRT.Token, ""), "srt.token", "CORE_SRT_TOKEN", nil, "SRT token for publishing and playing", false, true)
d.vars.Register(value.NewBool(&d.SRT.Log.Enable, false), "srt.log.enable", "CORE_SRT_LOG_ENABLE", nil, "Enable SRT server logging", false, false)
d.vars.Register(value.NewStringList(&d.SRT.Log.Topics, []string{}, ","), "srt.log.topics", "CORE_SRT_LOG_TOPICS", nil, "List of topics to log", false, false)
// FFmpeg
d.vars.Register(value.NewExec(&d.FFmpeg.Binary, "ffmpeg", d.fs), "ffmpeg.binary", "CORE_FFMPEG_BINARY", nil, "Path to ffmpeg binary", true, false)
d.vars.Register(value.NewInt64(&d.FFmpeg.MaxProcesses, 0), "ffmpeg.max_processes", "CORE_FFMPEG_MAXPROCESSES", nil, "Max. allowed simultaneously running ffmpeg instances, 0 for unlimited", false, false)
d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Input.Allow, []string{}, " "), "ffmpeg.access.input.allow", "CORE_FFMPEG_ACCESS_INPUT_ALLOW", nil, "List of allowed expression to match against the input addresses", false, false)
d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Input.Block, []string{}, " "), "ffmpeg.access.input.block", "CORE_FFMPEG_ACCESS_INPUT_BLOCK", nil, "List of blocked expression to match against the input addresses", false, false)
d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Output.Allow, []string{}, " "), "ffmpeg.access.output.allow", "CORE_FFMPEG_ACCESS_OUTPUT_ALLOW", nil, "List of allowed expression to match against the output addresses", false, false)
d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Output.Block, []string{}, " "), "ffmpeg.access.output.block", "CORE_FFMPEG_ACCESS_OUTPUT_BLOCK", nil, "List of blocked expression to match against the output addresses", false, false)
d.vars.Register(value.NewInt(&d.FFmpeg.Log.MaxLines, 50), "ffmpeg.log.max_lines", "CORE_FFMPEG_LOG_MAXLINES", nil, "Number of latest log lines to keep for each process", false, false)
d.vars.Register(value.NewInt(&d.FFmpeg.Log.MaxHistory, 3), "ffmpeg.log.max_history", "CORE_FFMPEG_LOG_MAXHISTORY", nil, "Number of latest logs to keep for each process", false, false)
// Playout
d.vars.Register(value.NewBool(&d.Playout.Enable, false), "playout.enable", "CORE_PLAYOUT_ENABLE", nil, "Enable playout proxy where available", false, false)
d.vars.Register(value.NewPort(&d.Playout.MinPort, 0), "playout.min_port", "CORE_PLAYOUT_MINPORT", nil, "Min. playout server port", false, false)
d.vars.Register(value.NewPort(&d.Playout.MaxPort, 0), "playout.max_port", "CORE_PLAYOUT_MAXPORT", nil, "Max. playout server port", false, false)
// Debug
d.vars.Register(value.NewBool(&d.Debug.Profiling, false), "debug.profiling", "CORE_DEBUG_PROFILING", nil, "Enable profiling endpoint on /profiling", false, false)
d.vars.Register(value.NewInt(&d.Debug.ForceGC, 0), "debug.force_gc", "CORE_DEBUG_FORCEGC", nil, "Number of seconds between forcing GC to return memory to the OS", false, false)
// Metrics
d.vars.Register(value.NewBool(&d.Metrics.Enable, false), "metrics.enable", "CORE_METRICS_ENABLE", nil, "Enable collecting historic metrics data", false, false)
d.vars.Register(value.NewBool(&d.Metrics.EnablePrometheus, false), "metrics.enable_prometheus", "CORE_METRICS_ENABLE_PROMETHEUS", nil, "Enable prometheus endpoint /metrics", false, false)
d.vars.Register(value.NewInt64(&d.Metrics.Range, 300), "metrics.range_seconds", "CORE_METRICS_RANGE_SECONDS", nil, "Seconds to keep history data", false, false)
d.vars.Register(value.NewInt64(&d.Metrics.Interval, 2), "metrics.interval_seconds", "CORE_METRICS_INTERVAL_SECONDS", nil, "Interval for collecting metrics", false, false)
// Sessions
d.vars.Register(value.NewBool(&d.Sessions.Enable, true), "sessions.enable", "CORE_SESSIONS_ENABLE", nil, "Enable collecting HLS session stats for /memfs", false, false)
d.vars.Register(value.NewCIDRList(&d.Sessions.IPIgnoreList, []string{"127.0.0.1/32", "::1/128"}, ","), "sessions.ip_ignorelist", "CORE_SESSIONS_IP_IGNORELIST", nil, "List of IP ranges in CIDR notation to ignore", false, false)
d.vars.Register(value.NewInt(&d.Sessions.SessionTimeout, 30), "sessions.session_timeout_sec", "CORE_SESSIONS_SESSION_TIMEOUT_SEC", nil, "Timeout for an idle session", false, false)
d.vars.Register(value.NewBool(&d.Sessions.Persist, false), "sessions.persist", "CORE_SESSIONS_PERSIST", nil, "Whether to persist session history. Will be stored as sessions.json in db.dir", false, false)
d.vars.Register(value.NewInt(&d.Sessions.PersistInterval, 300), "sessions.persist_interval_sec", "CORE_SESSIONS_PERSIST_INTERVAL_SEC", nil, "Interval in seconds in which to persist the current session history", false, false)
d.vars.Register(value.NewUint64(&d.Sessions.MaxBitrate, 0), "sessions.max_bitrate_mbit", "CORE_SESSIONS_MAXBITRATE_MBIT", nil, "Max. allowed outgoing bitrate in mbit/s, 0 for unlimited", false, false)
d.vars.Register(value.NewUint64(&d.Sessions.MaxSessions, 0), "sessions.max_sessions", "CORE_SESSIONS_MAXSESSIONS", nil, "Max. allowed number of simultaneous sessions, 0 for unlimited", false, false)
// Service
d.vars.Register(value.NewBool(&d.Service.Enable, false), "service.enable", "CORE_SERVICE_ENABLE", nil, "Enable connecting to the Restreamer Service", false, false)
d.vars.Register(value.NewString(&d.Service.Token, ""), "service.token", "CORE_SERVICE_TOKEN", nil, "Restreamer Service account token", false, true)
d.vars.Register(value.NewURL(&d.Service.URL, "https://service.datarhei.com"), "service.url", "CORE_SERVICE_URL", nil, "URL of the Restreamer Service", false, false)
// Router
d.vars.Register(value.NewStringList(&d.Router.BlockedPrefixes, []string{"/api"}, ","), "router.blocked_prefixes", "CORE_ROUTER_BLOCKED_PREFIXES", nil, "List of path prefixes that can't be routed", false, false)
d.vars.Register(value.NewStringMapString(&d.Router.Routes, nil), "router.routes", "CORE_ROUTER_ROUTES", nil, "List of route mappings", false, false)
d.vars.Register(value.NewDir(&d.Router.UIPath, "", d.fs), "router.ui_path", "CORE_ROUTER_UI_PATH", nil, "Path to a directory holding UI files mounted as /ui", false, false)
}
// Validate validates the current state of the Config for completeness and sanity. Errors are
// written to the log. Use resetLogs to indicate to reset the logs prior validation.
func (d *Config) Validate(resetLogs bool) {
if resetLogs {
d.vars.ResetLogs()
}
if d.Version != version {
d.vars.Log("error", "version", "unknown configuration layout version (found version %d, expecting version %d)", d.Version, version)
return
}
d.vars.Validate()
// Individual sanity checks
// If HTTP Auth is enabled, check that the username and password are set
if d.API.Auth.Enable {
if len(d.API.Auth.Username) == 0 || len(d.API.Auth.Password) == 0 {
d.vars.Log("error", "api.auth.enable", "api.auth.username and api.auth.password must be set")
}
}
// If Auth0 is enabled, check that domain, audience, and clientid are set
if d.API.Auth.Auth0.Enable {
if len(d.API.Auth.Auth0.Tenants) == 0 {
d.vars.Log("error", "api.auth.auth0.enable", "at least one tenants must be set")
}
for i, t := range d.API.Auth.Auth0.Tenants {
if len(t.Domain) == 0 || len(t.Audience) == 0 || len(t.ClientID) == 0 {
d.vars.Log("error", "api.auth.auth0.tenants", "domain, audience, and clientid must be set (tenant %d)", i)
}
}
}
// If TLS is enabled and Let's Encrypt is disabled, require certfile and keyfile
if d.TLS.Enable && !d.TLS.Auto {
if len(d.TLS.CertFile) == 0 || len(d.TLS.KeyFile) == 0 {
d.vars.Log("error", "tls.enable", "tls.certfile and tls.keyfile must be set")
}
}
// If TLS and Let's Encrypt certificate is enabled, we require a public hostname
if d.TLS.Enable && d.TLS.Auto {
if len(d.Host.Name) == 0 {
d.vars.Log("error", "host.name", "a hostname must be set in order to get an automatic TLS certificate")
} else {
r := &net.Resolver{
PreferGo: true,
StrictErrors: true,
}
for _, host := range d.Host.Name {
// Don't lookup IP addresses
if ip := net.ParseIP(host); ip != nil {
d.vars.Log("error", "host.name", "only host names are allowed if automatic TLS is enabled, but found IP address: %s", host)
}
// Lookup host name with a timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
_, err := r.LookupHost(ctx, host)
if err != nil {
d.vars.Log("error", "host.name", "the host '%s' can't be resolved and will not work with automatic TLS", host)
}
cancel()
}
}
}
// If TLS for RTMP is enabled, TLS must be enabled
if d.RTMP.EnableTLS {
if !d.RTMP.Enable {
d.vars.Log("error", "rtmp.enable", "RTMP server must be enabled if RTMPS server is enabled")
}
if !d.TLS.Enable {
d.vars.Log("error", "rtmp.enable_tls", "RTMPS server can only be enabled if TLS is enabled")
}
}
// If CORE_MEMFS_USERNAME and CORE_MEMFS_PASSWORD are set, automatically active/deactivate Basic-Auth for memfs
if d.vars.IsMerged("storage.memory.auth.username") && d.vars.IsMerged("storage.memory.auth.password") {
d.Storage.Memory.Auth.Enable = true
if len(d.Storage.Memory.Auth.Username) == 0 && len(d.Storage.Memory.Auth.Password) == 0 {
d.Storage.Memory.Auth.Enable = false
}
}
// If Basic-Auth for memfs is enable, check that the username and password are set
if d.Storage.Memory.Auth.Enable {
if len(d.Storage.Memory.Auth.Username) == 0 || len(d.Storage.Memory.Auth.Password) == 0 {
d.vars.Log("error", "storage.memory.auth.enable", "storage.memory.auth.username and storage.memory.auth.password must be set")
}
}
// If playout is enabled, check that the port range is sane
if d.Playout.Enable {
if d.Playout.MinPort >= d.Playout.MaxPort {
d.vars.Log("error", "playout.min_port", "must be bigger than playout.max_port")
}
}
// If cache is enabled, a valid TTL has to be set to a useful value
if d.Storage.Disk.Cache.Enable && d.Storage.Disk.Cache.TTL < 0 {
d.vars.Log("error", "storage.disk.cache.ttl_seconds", "must be equal or greater than 0")
}
// If the stats are enabled, the session timeout has to be set to a useful value
if d.Sessions.Enable && d.Sessions.SessionTimeout < 1 {
d.vars.Log("error", "stats.session_timeout_sec", "must be equal or greater than 1")
}
// If the stats and their persistence are enabled, the persist interval has to be set to a useful value
if d.Sessions.Enable && d.Sessions.PersistInterval < 0 {
d.vars.Log("error", "stats.persist_interval_sec", "must be at equal or greater than 0")
}
// If the service is enabled, the token and enpoint have to be defined
if d.Service.Enable {
if len(d.Service.Token) == 0 {
d.vars.Log("error", "service.token", "must be non-empty")
}
if len(d.Service.URL) == 0 {
d.vars.Log("error", "service.url", "must be non-empty")
}
}
// If historic metrics are enabled, the timerange and interval have to be valid
if d.Metrics.Enable {
if d.Metrics.Range <= 0 {
d.vars.Log("error", "metrics.range", "must be greater 0")
}
if d.Metrics.Interval <= 0 {
d.vars.Log("error", "metrics.interval", "must be greater 0")
}
if d.Metrics.Interval > d.Metrics.Range {
d.vars.Log("error", "metrics.interval", "must be smaller than the range")
}
}
}
func (d *Config) Merge() {
d.vars.Merge()
}
func (d *Config) Messages(logger func(level string, v vars.Variable, message string)) {
d.vars.Messages(logger)
}
func (d *Config) HasErrors() bool {
return d.vars.HasErrors()
}
func (d *Config) Overrides() []string {
return d.vars.Overrides()
}

View File

@@ -1,12 +1,16 @@
package config package v1
import "time" import (
"time"
type dataV1 struct { "github.com/datarhei/core/v16/config/value"
)
type Data struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
LoadedAt time.Time `json:"-"` LoadedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"`
Version int64 `json:"version" jsonschema:"minimum=1,maximum=1"` Version int64 `json:"version" jsonschema:"minimum=1,maximum=1" format:"int64"`
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Address string `json:"address"` Address string `json:"address"`
@@ -14,7 +18,7 @@ type dataV1 struct {
Log struct { Log struct {
Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"` Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"`
Topics []string `json:"topics"` Topics []string `json:"topics"`
MaxLines int `json:"max_lines"` MaxLines int `json:"max_lines" format:"int"`
} `json:"log"` } `json:"log"`
DB struct { DB struct {
Dir string `json:"dir"` Dir string `json:"dir"`
@@ -45,7 +49,7 @@ type dataV1 struct {
} `json:"jwt"` } `json:"jwt"`
Auth0 struct { Auth0 struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Tenants []Auth0Tenant `json:"tenants"` Tenants []value.Auth0Tenant `json:"tenants"`
} `json:"auth0"` } `json:"auth0"`
} `json:"auth"` } `json:"auth"`
} `json:"api"` } `json:"api"`
@@ -59,12 +63,12 @@ type dataV1 struct {
Storage struct { Storage struct {
Disk struct { Disk struct {
Dir string `json:"dir"` Dir string `json:"dir"`
Size int64 `json:"max_size_mbytes"` Size int64 `json:"max_size_mbytes" format:"int64"`
Cache struct { Cache struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Size uint64 `json:"max_size_mbytes"` Size uint64 `json:"max_size_mbytes" format:"uint64"`
TTL int64 `json:"ttl_seconds"` TTL int64 `json:"ttl_seconds" format:"int64"`
FileSize uint64 `json:"max_file_size_mbytes"` FileSize uint64 `json:"max_file_size_mbytes" format:"uint64"`
Types []string `json:"types"` Types []string `json:"types"`
} `json:"cache"` } `json:"cache"`
} `json:"disk"` } `json:"disk"`
@@ -74,7 +78,7 @@ type dataV1 struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} `json:"auth"` } `json:"auth"`
Size int64 `json:"max_size_mbytes"` Size int64 `json:"max_size_mbytes" format:"int64"`
Purge bool `json:"purge"` Purge bool `json:"purge"`
} `json:"memory"` } `json:"memory"`
CORS struct { CORS struct {
@@ -101,7 +105,7 @@ type dataV1 struct {
} `json:"srt"` } `json:"srt"`
FFmpeg struct { FFmpeg struct {
Binary string `json:"binary"` Binary string `json:"binary"`
MaxProcesses int64 `json:"max_processes"` MaxProcesses int64 `json:"max_processes" format:"int64"`
Access struct { Access struct {
Input struct { Input struct {
Allow []string `json:"allow"` Allow []string `json:"allow"`
@@ -113,33 +117,33 @@ type dataV1 struct {
} `json:"output"` } `json:"output"`
} `json:"access"` } `json:"access"`
Log struct { Log struct {
MaxLines int `json:"max_lines"` MaxLines int `json:"max_lines" format:"int"`
MaxHistory int `json:"max_history"` MaxHistory int `json:"max_history" format:"int"`
} `json:"log"` } `json:"log"`
} `json:"ffmpeg"` } `json:"ffmpeg"`
Playout struct { Playout struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
MinPort int `json:"min_port"` MinPort int `json:"min_port" format:"int"`
MaxPort int `json:"max_port"` MaxPort int `json:"max_port" format:"int"`
} `json:"playout"` } `json:"playout"`
Debug struct { Debug struct {
Profiling bool `json:"profiling"` Profiling bool `json:"profiling"`
ForceGC int `json:"force_gc"` ForceGC int `json:"force_gc" format:"int"`
} `json:"debug"` } `json:"debug"`
Metrics struct { Metrics struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
EnablePrometheus bool `json:"enable_prometheus"` EnablePrometheus bool `json:"enable_prometheus"`
Range int64 `json:"range_sec"` // seconds Range int64 `json:"range_sec" format:"int64"` // seconds
Interval int64 `json:"interval_sec"` // seconds Interval int64 `json:"interval_sec" format:"int64"` // seconds
} `json:"metrics"` } `json:"metrics"`
Sessions struct { Sessions struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
IPIgnoreList []string `json:"ip_ignorelist"` IPIgnoreList []string `json:"ip_ignorelist"`
SessionTimeout int `json:"session_timeout_sec"` SessionTimeout int `json:"session_timeout_sec" format:"int"`
Persist bool `json:"persist"` Persist bool `json:"persist"`
PersistInterval int `json:"persist_interval_sec"` PersistInterval int `json:"persist_interval_sec" format:"int"`
MaxBitrate uint64 `json:"max_bitrate_mbit"` MaxBitrate uint64 `json:"max_bitrate_mbit" format:"uint64"`
MaxSessions uint64 `json:"max_sessions"` MaxSessions uint64 `json:"max_sessions" format:"uint64"`
} `json:"sessions"` } `json:"sessions"`
Service struct { Service struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`

406
config/v2/config.go Normal file
View File

@@ -0,0 +1,406 @@
package v2
import (
"context"
"net"
"time"
"github.com/datarhei/core/v16/config/copy"
"github.com/datarhei/core/v16/config/value"
"github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/io/fs"
"github.com/datarhei/core/v16/math/rand"
haikunator "github.com/atrox/haikunatorgo/v2"
"github.com/google/uuid"
)
const version int64 = 2
// Make sure that the config.Config interface is satisfied
//var _ config.Config = &Config{}
// Config is a wrapper for Data
type Config struct {
fs fs.Filesystem
vars vars.Variables
Data
}
// New returns a Config which is initialized with its default values
func New(f fs.Filesystem) *Config {
cfg := &Config{
fs: f,
}
if cfg.fs == nil {
cfg.fs, _ = fs.NewMemFilesystem(fs.MemConfig{})
}
cfg.init()
return cfg
}
func (d *Config) Get(name string) (string, error) {
return d.vars.Get(name)
}
func (d *Config) Set(name, val string) error {
return d.vars.Set(name, val)
}
// NewConfigFrom returns a clone of a Config
func (d *Config) Clone() *Config {
data := New(d.fs)
data.CreatedAt = d.CreatedAt
data.LoadedAt = d.LoadedAt
data.UpdatedAt = d.UpdatedAt
data.Version = d.Version
data.ID = d.ID
data.Name = d.Name
data.Address = d.Address
data.CheckForUpdates = d.CheckForUpdates
data.Log = d.Log
data.DB = d.DB
data.Host = d.Host
data.API = d.API
data.TLS = d.TLS
data.Storage = d.Storage
data.RTMP = d.RTMP
data.SRT = d.SRT
data.FFmpeg = d.FFmpeg
data.Playout = d.Playout
data.Debug = d.Debug
data.Metrics = d.Metrics
data.Sessions = d.Sessions
data.Service = d.Service
data.Router = d.Router
data.Log.Topics = copy.Slice(d.Log.Topics)
data.Host.Name = copy.Slice(d.Host.Name)
data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow)
data.API.Access.HTTP.Block = copy.Slice(d.API.Access.HTTP.Block)
data.API.Access.HTTPS.Allow = copy.Slice(d.API.Access.HTTPS.Allow)
data.API.Access.HTTPS.Block = copy.Slice(d.API.Access.HTTPS.Block)
data.API.Auth.Auth0.Tenants = copy.TenantSlice(d.API.Auth.Auth0.Tenants)
data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.Storage.Disk.Cache.Types = copy.Slice(d.Storage.Disk.Cache.Types)
data.FFmpeg.Access.Input.Allow = copy.Slice(d.FFmpeg.Access.Input.Allow)
data.FFmpeg.Access.Input.Block = copy.Slice(d.FFmpeg.Access.Input.Block)
data.FFmpeg.Access.Output.Allow = copy.Slice(d.FFmpeg.Access.Output.Allow)
data.FFmpeg.Access.Output.Block = copy.Slice(d.FFmpeg.Access.Output.Block)
data.Sessions.IPIgnoreList = copy.Slice(d.Sessions.IPIgnoreList)
data.SRT.Log.Topics = copy.Slice(d.SRT.Log.Topics)
data.Router.BlockedPrefixes = copy.Slice(d.Router.BlockedPrefixes)
data.Router.Routes = copy.StringMap(d.Router.Routes)
data.vars.Transfer(&d.vars)
return data
}
func (d *Config) init() {
d.vars.Register(value.NewInt64(&d.Version, version), "version", "", nil, "Configuration file layout version", true, false)
d.vars.Register(value.NewTime(&d.CreatedAt, time.Now()), "created_at", "", nil, "Configuration file creation time", false, false)
d.vars.Register(value.NewString(&d.ID, uuid.New().String()), "id", "CORE_ID", nil, "ID for this instance", true, false)
d.vars.Register(value.NewString(&d.Name, haikunator.New().Haikunate()), "name", "CORE_NAME", nil, "A human readable name for this instance", false, false)
d.vars.Register(value.NewAddress(&d.Address, ":8080"), "address", "CORE_ADDRESS", nil, "HTTP listening address", false, false)
d.vars.Register(value.NewBool(&d.CheckForUpdates, true), "update_check", "CORE_UPDATE_CHECK", nil, "Check for updates and send anonymized data", false, false)
// Log
d.vars.Register(value.NewString(&d.Log.Level, "info"), "log.level", "CORE_LOG_LEVEL", nil, "Loglevel: silent, error, warn, info, debug", false, false)
d.vars.Register(value.NewStringList(&d.Log.Topics, []string{}, ","), "log.topics", "CORE_LOG_TOPICS", nil, "Show only selected log topics", false, false)
d.vars.Register(value.NewInt(&d.Log.MaxLines, 1000), "log.max_lines", "CORE_LOG_MAXLINES", nil, "Number of latest log lines to keep in memory", false, false)
// DB
d.vars.Register(value.NewMustDir(&d.DB.Dir, "./config", d.fs), "db.dir", "CORE_DB_DIR", nil, "Directory for holding the operational data", false, false)
// Host
d.vars.Register(value.NewStringList(&d.Host.Name, []string{}, ","), "host.name", "CORE_HOST_NAME", nil, "Comma separated list of public host/domain names or IPs", false, false)
d.vars.Register(value.NewBool(&d.Host.Auto, true), "host.auto", "CORE_HOST_AUTO", nil, "Enable detection of public IP addresses", false, false)
// API
d.vars.Register(value.NewBool(&d.API.ReadOnly, false), "api.read_only", "CORE_API_READ_ONLY", nil, "Allow only ready only access to the API", false, false)
d.vars.Register(value.NewCIDRList(&d.API.Access.HTTP.Allow, []string{}, ","), "api.access.http.allow", "CORE_API_ACCESS_HTTP_ALLOW", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false)
d.vars.Register(value.NewCIDRList(&d.API.Access.HTTP.Block, []string{}, ","), "api.access.http.block", "CORE_API_ACCESS_HTTP_BLOCK", nil, "List of IPs in CIDR notation (HTTP traffic)", false, false)
d.vars.Register(value.NewCIDRList(&d.API.Access.HTTPS.Allow, []string{}, ","), "api.access.https.allow", "CORE_API_ACCESS_HTTPS_ALLOW", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false)
d.vars.Register(value.NewCIDRList(&d.API.Access.HTTPS.Block, []string{}, ","), "api.access.https.block", "CORE_API_ACCESS_HTTPS_BLOCK", nil, "List of IPs in CIDR notation (HTTPS traffic)", false, false)
d.vars.Register(value.NewBool(&d.API.Auth.Enable, false), "api.auth.enable", "CORE_API_AUTH_ENABLE", nil, "Enable authentication for all clients", false, false)
d.vars.Register(value.NewBool(&d.API.Auth.DisableLocalhost, false), "api.auth.disable_localhost", "CORE_API_AUTH_DISABLE_LOCALHOST", nil, "Disable authentication for clients from localhost", false, false)
d.vars.Register(value.NewString(&d.API.Auth.Username, ""), "api.auth.username", "CORE_API_AUTH_USERNAME", []string{"RS_USERNAME"}, "Username", false, false)
d.vars.Register(value.NewString(&d.API.Auth.Password, ""), "api.auth.password", "CORE_API_AUTH_PASSWORD", []string{"RS_PASSWORD"}, "Password", false, true)
// Auth JWT
d.vars.Register(value.NewString(&d.API.Auth.JWT.Secret, rand.String(32)), "api.auth.jwt.secret", "CORE_API_AUTH_JWT_SECRET", nil, "JWT secret, leave empty for generating a random value", false, true)
// Auth Auth0
d.vars.Register(value.NewBool(&d.API.Auth.Auth0.Enable, false), "api.auth.auth0.enable", "CORE_API_AUTH_AUTH0_ENABLE", nil, "Enable Auth0", false, false)
d.vars.Register(value.NewTenantList(&d.API.Auth.Auth0.Tenants, []value.Auth0Tenant{}, ","), "api.auth.auth0.tenants", "CORE_API_AUTH_AUTH0_TENANTS", nil, "List of Auth0 tenants", false, false)
// TLS
d.vars.Register(value.NewAddress(&d.TLS.Address, ":8181"), "tls.address", "CORE_TLS_ADDRESS", nil, "HTTPS listening address", false, false)
d.vars.Register(value.NewBool(&d.TLS.Enable, false), "tls.enable", "CORE_TLS_ENABLE", nil, "Enable HTTPS", false, false)
d.vars.Register(value.NewBool(&d.TLS.Auto, false), "tls.auto", "CORE_TLS_AUTO", nil, "Enable Let's Encrypt certificate", false, false)
d.vars.Register(value.NewFile(&d.TLS.CertFile, "", d.fs), "tls.cert_file", "CORE_TLS_CERTFILE", nil, "Path to certificate file in PEM format", false, false)
d.vars.Register(value.NewFile(&d.TLS.KeyFile, "", d.fs), "tls.key_file", "CORE_TLS_KEYFILE", nil, "Path to key file in PEM format", false, false)
// Storage
d.vars.Register(value.NewFile(&d.Storage.MimeTypes, "./mime.types", d.fs), "storage.mimetypes_file", "CORE_STORAGE_MIMETYPES_FILE", []string{"CORE_MIMETYPES_FILE"}, "Path to file with mime-types", false, false)
// Storage (Disk)
d.vars.Register(value.NewMustDir(&d.Storage.Disk.Dir, "./data", d.fs), "storage.disk.dir", "CORE_STORAGE_DISK_DIR", nil, "Directory on disk, exposed on /", false, false)
d.vars.Register(value.NewInt64(&d.Storage.Disk.Size, 0), "storage.disk.max_size_mbytes", "CORE_STORAGE_DISK_MAXSIZEMBYTES", nil, "Max. allowed megabytes for storage.disk.dir, 0 for unlimited", false, false)
d.vars.Register(value.NewBool(&d.Storage.Disk.Cache.Enable, true), "storage.disk.cache.enable", "CORE_STORAGE_DISK_CACHE_ENABLE", nil, "Enable cache for /", false, false)
d.vars.Register(value.NewUint64(&d.Storage.Disk.Cache.Size, 0), "storage.disk.cache.max_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXSIZEMBYTES", nil, "Max. allowed cache size, 0 for unlimited", false, false)
d.vars.Register(value.NewInt64(&d.Storage.Disk.Cache.TTL, 300), "storage.disk.cache.ttl_seconds", "CORE_STORAGE_DISK_CACHE_TTLSECONDS", nil, "Seconds to keep files in cache", false, false)
d.vars.Register(value.NewUint64(&d.Storage.Disk.Cache.FileSize, 1), "storage.disk.cache.max_file_size_mbytes", "CORE_STORAGE_DISK_CACHE_MAXFILESIZEMBYTES", nil, "Max. file size to put in cache", false, false)
d.vars.Register(value.NewStringList(&d.Storage.Disk.Cache.Types, []string{}, " "), "storage.disk.cache.types", "CORE_STORAGE_DISK_CACHE_TYPES_ALLOW", []string{"CORE_STORAGE_DISK_CACHE_TYPES"}, "File extensions to cache, empty for all", false, false)
// Storage (Memory)
d.vars.Register(value.NewBool(&d.Storage.Memory.Auth.Enable, true), "storage.memory.auth.enable", "CORE_STORAGE_MEMORY_AUTH_ENABLE", nil, "Enable basic auth for PUT,POST, and DELETE on /memfs", false, false)
d.vars.Register(value.NewString(&d.Storage.Memory.Auth.Username, "admin"), "storage.memory.auth.username", "CORE_STORAGE_MEMORY_AUTH_USERNAME", nil, "Username for Basic-Auth of /memfs", false, false)
d.vars.Register(value.NewString(&d.Storage.Memory.Auth.Password, rand.StringAlphanumeric(18)), "storage.memory.auth.password", "CORE_STORAGE_MEMORY_AUTH_PASSWORD", nil, "Password for Basic-Auth of /memfs", false, true)
d.vars.Register(value.NewInt64(&d.Storage.Memory.Size, 0), "storage.memory.max_size_mbytes", "CORE_STORAGE_MEMORY_MAXSIZEMBYTES", nil, "Max. allowed megabytes for /memfs, 0 for unlimited", false, false)
d.vars.Register(value.NewBool(&d.Storage.Memory.Purge, false), "storage.memory.purge", "CORE_STORAGE_MEMORY_PURGE", nil, "Automatically remove the oldest files if /memfs is full", false, false)
// Storage (CORS)
d.vars.Register(value.NewCORSOrigins(&d.Storage.CORS.Origins, []string{"*"}, ","), "storage.cors.origins", "CORE_STORAGE_CORS_ORIGINS", nil, "Allowed CORS origins for /memfs and /data", false, false)
// RTMP
d.vars.Register(value.NewBool(&d.RTMP.Enable, false), "rtmp.enable", "CORE_RTMP_ENABLE", nil, "Enable RTMP server", false, false)
d.vars.Register(value.NewBool(&d.RTMP.EnableTLS, false), "rtmp.enable_tls", "CORE_RTMP_ENABLE_TLS", nil, "Enable RTMPS server instead of RTMP", false, false)
d.vars.Register(value.NewAddress(&d.RTMP.Address, ":1935"), "rtmp.address", "CORE_RTMP_ADDRESS", nil, "RTMP server listen address", false, false)
d.vars.Register(value.NewAddress(&d.RTMP.AddressTLS, ":1936"), "rtmp.address_tls", "CORE_RTMP_ADDRESS_TLS", nil, "RTMPS server listen address", false, false)
d.vars.Register(value.NewAbsolutePath(&d.RTMP.App, "/"), "rtmp.app", "CORE_RTMP_APP", nil, "RTMP app for publishing", false, false)
d.vars.Register(value.NewString(&d.RTMP.Token, ""), "rtmp.token", "CORE_RTMP_TOKEN", nil, "RTMP token for publishing and playing", false, true)
// SRT
d.vars.Register(value.NewBool(&d.SRT.Enable, false), "srt.enable", "CORE_SRT_ENABLE", nil, "Enable SRT server", false, false)
d.vars.Register(value.NewAddress(&d.SRT.Address, ":6000"), "srt.address", "CORE_SRT_ADDRESS", nil, "SRT server listen address", false, false)
d.vars.Register(value.NewString(&d.SRT.Passphrase, ""), "srt.passphrase", "CORE_SRT_PASSPHRASE", nil, "SRT encryption passphrase", false, true)
d.vars.Register(value.NewString(&d.SRT.Token, ""), "srt.token", "CORE_SRT_TOKEN", nil, "SRT token for publishing and playing", false, true)
d.vars.Register(value.NewBool(&d.SRT.Log.Enable, false), "srt.log.enable", "CORE_SRT_LOG_ENABLE", nil, "Enable SRT server logging", false, false)
d.vars.Register(value.NewStringList(&d.SRT.Log.Topics, []string{}, ","), "srt.log.topics", "CORE_SRT_LOG_TOPICS", nil, "List of topics to log", false, false)
// FFmpeg
d.vars.Register(value.NewExec(&d.FFmpeg.Binary, "ffmpeg", d.fs), "ffmpeg.binary", "CORE_FFMPEG_BINARY", nil, "Path to ffmpeg binary", true, false)
d.vars.Register(value.NewInt64(&d.FFmpeg.MaxProcesses, 0), "ffmpeg.max_processes", "CORE_FFMPEG_MAXPROCESSES", nil, "Max. allowed simultaneously running ffmpeg instances, 0 for unlimited", false, false)
d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Input.Allow, []string{}, " "), "ffmpeg.access.input.allow", "CORE_FFMPEG_ACCESS_INPUT_ALLOW", nil, "List of allowed expression to match against the input addresses", false, false)
d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Input.Block, []string{}, " "), "ffmpeg.access.input.block", "CORE_FFMPEG_ACCESS_INPUT_BLOCK", nil, "List of blocked expression to match against the input addresses", false, false)
d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Output.Allow, []string{}, " "), "ffmpeg.access.output.allow", "CORE_FFMPEG_ACCESS_OUTPUT_ALLOW", nil, "List of allowed expression to match against the output addresses", false, false)
d.vars.Register(value.NewStringList(&d.FFmpeg.Access.Output.Block, []string{}, " "), "ffmpeg.access.output.block", "CORE_FFMPEG_ACCESS_OUTPUT_BLOCK", nil, "List of blocked expression to match against the output addresses", false, false)
d.vars.Register(value.NewInt(&d.FFmpeg.Log.MaxLines, 50), "ffmpeg.log.max_lines", "CORE_FFMPEG_LOG_MAXLINES", nil, "Number of latest log lines to keep for each process", false, false)
d.vars.Register(value.NewInt(&d.FFmpeg.Log.MaxHistory, 3), "ffmpeg.log.max_history", "CORE_FFMPEG_LOG_MAXHISTORY", nil, "Number of latest logs to keep for each process", false, false)
// Playout
d.vars.Register(value.NewBool(&d.Playout.Enable, false), "playout.enable", "CORE_PLAYOUT_ENABLE", nil, "Enable playout proxy where available", false, false)
d.vars.Register(value.NewPort(&d.Playout.MinPort, 0), "playout.min_port", "CORE_PLAYOUT_MINPORT", nil, "Min. playout server port", false, false)
d.vars.Register(value.NewPort(&d.Playout.MaxPort, 0), "playout.max_port", "CORE_PLAYOUT_MAXPORT", nil, "Max. playout server port", false, false)
// Debug
d.vars.Register(value.NewBool(&d.Debug.Profiling, false), "debug.profiling", "CORE_DEBUG_PROFILING", nil, "Enable profiling endpoint on /profiling", false, false)
d.vars.Register(value.NewInt(&d.Debug.ForceGC, 0), "debug.force_gc", "CORE_DEBUG_FORCEGC", nil, "Number of seconds between forcing GC to return memory to the OS", false, false)
// Metrics
d.vars.Register(value.NewBool(&d.Metrics.Enable, false), "metrics.enable", "CORE_METRICS_ENABLE", nil, "Enable collecting historic metrics data", false, false)
d.vars.Register(value.NewBool(&d.Metrics.EnablePrometheus, false), "metrics.enable_prometheus", "CORE_METRICS_ENABLE_PROMETHEUS", nil, "Enable prometheus endpoint /metrics", false, false)
d.vars.Register(value.NewInt64(&d.Metrics.Range, 300), "metrics.range_seconds", "CORE_METRICS_RANGE_SECONDS", nil, "Seconds to keep history data", false, false)
d.vars.Register(value.NewInt64(&d.Metrics.Interval, 2), "metrics.interval_seconds", "CORE_METRICS_INTERVAL_SECONDS", nil, "Interval for collecting metrics", false, false)
// Sessions
d.vars.Register(value.NewBool(&d.Sessions.Enable, true), "sessions.enable", "CORE_SESSIONS_ENABLE", nil, "Enable collecting HLS session stats for /memfs", false, false)
d.vars.Register(value.NewCIDRList(&d.Sessions.IPIgnoreList, []string{"127.0.0.1/32", "::1/128"}, ","), "sessions.ip_ignorelist", "CORE_SESSIONS_IP_IGNORELIST", nil, "List of IP ranges in CIDR notation to ignore", false, false)
d.vars.Register(value.NewInt(&d.Sessions.SessionTimeout, 30), "sessions.session_timeout_sec", "CORE_SESSIONS_SESSION_TIMEOUT_SEC", nil, "Timeout for an idle session", false, false)
d.vars.Register(value.NewBool(&d.Sessions.Persist, false), "sessions.persist", "CORE_SESSIONS_PERSIST", nil, "Whether to persist session history. Will be stored as sessions.json in db.dir", false, false)
d.vars.Register(value.NewInt(&d.Sessions.PersistInterval, 300), "sessions.persist_interval_sec", "CORE_SESSIONS_PERSIST_INTERVAL_SEC", nil, "Interval in seconds in which to persist the current session history", false, false)
d.vars.Register(value.NewUint64(&d.Sessions.MaxBitrate, 0), "sessions.max_bitrate_mbit", "CORE_SESSIONS_MAXBITRATE_MBIT", nil, "Max. allowed outgoing bitrate in mbit/s, 0 for unlimited", false, false)
d.vars.Register(value.NewUint64(&d.Sessions.MaxSessions, 0), "sessions.max_sessions", "CORE_SESSIONS_MAXSESSIONS", nil, "Max. allowed number of simultaneous sessions, 0 for unlimited", false, false)
// Service
d.vars.Register(value.NewBool(&d.Service.Enable, false), "service.enable", "CORE_SERVICE_ENABLE", nil, "Enable connecting to the Restreamer Service", false, false)
d.vars.Register(value.NewString(&d.Service.Token, ""), "service.token", "CORE_SERVICE_TOKEN", nil, "Restreamer Service account token", false, true)
d.vars.Register(value.NewURL(&d.Service.URL, "https://service.datarhei.com"), "service.url", "CORE_SERVICE_URL", nil, "URL of the Restreamer Service", false, false)
// Router
d.vars.Register(value.NewStringList(&d.Router.BlockedPrefixes, []string{"/api"}, ","), "router.blocked_prefixes", "CORE_ROUTER_BLOCKED_PREFIXES", nil, "List of path prefixes that can't be routed", false, false)
d.vars.Register(value.NewStringMapString(&d.Router.Routes, nil), "router.routes", "CORE_ROUTER_ROUTES", nil, "List of route mappings", false, false)
d.vars.Register(value.NewDir(&d.Router.UIPath, "", d.fs), "router.ui_path", "CORE_ROUTER_UI_PATH", nil, "Path to a directory holding UI files mounted as /ui", false, false)
}
// Validate validates the current state of the Config for completeness and sanity. Errors are
// written to the log. Use resetLogs to indicate to reset the logs prior validation.
func (d *Config) Validate(resetLogs bool) {
if resetLogs {
d.vars.ResetLogs()
}
if d.Version != version {
d.vars.Log("error", "version", "unknown configuration layout version (found version %d, expecting version %d)", d.Version, version)
return
}
d.vars.Validate()
// Individual sanity checks
// If HTTP Auth is enabled, check that the username and password are set
if d.API.Auth.Enable {
if len(d.API.Auth.Username) == 0 || len(d.API.Auth.Password) == 0 {
d.vars.Log("error", "api.auth.enable", "api.auth.username and api.auth.password must be set")
}
}
// If Auth0 is enabled, check that domain, audience, and clientid are set
if d.API.Auth.Auth0.Enable {
if len(d.API.Auth.Auth0.Tenants) == 0 {
d.vars.Log("error", "api.auth.auth0.enable", "at least one tenants must be set")
}
for i, t := range d.API.Auth.Auth0.Tenants {
if len(t.Domain) == 0 || len(t.Audience) == 0 || len(t.ClientID) == 0 {
d.vars.Log("error", "api.auth.auth0.tenants", "domain, audience, and clientid must be set (tenant %d)", i)
}
}
}
// If TLS is enabled and Let's Encrypt is disabled, require certfile and keyfile
if d.TLS.Enable && !d.TLS.Auto {
if len(d.TLS.CertFile) == 0 || len(d.TLS.KeyFile) == 0 {
d.vars.Log("error", "tls.enable", "tls.certfile and tls.keyfile must be set")
}
}
// If TLS and Let's Encrypt certificate is enabled, we require a public hostname
if d.TLS.Enable && d.TLS.Auto {
if len(d.Host.Name) == 0 {
d.vars.Log("error", "host.name", "a hostname must be set in order to get an automatic TLS certificate")
} else {
r := &net.Resolver{
PreferGo: true,
StrictErrors: true,
}
for _, host := range d.Host.Name {
// Don't lookup IP addresses
if ip := net.ParseIP(host); ip != nil {
d.vars.Log("error", "host.name", "only host names are allowed if automatic TLS is enabled, but found IP address: %s", host)
}
// Lookup host name with a timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
_, err := r.LookupHost(ctx, host)
if err != nil {
d.vars.Log("error", "host.name", "the host '%s' can't be resolved and will not work with automatic TLS", host)
}
cancel()
}
}
}
// If TLS for RTMP is enabled, TLS must be enabled
if d.RTMP.EnableTLS {
if !d.RTMP.Enable {
d.vars.Log("error", "rtmp.enable", "RTMP server must be enabled if RTMPS server is enabled")
}
if !d.TLS.Enable {
d.vars.Log("error", "rtmp.enable_tls", "RTMPS server can only be enabled if TLS is enabled")
}
}
// If CORE_MEMFS_USERNAME and CORE_MEMFS_PASSWORD are set, automatically active/deactivate Basic-Auth for memfs
if d.vars.IsMerged("storage.memory.auth.username") && d.vars.IsMerged("storage.memory.auth.password") {
d.Storage.Memory.Auth.Enable = true
if len(d.Storage.Memory.Auth.Username) == 0 && len(d.Storage.Memory.Auth.Password) == 0 {
d.Storage.Memory.Auth.Enable = false
}
}
// If Basic-Auth for memfs is enable, check that the username and password are set
if d.Storage.Memory.Auth.Enable {
if len(d.Storage.Memory.Auth.Username) == 0 || len(d.Storage.Memory.Auth.Password) == 0 {
d.vars.Log("error", "storage.memory.auth.enable", "storage.memory.auth.username and storage.memory.auth.password must be set")
}
}
// If playout is enabled, check that the port range is sane
if d.Playout.Enable {
if d.Playout.MinPort >= d.Playout.MaxPort {
d.vars.Log("error", "playout.min_port", "must be bigger than playout.max_port")
}
}
// If cache is enabled, a valid TTL has to be set to a useful value
if d.Storage.Disk.Cache.Enable && d.Storage.Disk.Cache.TTL < 0 {
d.vars.Log("error", "storage.disk.cache.ttl_seconds", "must be equal or greater than 0")
}
// If the stats are enabled, the session timeout has to be set to a useful value
if d.Sessions.Enable && d.Sessions.SessionTimeout < 1 {
d.vars.Log("error", "stats.session_timeout_sec", "must be equal or greater than 1")
}
// If the stats and their persistence are enabled, the persist interval has to be set to a useful value
if d.Sessions.Enable && d.Sessions.PersistInterval < 0 {
d.vars.Log("error", "stats.persist_interval_sec", "must be at equal or greater than 0")
}
// If the service is enabled, the token and enpoint have to be defined
if d.Service.Enable {
if len(d.Service.Token) == 0 {
d.vars.Log("error", "service.token", "must be non-empty")
}
if len(d.Service.URL) == 0 {
d.vars.Log("error", "service.url", "must be non-empty")
}
}
// If historic metrics are enabled, the timerange and interval have to be valid
if d.Metrics.Enable {
if d.Metrics.Range <= 0 {
d.vars.Log("error", "metrics.range", "must be greater 0")
}
if d.Metrics.Interval <= 0 {
d.vars.Log("error", "metrics.interval", "must be greater 0")
}
if d.Metrics.Interval > d.Metrics.Range {
d.vars.Log("error", "metrics.interval", "must be smaller than the range")
}
}
}
func (d *Config) Merge() {
d.vars.Merge()
}
func (d *Config) Messages(logger func(level string, v vars.Variable, message string)) {
d.vars.Messages(logger)
}
func (d *Config) HasErrors() bool {
return d.vars.HasErrors()
}
func (d *Config) Overrides() []string {
return d.vars.Overrides()
}

View File

@@ -1,4 +1,4 @@
package config package v2
import ( import (
"fmt" "fmt"
@@ -6,13 +6,18 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/datarhei/core/v16/config/copy"
v1 "github.com/datarhei/core/v16/config/v1"
"github.com/datarhei/core/v16/config/value"
"github.com/datarhei/core/v16/io/fs"
) )
type dataV2 struct { type Data struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
LoadedAt time.Time `json:"-"` LoadedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"`
Version int64 `json:"version" jsonschema:"minimum=2,maximum=2"` Version int64 `json:"version" jsonschema:"minimum=2,maximum=2" format:"int64"`
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Address string `json:"address"` Address string `json:"address"`
@@ -20,7 +25,7 @@ type dataV2 struct {
Log struct { Log struct {
Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"` Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"`
Topics []string `json:"topics"` Topics []string `json:"topics"`
MaxLines int `json:"max_lines"` MaxLines int `json:"max_lines" format:"int"`
} `json:"log"` } `json:"log"`
DB struct { DB struct {
Dir string `json:"dir"` Dir string `json:"dir"`
@@ -51,7 +56,7 @@ type dataV2 struct {
} `json:"jwt"` } `json:"jwt"`
Auth0 struct { Auth0 struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Tenants []Auth0Tenant `json:"tenants"` Tenants []value.Auth0Tenant `json:"tenants"`
} `json:"auth0"` } `json:"auth0"`
} `json:"auth"` } `json:"auth"`
} `json:"api"` } `json:"api"`
@@ -65,12 +70,12 @@ type dataV2 struct {
Storage struct { Storage struct {
Disk struct { Disk struct {
Dir string `json:"dir"` Dir string `json:"dir"`
Size int64 `json:"max_size_mbytes"` Size int64 `json:"max_size_mbytes" format:"int64"`
Cache struct { Cache struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Size uint64 `json:"max_size_mbytes"` Size uint64 `json:"max_size_mbytes" format:"uint64"`
TTL int64 `json:"ttl_seconds"` TTL int64 `json:"ttl_seconds" format:"int64"`
FileSize uint64 `json:"max_file_size_mbytes"` FileSize uint64 `json:"max_file_size_mbytes" format:"uint64"`
Types []string `json:"types"` Types []string `json:"types"`
} `json:"cache"` } `json:"cache"`
} `json:"disk"` } `json:"disk"`
@@ -80,7 +85,7 @@ type dataV2 struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} `json:"auth"` } `json:"auth"`
Size int64 `json:"max_size_mbytes"` Size int64 `json:"max_size_mbytes" format:"int64"`
Purge bool `json:"purge"` Purge bool `json:"purge"`
} `json:"memory"` } `json:"memory"`
CORS struct { CORS struct {
@@ -108,7 +113,7 @@ type dataV2 struct {
} `json:"srt"` } `json:"srt"`
FFmpeg struct { FFmpeg struct {
Binary string `json:"binary"` Binary string `json:"binary"`
MaxProcesses int64 `json:"max_processes"` MaxProcesses int64 `json:"max_processes" format:"int64"`
Access struct { Access struct {
Input struct { Input struct {
Allow []string `json:"allow"` Allow []string `json:"allow"`
@@ -120,33 +125,33 @@ type dataV2 struct {
} `json:"output"` } `json:"output"`
} `json:"access"` } `json:"access"`
Log struct { Log struct {
MaxLines int `json:"max_lines"` MaxLines int `json:"max_lines" format:"int"`
MaxHistory int `json:"max_history"` MaxHistory int `json:"max_history" format:"int"`
} `json:"log"` } `json:"log"`
} `json:"ffmpeg"` } `json:"ffmpeg"`
Playout struct { Playout struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
MinPort int `json:"min_port"` MinPort int `json:"min_port" format:"int"`
MaxPort int `json:"max_port"` MaxPort int `json:"max_port" format:"int"`
} `json:"playout"` } `json:"playout"`
Debug struct { Debug struct {
Profiling bool `json:"profiling"` Profiling bool `json:"profiling"`
ForceGC int `json:"force_gc"` ForceGC int `json:"force_gc" format:"int"`
} `json:"debug"` } `json:"debug"`
Metrics struct { Metrics struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
EnablePrometheus bool `json:"enable_prometheus"` EnablePrometheus bool `json:"enable_prometheus"`
Range int64 `json:"range_sec"` // seconds Range int64 `json:"range_sec" format:"int64"` // seconds
Interval int64 `json:"interval_sec"` // seconds Interval int64 `json:"interval_sec" format:"int64"` // seconds
} `json:"metrics"` } `json:"metrics"`
Sessions struct { Sessions struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
IPIgnoreList []string `json:"ip_ignorelist"` IPIgnoreList []string `json:"ip_ignorelist"`
SessionTimeout int `json:"session_timeout_sec"` SessionTimeout int `json:"session_timeout_sec" format:"int"`
Persist bool `json:"persist"` Persist bool `json:"persist"`
PersistInterval int `json:"persist_interval_sec"` PersistInterval int `json:"persist_interval_sec" format:"int"`
MaxBitrate uint64 `json:"max_bitrate_mbit"` MaxBitrate uint64 `json:"max_bitrate_mbit" format:"uint64"`
MaxSessions uint64 `json:"max_sessions"` MaxSessions uint64 `json:"max_sessions" format:"uint64"`
} `json:"sessions"` } `json:"sessions"`
Service struct { Service struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
@@ -160,11 +165,15 @@ type dataV2 struct {
} `json:"router"` } `json:"router"`
} }
// Migrate will migrate some settings, depending on the version it finds. Migrations func UpgradeV1ToV2(d *v1.Data, fs fs.Filesystem) (*Data, error) {
// are only going upwards,i.e. from a lower version to a higher version. cfg := New(fs)
func NewV2FromV1(d *dataV1) (*dataV2, error) {
data := &dataV2{}
return MergeV1ToV2(&cfg.Data, d)
}
// Migrate will migrate some settings, depending on the version it finds. Migrations
// are only going upwards, i.e. from a lower version to a higher version.
func MergeV1ToV2(data *Data, d *v1.Data) (*Data, error) {
data.CreatedAt = d.CreatedAt data.CreatedAt = d.CreatedAt
data.LoadedAt = d.LoadedAt data.LoadedAt = d.LoadedAt
data.UpdatedAt = d.UpdatedAt data.UpdatedAt = d.UpdatedAt
@@ -189,30 +198,30 @@ func NewV2FromV1(d *dataV1) (*dataV2, error) {
data.Service = d.Service data.Service = d.Service
data.Router = d.Router data.Router = d.Router
data.Log.Topics = copyStringSlice(d.Log.Topics) data.Log.Topics = copy.Slice(d.Log.Topics)
data.Host.Name = copyStringSlice(d.Host.Name) data.Host.Name = copy.Slice(d.Host.Name)
data.API.Access.HTTP.Allow = copyStringSlice(d.API.Access.HTTP.Allow) data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow)
data.API.Access.HTTP.Block = copyStringSlice(d.API.Access.HTTP.Block) data.API.Access.HTTP.Block = copy.Slice(d.API.Access.HTTP.Block)
data.API.Access.HTTPS.Allow = copyStringSlice(d.API.Access.HTTPS.Allow) data.API.Access.HTTPS.Allow = copy.Slice(d.API.Access.HTTPS.Allow)
data.API.Access.HTTPS.Block = copyStringSlice(d.API.Access.HTTPS.Block) data.API.Access.HTTPS.Block = copy.Slice(d.API.Access.HTTPS.Block)
data.API.Auth.Auth0.Tenants = copyTenantSlice(d.API.Auth.Auth0.Tenants) data.API.Auth.Auth0.Tenants = copy.TenantSlice(d.API.Auth.Auth0.Tenants)
data.Storage.CORS.Origins = copyStringSlice(d.Storage.CORS.Origins) data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.FFmpeg.Access.Input.Allow = copyStringSlice(d.FFmpeg.Access.Input.Allow) data.FFmpeg.Access.Input.Allow = copy.Slice(d.FFmpeg.Access.Input.Allow)
data.FFmpeg.Access.Input.Block = copyStringSlice(d.FFmpeg.Access.Input.Block) data.FFmpeg.Access.Input.Block = copy.Slice(d.FFmpeg.Access.Input.Block)
data.FFmpeg.Access.Output.Allow = copyStringSlice(d.FFmpeg.Access.Output.Allow) data.FFmpeg.Access.Output.Allow = copy.Slice(d.FFmpeg.Access.Output.Allow)
data.FFmpeg.Access.Output.Block = copyStringSlice(d.FFmpeg.Access.Output.Block) data.FFmpeg.Access.Output.Block = copy.Slice(d.FFmpeg.Access.Output.Block)
data.Sessions.IPIgnoreList = copyStringSlice(d.Sessions.IPIgnoreList) data.Sessions.IPIgnoreList = copy.Slice(d.Sessions.IPIgnoreList)
data.SRT.Log.Topics = copyStringSlice(d.SRT.Log.Topics) data.SRT.Log.Topics = copy.Slice(d.SRT.Log.Topics)
data.Router.BlockedPrefixes = copyStringSlice(d.Router.BlockedPrefixes) data.Router.BlockedPrefixes = copy.Slice(d.Router.BlockedPrefixes)
data.Router.Routes = copyStringMap(d.Router.Routes) data.Router.Routes = copy.StringMap(d.Router.Routes)
// Actual changes // Actual changes
data.RTMP.Enable = d.RTMP.Enable data.RTMP.Enable = d.RTMP.Enable
@@ -245,3 +254,67 @@ func NewV2FromV1(d *dataV1) (*dataV2, error) {
return data, nil return data, nil
} }
func DowngradeV2toV1(d *Data) (*v1.Data, error) {
data := &v1.Data{}
data.CreatedAt = d.CreatedAt
data.LoadedAt = d.LoadedAt
data.UpdatedAt = d.UpdatedAt
data.ID = d.ID
data.Name = d.Name
data.Address = d.Address
data.CheckForUpdates = d.CheckForUpdates
data.Log = d.Log
data.DB = d.DB
data.Host = d.Host
data.API = d.API
data.TLS = d.TLS
data.Storage = d.Storage
data.SRT = d.SRT
data.FFmpeg = d.FFmpeg
data.Playout = d.Playout
data.Debug = d.Debug
data.Metrics = d.Metrics
data.Sessions = d.Sessions
data.Service = d.Service
data.Router = d.Router
data.Log.Topics = copy.Slice(d.Log.Topics)
data.Host.Name = copy.Slice(d.Host.Name)
data.API.Access.HTTP.Allow = copy.Slice(d.API.Access.HTTP.Allow)
data.API.Access.HTTP.Block = copy.Slice(d.API.Access.HTTP.Block)
data.API.Access.HTTPS.Allow = copy.Slice(d.API.Access.HTTPS.Allow)
data.API.Access.HTTPS.Block = copy.Slice(d.API.Access.HTTPS.Block)
data.API.Auth.Auth0.Tenants = copy.TenantSlice(d.API.Auth.Auth0.Tenants)
data.Storage.CORS.Origins = copy.Slice(d.Storage.CORS.Origins)
data.FFmpeg.Access.Input.Allow = copy.Slice(d.FFmpeg.Access.Input.Allow)
data.FFmpeg.Access.Input.Block = copy.Slice(d.FFmpeg.Access.Input.Block)
data.FFmpeg.Access.Output.Allow = copy.Slice(d.FFmpeg.Access.Output.Allow)
data.FFmpeg.Access.Output.Block = copy.Slice(d.FFmpeg.Access.Output.Block)
data.Sessions.IPIgnoreList = copy.Slice(d.Sessions.IPIgnoreList)
data.SRT.Log.Topics = copy.Slice(d.SRT.Log.Topics)
data.Router.BlockedPrefixes = copy.Slice(d.Router.BlockedPrefixes)
data.Router.Routes = copy.StringMap(d.Router.Routes)
// Actual changes
data.RTMP.Enable = d.RTMP.Enable
data.RTMP.EnableTLS = d.RTMP.EnableTLS
data.RTMP.Address = d.RTMP.Address
data.RTMP.App = d.RTMP.App
data.RTMP.Token = d.RTMP.Token
data.Version = 1
return data, nil
}

126
config/value/auth0.go Normal file
View File

@@ -0,0 +1,126 @@
package value
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
)
// array of auth0 tenants
type Auth0Tenant struct {
Domain string `json:"domain"`
Audience string `json:"audience"`
ClientID string `json:"clientid"`
Users []string `json:"users"`
}
func (a *Auth0Tenant) String() string {
u := url.URL{
Scheme: "auth0",
Host: a.Domain,
}
if len(a.ClientID) != 0 {
u.User = url.User(a.ClientID)
}
q := url.Values{}
q.Set("aud", a.Audience)
for _, user := range a.Users {
q.Add("user", user)
}
u.RawQuery = q.Encode()
return u.String()
}
type TenantList struct {
p *[]Auth0Tenant
separator string
}
func NewTenantList(p *[]Auth0Tenant, val []Auth0Tenant, separator string) *TenantList {
v := &TenantList{
p: p,
separator: separator,
}
*p = val
return v
}
// Set allows to set a tenant list in two formats:
// - a separator separated list of bas64 encoded Auth0Tenant JSON objects
// - a separator separated list of Auth0Tenant in URL representation: auth0://[clientid]@[domain]?aud=[audience]&user=...&user=...
func (s *TenantList) Set(val string) error {
list := []Auth0Tenant{}
for i, elm := range strings.Split(val, s.separator) {
t := Auth0Tenant{}
if strings.HasPrefix(elm, "auth0://") {
data, err := url.Parse(elm)
if err != nil {
return fmt.Errorf("invalid url encoding of tenant %d: %w", i, err)
}
t.Domain = data.Host
t.ClientID = data.User.Username()
t.Audience = data.Query().Get("aud")
t.Users = data.Query()["user"]
} else {
data, err := base64.StdEncoding.DecodeString(elm)
if err != nil {
return fmt.Errorf("invalid base64 encoding of tenant %d: %w", i, err)
}
if err := json.Unmarshal(data, &t); err != nil {
return fmt.Errorf("invalid JSON in tenant %d: %w", i, err)
}
}
list = append(list, t)
}
*s.p = list
return nil
}
func (s *TenantList) String() string {
if s.IsEmpty() {
return "(empty)"
}
list := []string{}
for _, t := range *s.p {
list = append(list, t.String())
}
return strings.Join(list, s.separator)
}
func (s *TenantList) Validate() error {
for i, t := range *s.p {
if len(t.Domain) == 0 {
return fmt.Errorf("the domain for tenant %d is missing", i)
}
if len(t.Audience) == 0 {
return fmt.Errorf("the audience for tenant %d is missing", i)
}
}
return nil
}
func (s *TenantList) IsEmpty() bool {
return len(*s.p) == 0
}

View File

@@ -0,0 +1,43 @@
package value
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestAuth0Value(t *testing.T) {
tenants := []Auth0Tenant{}
v := NewTenantList(&tenants, nil, " ")
require.Equal(t, "(empty)", v.String())
v.Set("auth0://clientid@domain?aud=audience&user=user1&user=user2 auth0://domain2?aud=audience2&user=user3")
require.Equal(t, []Auth0Tenant{
{
Domain: "domain",
ClientID: "clientid",
Audience: "audience",
Users: []string{"user1", "user2"},
},
{
Domain: "domain2",
Audience: "audience2",
Users: []string{"user3"},
},
}, tenants)
require.Equal(t, "auth0://clientid@domain?aud=audience&user=user1&user=user2 auth0://domain2?aud=audience2&user=user3", v.String())
require.NoError(t, v.Validate())
v.Set("eyJkb21haW4iOiJkYXRhcmhlaS5ldS5hdXRoMC5jb20iLCJhdWRpZW5jZSI6Imh0dHBzOi8vZGF0YXJoZWkuY29tL2NvcmUiLCJ1c2VycyI6WyJhdXRoMHx4eHgiXX0=")
require.Equal(t, []Auth0Tenant{
{
Domain: "datarhei.eu.auth0.com",
ClientID: "",
Audience: "https://datarhei.com/core",
Users: []string{"auth0|xxx"},
},
}, tenants)
require.Equal(t, "auth0://datarhei.eu.auth0.com?aud=https%3A%2F%2Fdatarhei.com%2Fcore&user=auth0%7Cxxx", v.String())
require.NoError(t, v.Validate())
}

277
config/value/network.go Normal file
View File

@@ -0,0 +1,277 @@
package value
import (
"fmt"
"net"
"net/mail"
"net/url"
"regexp"
"strconv"
"strings"
"github.com/datarhei/core/v16/http/cors"
)
// address (host?:port)
type Address string
func NewAddress(p *string, val string) *Address {
*p = val
return (*Address)(p)
}
func (s *Address) Set(val string) error {
// Check if the new value is only a port number
re := regexp.MustCompile("^[0-9]+$")
if re.MatchString(val) {
val = ":" + val
}
*s = Address(val)
return nil
}
func (s *Address) String() string {
return string(*s)
}
func (s *Address) Validate() error {
_, port, err := net.SplitHostPort(string(*s))
if err != nil {
return err
}
re := regexp.MustCompile("^[0-9]+$")
if !re.MatchString(port) {
return fmt.Errorf("the port must be numerical")
}
return nil
}
func (s *Address) IsEmpty() bool {
return s.Validate() != nil
}
// array of CIDR notation IP adresses
type CIDRList struct {
p *[]string
separator string
}
func NewCIDRList(p *[]string, val []string, separator string) *CIDRList {
v := &CIDRList{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *CIDRList) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *CIDRList) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *CIDRList) Validate() error {
for _, cidr := range *s.p {
_, _, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
}
return nil
}
func (s *CIDRList) IsEmpty() bool {
return len(*s.p) == 0
}
// array of origins for CORS
type CORSOrigins struct {
p *[]string
separator string
}
func NewCORSOrigins(p *[]string, val []string, separator string) *CORSOrigins {
v := &CORSOrigins{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *CORSOrigins) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *CORSOrigins) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *CORSOrigins) Validate() error {
return cors.Validate(*s.p)
}
func (s *CORSOrigins) IsEmpty() bool {
return len(*s.p) == 0
}
// network port
type Port int
func NewPort(p *int, val int) *Port {
*p = val
return (*Port)(p)
}
func (i *Port) Set(val string) error {
v, err := strconv.Atoi(val)
if err != nil {
return err
}
*i = Port(v)
return nil
}
func (i *Port) String() string {
return strconv.Itoa(int(*i))
}
func (i *Port) Validate() error {
val := int(*i)
if val < 0 || val >= (1<<16) {
return fmt.Errorf("%d is not in the range of [0, %d]", val, 1<<16-1)
}
return nil
}
func (i *Port) IsEmpty() bool {
return int(*i) == 0
}
// url
type URL string
func NewURL(p *string, val string) *URL {
*p = val
return (*URL)(p)
}
func (u *URL) Set(val string) error {
*u = URL(val)
return nil
}
func (u *URL) String() string {
return string(*u)
}
func (u *URL) Validate() error {
val := string(*u)
if len(val) == 0 {
return nil
}
URL, err := url.Parse(val)
if err != nil {
return fmt.Errorf("%s is not a valid URL", val)
}
if len(URL.Scheme) == 0 || len(URL.Host) == 0 {
return fmt.Errorf("%s is not a valid URL", val)
}
return nil
}
func (u *URL) IsEmpty() bool {
return len(string(*u)) == 0
}
// email address
type Email string
func NewEmail(p *string, val string) *Email {
*p = val
return (*Email)(p)
}
func (s *Email) Set(val string) error {
addr, err := mail.ParseAddress(val)
if err != nil {
return err
}
*s = Email(addr.Address)
return nil
}
func (s *Email) String() string {
return string(*s)
}
func (s *Email) Validate() error {
if len(s.String()) == 0 {
return nil
}
_, err := mail.ParseAddress(s.String())
return err
}
func (s *Email) IsEmpty() bool {
return len(string(*s)) == 0
}

View File

@@ -0,0 +1,127 @@
package value
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestAddressValue(t *testing.T) {
var x string
val := NewAddress(&x, ":8080")
require.Equal(t, ":8080", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = "foobaz:9090"
require.Equal(t, "foobaz:9090", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("fooboz:7070")
require.Equal(t, "fooboz:7070", x)
}
func TestCIDRListValue(t *testing.T) {
var x []string
val := NewCIDRList(&x, []string{}, " ")
require.Equal(t, "(empty)", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, true, val.IsEmpty())
x = []string{"127.0.0.1/32", "127.0.0.2/32"}
require.Equal(t, "127.0.0.1/32 127.0.0.2/32", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("129.0.0.1/32 129.0.0.2/32")
require.Equal(t, []string{"129.0.0.1/32", "129.0.0.2/32"}, x)
}
func TestCORSOriginaValue(t *testing.T) {
var x []string
val := NewCORSOrigins(&x, []string{}, " ")
require.Equal(t, "(empty)", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, true, val.IsEmpty())
x = []string{"*"}
require.Equal(t, "*", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("http://localhost")
require.Equal(t, []string{"http://localhost"}, x)
}
func TestPortValue(t *testing.T) {
var x int
val := NewPort(&x, 11)
require.Equal(t, "11", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = 42
require.Equal(t, "42", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("77")
require.Equal(t, int(77), x)
}
func TestURLValue(t *testing.T) {
var x string
val := NewURL(&x, "http://localhost/foobar")
require.Equal(t, "http://localhost/foobar", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = "http://localhost:8080/foobar"
require.Equal(t, "http://localhost:8080/foobar", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("http://localhost:8080/fooboz/foobaz")
require.Equal(t, "http://localhost:8080/fooboz/foobaz", x)
}
func TestEmailValue(t *testing.T) {
var x string
val := NewEmail(&x, "foobar@example.com")
require.Equal(t, "foobar@example.com", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = "foobar+baz@example.com"
require.Equal(t, "foobar+baz@example.com", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("foobar@sub.example.com")
require.Equal(t, "foobar@sub.example.com", x)
}

238
config/value/os.go Normal file
View File

@@ -0,0 +1,238 @@
package value
import (
"fmt"
"path/filepath"
"strings"
"github.com/datarhei/core/v16/io/fs"
)
// must directory
type MustDir struct {
p *string
fs fs.Filesystem
}
func NewMustDir(p *string, val string, fs fs.Filesystem) *MustDir {
v := &MustDir{
p: p,
fs: fs,
}
*p = val
return v
}
func (u *MustDir) Set(val string) error {
*u.p = val
return nil
}
func (u *MustDir) String() string {
return *u.p
}
func (u *MustDir) Validate() error {
val := *u.p
if len(strings.TrimSpace(val)) == 0 {
return fmt.Errorf("path name must not be empty")
}
if err := u.fs.MkdirAll(val, 0750); err != nil {
return fmt.Errorf("%s can't be created (%w)", val, err)
}
finfo, err := u.fs.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.IsDir() {
return fmt.Errorf("%s is not a directory", val)
}
return nil
}
func (u *MustDir) IsEmpty() bool {
return len(*u.p) == 0
}
// directory
type Dir struct {
p *string
fs fs.Filesystem
}
func NewDir(p *string, val string, fs fs.Filesystem) *Dir {
v := &Dir{
p: p,
fs: fs,
}
*p = val
return v
}
func (u *Dir) Set(val string) error {
*u.p = val
return nil
}
func (u *Dir) String() string {
return *u.p
}
func (u *Dir) Validate() error {
val := *u.p
if len(strings.TrimSpace(val)) == 0 {
return nil
}
finfo, err := u.fs.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.IsDir() {
return fmt.Errorf("%s is not a directory", val)
}
return nil
}
func (u *Dir) IsEmpty() bool {
return len(*u.p) == 0
}
// executable
type Exec struct {
p *string
fs fs.Filesystem
}
func NewExec(p *string, val string, fs fs.Filesystem) *Exec {
v := &Exec{
p: p,
fs: fs,
}
*p = val
return v
}
func (u *Exec) Set(val string) error {
*u.p = val
return nil
}
func (u *Exec) String() string {
return *u.p
}
func (u *Exec) Validate() error {
val := *u.p
_, err := u.fs.LookPath(val)
if err != nil {
return fmt.Errorf("%s not found or is not executable", val)
}
return nil
}
func (u *Exec) IsEmpty() bool {
return len(*u.p) == 0
}
// regular file
type File struct {
p *string
fs fs.Filesystem
}
func NewFile(p *string, val string, fs fs.Filesystem) *File {
v := &File{
p: p,
fs: fs,
}
*p = val
return v
}
func (u *File) Set(val string) error {
*u.p = val
return nil
}
func (u *File) String() string {
return *u.p
}
func (u *File) Validate() error {
val := *u.p
if len(val) == 0 {
return nil
}
finfo, err := u.fs.Stat(val)
if err != nil {
return fmt.Errorf("%s does not exist", val)
}
if !finfo.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", val)
}
return nil
}
func (u *File) IsEmpty() bool {
return len(*u.p) == 0
}
// absolute path
type AbsolutePath string
func NewAbsolutePath(p *string, val string) *AbsolutePath {
*p = filepath.Clean(val)
return (*AbsolutePath)(p)
}
func (s *AbsolutePath) Set(val string) error {
*s = AbsolutePath(filepath.Clean(val))
return nil
}
func (s *AbsolutePath) String() string {
return string(*s)
}
func (s *AbsolutePath) Validate() error {
path := string(*s)
if !filepath.IsAbs(path) {
return fmt.Errorf("%s is not an absolute path", path)
}
return nil
}
func (s *AbsolutePath) IsEmpty() bool {
return len(string(*s)) == 0
}

142
config/value/os_test.go Normal file
View File

@@ -0,0 +1,142 @@
package value
import (
"testing"
"github.com/datarhei/core/v16/io/fs"
"github.com/stretchr/testify/require"
)
func TestMustDirValue(t *testing.T) {
memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
_, err = memfs.Stat("/foobar")
require.Error(t, err)
var x string
val := NewMustDir(&x, "./foobar", memfs)
require.Equal(t, "./foobar", val.String())
require.NoError(t, val.Validate())
require.Equal(t, false, val.IsEmpty())
info, err := memfs.Stat("/foobar")
require.NoError(t, err)
require.True(t, info.IsDir())
x = "/bar/foo"
require.Equal(t, "/bar/foo", val.String())
_, err = memfs.Stat("/bar/foo")
require.Error(t, err)
require.NoError(t, val.Validate())
info, err = memfs.Stat("/bar/foo")
require.NoError(t, err)
require.True(t, info.IsDir())
memfs.WriteFile("/foo/bar", []byte("hello"))
val.Set("/foo/bar")
require.Error(t, val.Validate())
}
func TestDirValue(t *testing.T) {
memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
var x string
val := NewDir(&x, "/foobar", memfs)
require.Equal(t, "/foobar", val.String())
require.Error(t, val.Validate())
require.Equal(t, false, val.IsEmpty())
err = memfs.MkdirAll("/foobar", 0755)
require.NoError(t, err)
require.NoError(t, val.Validate())
_, _, err = memfs.WriteFile("/foo/bar", []byte("hello"))
require.NoError(t, err)
val.Set("/foo/bar")
require.Error(t, val.Validate())
}
func TestFileValue(t *testing.T) {
memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
var x string
val := NewFile(&x, "/foobar", memfs)
require.Equal(t, "/foobar", val.String())
require.Error(t, val.Validate())
require.Equal(t, false, val.IsEmpty())
_, _, err = memfs.WriteFile("/foobar", []byte("hello"))
require.NoError(t, err)
require.NoError(t, val.Validate())
err = memfs.MkdirAll("/foo/bar", 0755)
require.NoError(t, err)
val.Set("/foo/bar")
require.Error(t, val.Validate())
}
func TestExecValue(t *testing.T) {
memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
var x string
val := NewExec(&x, "/foobar", memfs)
require.Equal(t, "/foobar", val.String())
require.Error(t, val.Validate())
require.Equal(t, false, val.IsEmpty())
_, _, err = memfs.WriteFile("/foobar", []byte("hello"))
require.NoError(t, err)
require.NoError(t, val.Validate())
err = memfs.MkdirAll("/foo/bar", 0755)
require.NoError(t, err)
val.Set("/foo/bar")
require.Error(t, val.Validate())
}
func TestAbsolutePathValue(t *testing.T) {
var x string
val := NewAbsolutePath(&x, "foobar")
require.Equal(t, "foobar", val.String())
require.Error(t, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = "/foobaz"
require.Equal(t, "/foobaz", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("/fooboz")
require.Equal(t, "/fooboz", x)
}

281
config/value/primitives.go Normal file
View File

@@ -0,0 +1,281 @@
package value
import (
"sort"
"strconv"
"strings"
)
// string
type String string
func NewString(p *string, val string) *String {
*p = val
return (*String)(p)
}
func (s *String) Set(val string) error {
*s = String(val)
return nil
}
func (s *String) String() string {
return string(*s)
}
func (s *String) Validate() error {
return nil
}
func (s *String) IsEmpty() bool {
return len(string(*s)) == 0
}
// array of strings
type StringList struct {
p *[]string
separator string
}
func NewStringList(p *[]string, val []string, separator string) *StringList {
v := &StringList{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *StringList) Set(val string) error {
list := []string{}
for _, elm := range strings.Split(val, s.separator) {
elm = strings.TrimSpace(elm)
if len(elm) != 0 {
list = append(list, elm)
}
}
*s.p = list
return nil
}
func (s *StringList) String() string {
if s.IsEmpty() {
return "(empty)"
}
return strings.Join(*s.p, s.separator)
}
func (s *StringList) Validate() error {
return nil
}
func (s *StringList) IsEmpty() bool {
return len(*s.p) == 0
}
// map of strings to strings
type StringMapString struct {
p *map[string]string
}
func NewStringMapString(p *map[string]string, val map[string]string) *StringMapString {
v := &StringMapString{
p: p,
}
if *p == nil {
*p = make(map[string]string)
}
if val != nil {
*p = val
}
return v
}
func (s *StringMapString) Set(val string) error {
mappings := make(map[string]string)
for _, elm := range strings.Split(val, " ") {
elm = strings.TrimSpace(elm)
if len(elm) == 0 {
continue
}
mapping := strings.SplitN(elm, ":", 2)
mappings[mapping[0]] = mapping[1]
}
*s.p = mappings
return nil
}
func (s *StringMapString) String() string {
if s.IsEmpty() {
return "(empty)"
}
sms := *s.p
keys := []string{}
for k := range sms {
keys = append(keys, k)
}
sort.Strings(keys)
mappings := make([]string, len(*s.p))
i := 0
for _, k := range keys {
mappings[i] = k + ":" + sms[k]
i++
}
return strings.Join(mappings, " ")
}
func (s *StringMapString) Validate() error {
return nil
}
func (s *StringMapString) IsEmpty() bool {
return len(*s.p) == 0
}
// boolean
type Bool bool
func NewBool(p *bool, val bool) *Bool {
*p = val
return (*Bool)(p)
}
func (b *Bool) Set(val string) error {
v, err := strconv.ParseBool(val)
if err != nil {
return err
}
*b = Bool(v)
return nil
}
func (b *Bool) String() string {
return strconv.FormatBool(bool(*b))
}
func (b *Bool) Validate() error {
return nil
}
func (b *Bool) IsEmpty() bool {
return !bool(*b)
}
// int
type Int int
func NewInt(p *int, val int) *Int {
*p = val
return (*Int)(p)
}
func (i *Int) Set(val string) error {
v, err := strconv.Atoi(val)
if err != nil {
return err
}
*i = Int(v)
return nil
}
func (i *Int) String() string {
return strconv.Itoa(int(*i))
}
func (i *Int) Validate() error {
return nil
}
func (i *Int) IsEmpty() bool {
return int(*i) == 0
}
// int64
type Int64 int64
func NewInt64(p *int64, val int64) *Int64 {
*p = val
return (*Int64)(p)
}
func (u *Int64) Set(val string) error {
v, err := strconv.ParseInt(val, 0, 64)
if err != nil {
return err
}
*u = Int64(v)
return nil
}
func (u *Int64) String() string {
return strconv.FormatInt(int64(*u), 10)
}
func (u *Int64) Validate() error {
return nil
}
func (u *Int64) IsEmpty() bool {
return int64(*u) == 0
}
// uint64
type Uint64 uint64
func NewUint64(p *uint64, val uint64) *Uint64 {
*p = val
return (*Uint64)(p)
}
func (u *Uint64) Set(val string) error {
v, err := strconv.ParseUint(val, 0, 64)
if err != nil {
return err
}
*u = Uint64(v)
return nil
}
func (u *Uint64) String() string {
return strconv.FormatUint(uint64(*u), 10)
}
func (u *Uint64) Validate() error {
return nil
}
func (u *Uint64) IsEmpty() bool {
return uint64(*u) == 0
}

View File

@@ -0,0 +1,147 @@
package value
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestStringValue(t *testing.T) {
var x string
val := NewString(&x, "foobar")
require.Equal(t, "foobar", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = "foobaz"
require.Equal(t, "foobaz", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("fooboz")
require.Equal(t, "fooboz", x)
}
func TestStringListValue(t *testing.T) {
var x []string
val := NewStringList(&x, []string{"foobar"}, " ")
require.Equal(t, "foobar", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = []string{"foobar", "foobaz"}
require.Equal(t, "foobar foobaz", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("fooboz foobar")
require.Equal(t, []string{"fooboz", "foobar"}, x)
}
func TestStringMapStringValue(t *testing.T) {
var x map[string]string
val := NewStringMapString(&x, map[string]string{"a": "foobar"})
require.Equal(t, "a:foobar", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = map[string]string{"a": "foobar", "b": "foobaz"}
require.Equal(t, "a:foobar b:foobaz", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("x:fooboz y:foobar")
require.Equal(t, map[string]string{"x": "fooboz", "y": "foobar"}, x)
}
func TestBoolValue(t *testing.T) {
var x bool
val := NewBool(&x, false)
require.Equal(t, "false", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, true, val.IsEmpty())
x = true
require.Equal(t, "true", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("false")
require.Equal(t, false, x)
}
func TestIntValue(t *testing.T) {
var x int
val := NewInt(&x, 11)
require.Equal(t, "11", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = 42
require.Equal(t, "42", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("77")
require.Equal(t, int(77), x)
}
func TestInt64Value(t *testing.T) {
var x int64
val := NewInt64(&x, 11)
require.Equal(t, "11", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = 42
require.Equal(t, "42", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("77")
require.Equal(t, int64(77), x)
}
func TestUint64Value(t *testing.T) {
var x uint64
val := NewUint64(&x, 11)
require.Equal(t, "11", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = 42
require.Equal(t, "42", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("77")
require.Equal(t, uint64(77), x)
}

179
config/value/s3.go Normal file
View File

@@ -0,0 +1,179 @@
package value
import (
"fmt"
"net/url"
"strings"
"golang.org/x/net/publicsuffix"
)
// array of s3 storages
// https://access_key_id:secret_access_id@region.endpoint/bucket?name=aaa&mount=/abc&username=xxx&password=yyy
type S3Storage struct {
Name string `json:"name"`
Mountpoint string `json:"mountpoint"`
Auth struct {
Enable bool `json:"enable"`
Username string `json:"username"`
Password string `json:"password"`
} `json:"auth"`
Endpoint string `json:"endpoint"`
AccessKeyID string `json:"access_key_id"`
SecretAccessKey string `json:"secret_access_key"`
Bucket string `json:"bucket"`
Region string `json:"region"`
UseSSL bool `json:"use_ssl"`
}
func (t *S3Storage) String() string {
u := url.URL{}
if t.UseSSL {
u.Scheme = "https"
} else {
u.Scheme = "http"
}
u.User = url.UserPassword(t.AccessKeyID, "---")
u.Host = t.Endpoint
if len(t.Region) != 0 {
u.Host = t.Region + "." + u.Host
}
if len(t.Bucket) != 0 {
u.Path = "/" + t.Bucket
}
v := url.Values{}
v.Set("name", t.Name)
v.Set("mountpoint", t.Mountpoint)
if t.Auth.Enable {
if len(t.Auth.Username) != 0 {
v.Set("username", t.Auth.Username)
}
if len(t.Auth.Password) != 0 {
v.Set("password", "---")
}
}
u.RawQuery = v.Encode()
return u.String()
}
type s3StorageListValue struct {
p *[]S3Storage
separator string
}
func NewS3StorageListValue(p *[]S3Storage, val []S3Storage, separator string) *s3StorageListValue {
v := &s3StorageListValue{
p: p,
separator: separator,
}
*p = val
return v
}
func (s *s3StorageListValue) Set(val string) error {
list := []S3Storage{}
for _, elm := range strings.Split(val, s.separator) {
u, err := url.Parse(elm)
if err != nil {
return fmt.Errorf("invalid S3 storage URL (%s): %w", elm, err)
}
t := S3Storage{
Name: u.Query().Get("name"),
Mountpoint: u.Query().Get("mountpoint"),
AccessKeyID: u.User.Username(),
}
hostname := u.Hostname()
port := u.Port()
domain, err := publicsuffix.EffectiveTLDPlusOne(hostname)
if err != nil {
return fmt.Errorf("invalid eTLD (%s): %w", hostname, err)
}
t.Endpoint = domain
if len(port) != 0 {
t.Endpoint += ":" + port
}
region := strings.TrimSuffix(hostname, domain)
if len(region) != 0 {
t.Region = strings.TrimSuffix(region, ".")
}
secret, ok := u.User.Password()
if ok {
t.SecretAccessKey = secret
}
t.Bucket = strings.TrimPrefix(u.Path, "/")
if u.Scheme == "https" {
t.UseSSL = true
}
if u.Query().Has("username") || u.Query().Has("password") {
t.Auth.Enable = true
t.Auth.Username = u.Query().Get("username")
t.Auth.Username = u.Query().Get("password")
}
list = append(list, t)
}
*s.p = list
return nil
}
func (s *s3StorageListValue) String() string {
if s.IsEmpty() {
return "(empty)"
}
list := []string{}
for _, t := range *s.p {
list = append(list, t.String())
}
return strings.Join(list, s.separator)
}
func (s *s3StorageListValue) Validate() error {
for i, t := range *s.p {
if len(t.Name) == 0 {
return fmt.Errorf("the name for s3 storage %d is missing", i)
}
if len(t.Mountpoint) == 0 {
return fmt.Errorf("the mountpoint for s3 storage %d is missing", i)
}
if t.Auth.Enable {
if len(t.Auth.Username) == 0 && len(t.Auth.Password) == 0 {
return fmt.Errorf("auth is enabled, but no username and password are set for s3 storage %d", i)
}
}
}
return nil
}
func (s *s3StorageListValue) IsEmpty() bool {
return len(*s.p) == 0
}

36
config/value/time.go Normal file
View File

@@ -0,0 +1,36 @@
package value
import "time"
// time
type Time time.Time
func NewTime(p *time.Time, val time.Time) *Time {
*p = val
return (*Time)(p)
}
func (u *Time) Set(val string) error {
v, err := time.Parse(time.RFC3339, val)
if err != nil {
return err
}
*u = Time(v)
return nil
}
func (u *Time) String() string {
v := time.Time(*u)
return v.Format(time.RFC3339)
}
func (u *Time) Validate() error {
return nil
}
func (u *Time) IsEmpty() bool {
v := time.Time(*u)
return v.IsZero()
}

30
config/value/time_test.go Normal file
View File

@@ -0,0 +1,30 @@
package value
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestTimeValue(t *testing.T) {
var x time.Time
tm := time.Unix(1257894000, 0).UTC()
val := NewTime(&x, tm)
require.Equal(t, "2009-11-10T23:00:00Z", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
x = time.Unix(1257894001, 0).UTC()
require.Equal(t, "2009-11-10T23:00:01Z", val.String())
require.Equal(t, nil, val.Validate())
require.Equal(t, false, val.IsEmpty())
val.Set("2009-11-11T23:00:00Z")
require.Equal(t, time.Time(time.Date(2009, time.November, 11, 23, 0, 0, 0, time.UTC)), x)
}

21
config/value/value.go Normal file
View File

@@ -0,0 +1,21 @@
package value
type Value interface {
// String returns a string representation of the value.
String() string
// Set a new value for the value. Returns an
// error if the given string representation can't
// be transformed to the value. Returns nil
// if the new value has been set.
Set(string) error
// Validate the value. The returned error will
// indicate what is wrong with the current value.
// Returns nil if the value is OK.
Validate() error
// IsEmpty returns whether the value represents an empty
// representation for that value.
IsEmpty() bool
}

View File

@@ -0,0 +1,38 @@
package value
import (
"testing"
"github.com/stretchr/testify/require"
)
type testdata struct {
value1 int
value2 int
}
func TestCopyStruct(t *testing.T) {
data1 := testdata{}
NewInt(&data1.value1, 1)
NewInt(&data1.value2, 2)
require.Equal(t, int(1), data1.value1)
require.Equal(t, int(2), data1.value2)
data2 := testdata{}
val21 := NewInt(&data2.value1, 3)
val22 := NewInt(&data2.value2, 4)
require.Equal(t, int(3), data2.value1)
require.Equal(t, int(4), data2.value2)
data2 = data1
require.Equal(t, int(1), data2.value1)
require.Equal(t, int(2), data2.value2)
require.Equal(t, "1", val21.String())
require.Equal(t, "2", val22.String())
}

216
config/vars/vars.go Normal file
View File

@@ -0,0 +1,216 @@
package vars
import (
"fmt"
"os"
"github.com/datarhei/core/v16/config/value"
)
type variable struct {
value value.Value // The actual value
defVal string // The default value in string representation
name string // A name for this value
envName string // The environment variable that corresponds to this value
envAltNames []string // Alternative environment variable names
description string // A desriptions for this value
required bool // Whether a non-empty value is required
disguise bool // Whether the value should be disguised if printed
merged bool // Whether this value has been replaced by its corresponding environment variable
}
type Variable struct {
Value string
Name string
EnvName string
Description string
Merged bool
}
type message struct {
message string // The log message
variable Variable // The config field this message refers to
level string // The loglevel for this message
}
type Variables struct {
vars []*variable
logs []message
}
func (vs *Variables) Register(val value.Value, name, envName string, envAltNames []string, description string, required, disguise bool) {
vs.vars = append(vs.vars, &variable{
value: val,
defVal: val.String(),
name: name,
envName: envName,
envAltNames: envAltNames,
description: description,
required: required,
disguise: disguise,
})
}
func (vs *Variables) Transfer(vss *Variables) {
for _, v := range vs.vars {
if vss.IsMerged(v.name) {
v.merged = true
}
}
}
func (vs *Variables) SetDefault(name string) {
v := vs.findVariable(name)
if v == nil {
return
}
v.value.Set(v.defVal)
}
func (vs *Variables) Get(name string) (string, error) {
v := vs.findVariable(name)
if v == nil {
return "", fmt.Errorf("variable not found")
}
return v.value.String(), nil
}
func (vs *Variables) Set(name, val string) error {
v := vs.findVariable(name)
if v == nil {
return fmt.Errorf("variable not found")
}
return v.value.Set(val)
}
func (vs *Variables) Log(level, name string, format string, args ...interface{}) {
v := vs.findVariable(name)
if v == nil {
return
}
variable := Variable{
Value: v.value.String(),
Name: v.name,
EnvName: v.envName,
Description: v.description,
Merged: v.merged,
}
if v.disguise {
variable.Value = "***"
}
l := message{
message: fmt.Sprintf(format, args...),
variable: variable,
level: level,
}
vs.logs = append(vs.logs, l)
}
func (vs *Variables) Merge() {
for _, v := range vs.vars {
if len(v.envName) == 0 {
continue
}
var envval string
var ok bool
envval, ok = os.LookupEnv(v.envName)
if !ok {
foundAltName := false
for _, envName := range v.envAltNames {
envval, ok = os.LookupEnv(envName)
if ok {
foundAltName = true
vs.Log("warn", v.name, "deprecated name, please use %s", v.envName)
break
}
}
if !foundAltName {
continue
}
}
err := v.value.Set(envval)
if err != nil {
vs.Log("error", v.name, "%s", err.Error())
}
v.merged = true
}
}
func (vs *Variables) IsMerged(name string) bool {
v := vs.findVariable(name)
if v == nil {
return false
}
return v.merged
}
func (vs *Variables) Validate() {
for _, v := range vs.vars {
vs.Log("info", v.name, "%s", "")
err := v.value.Validate()
if err != nil {
vs.Log("error", v.name, "%s", err.Error())
}
if v.required && v.value.IsEmpty() {
vs.Log("error", v.name, "a value is required")
}
}
}
func (vs *Variables) ResetLogs() {
vs.logs = nil
}
func (vs *Variables) Messages(logger func(level string, v Variable, message string)) {
for _, l := range vs.logs {
logger(l.level, l.variable, l.message)
}
}
func (vs *Variables) HasErrors() bool {
for _, l := range vs.logs {
if l.level == "error" {
return true
}
}
return false
}
func (vs *Variables) Overrides() []string {
overrides := []string{}
for _, v := range vs.vars {
if v.merged {
overrides = append(overrides, v.name)
}
}
return overrides
}
func (vs *Variables) findVariable(name string) *variable {
for _, v := range vs.vars {
if v.name == name {
return v
}
}
return nil
}

248
config/vars/vars_test.go Normal file
View File

@@ -0,0 +1,248 @@
package vars
import (
"os"
"testing"
"github.com/datarhei/core/v16/config/value"
"github.com/stretchr/testify/require"
)
func TestVars(t *testing.T) {
v1 := Variables{}
s := ""
v1.Register(value.NewString(&s, "foobar"), "string", "", nil, "a string", false, false)
require.Equal(t, "foobar", s)
x, _ := v1.Get("string")
require.Equal(t, "foobar", x)
v := v1.findVariable("string")
v.value.Set("barfoo")
require.Equal(t, "barfoo", s)
x, _ = v1.Get("string")
require.Equal(t, "barfoo", x)
v1.Set("string", "foobaz")
require.Equal(t, "foobaz", s)
x, _ = v1.Get("string")
require.Equal(t, "foobaz", x)
v1.SetDefault("string")
require.Equal(t, "foobar", s)
x, _ = v1.Get("string")
require.Equal(t, "foobar", x)
}
func TestSetDefault(t *testing.T) {
v := Variables{}
s := ""
v.Register(value.NewString(&s, "foobar"), "string", "", nil, "a string", false, false)
require.Equal(t, "foobar", s)
v.Set("string", "foobaz")
require.Equal(t, "foobaz", s)
v.SetDefault("strong")
require.Equal(t, "foobaz", s)
v.SetDefault("string")
require.Equal(t, "foobar", s)
}
func TestGet(t *testing.T) {
v := Variables{}
s := ""
v.Register(value.NewString(&s, "foobar"), "string", "", nil, "a string", false, false)
value, err := v.Get("string")
require.NoError(t, err)
require.Equal(t, "foobar", value)
value, err = v.Get("strong")
require.Error(t, err)
require.Equal(t, "", value)
}
func TestSet(t *testing.T) {
v := Variables{}
s := ""
v.Register(value.NewString(&s, "foobar"), "string", "", nil, "a string", false, false)
err := v.Set("string", "foobaz")
require.NoError(t, err)
require.Equal(t, "foobaz", s)
err = v.Set("strong", "fooboz")
require.Error(t, err)
require.Equal(t, "foobaz", s)
}
func TestLog(t *testing.T) {
v := Variables{}
s := ""
v.Register(value.NewString(&s, "foobar"), "string", "", nil, "a string", false, false)
v.Log("info", "string", "hello %s", "world")
require.Equal(t, 1, len(v.logs))
v.Log("info", "strong", "hello %s", "world")
require.Equal(t, 1, len(v.logs))
require.Equal(t, "hello world", v.logs[0].message)
require.Equal(t, "info", v.logs[0].level)
require.Equal(t, Variable{
Value: "foobar",
Name: "string",
EnvName: "",
Description: "a string",
Merged: false,
}, v.logs[0].variable)
v.ResetLogs()
require.Equal(t, 0, len(v.logs))
}
func TestMerge(t *testing.T) {
v := Variables{}
s := ""
os.Setenv("CORE_TEST_STRING", "foobaz")
v.Register(value.NewString(&s, "foobar"), "string", "CORE_TEST_STRING", nil, "a string", false, false)
require.Equal(t, s, "foobar")
v.Merge()
require.Equal(t, s, "foobaz")
require.Equal(t, true, v.IsMerged("string"))
require.Equal(t, 0, len(v.logs))
os.Unsetenv("CORE_TEST_STRING")
}
func TestMergeAlt(t *testing.T) {
v := Variables{}
s := ""
os.Setenv("CORE_TEST_STRING", "foobaz")
v.Register(value.NewString(&s, "foobar"), "string", "CORE_TEST_STRUNG", []string{"CORE_TEST_STRING"}, "a string", false, false)
require.Equal(t, s, "foobar")
v.Merge()
require.Equal(t, s, "foobaz")
require.Equal(t, true, v.IsMerged("string"))
require.Equal(t, 1, len(v.logs))
require.Contains(t, v.logs[0].message, "CORE_TEST_STRUNG")
require.Equal(t, "warn", v.logs[0].level)
os.Unsetenv("CORE_TEST_STRING")
}
func TestNoMerge(t *testing.T) {
v := Variables{}
s := ""
os.Setenv("CORE_TEST_STRONG", "foobaz")
v.Register(value.NewString(&s, "foobar"), "string", "CORE_TEST_STRING", nil, "a string", false, false)
require.Equal(t, s, "foobar")
v.Merge()
require.Equal(t, s, "foobar")
require.Equal(t, false, v.IsMerged("string"))
os.Unsetenv("CORE_TEST_STRONG")
}
func TestValidate(t *testing.T) {
v := Variables{}
s1 := ""
s2 := ""
v.Register(value.NewString(&s1, ""), "string", "", nil, "a string", false, false)
v.Register(value.NewString(&s2, ""), "string", "", nil, "a string", true, false)
require.Equal(t, s1, "")
require.Equal(t, s2, "")
require.Equal(t, false, v.HasErrors())
v.Validate()
require.Equal(t, true, v.HasErrors())
ninfo := 0
nerror := 0
v.Messages(func(level string, v Variable, message string) {
if level == "info" {
ninfo++
} else if level == "error" {
nerror++
}
})
require.Equal(t, 2, ninfo)
require.Equal(t, 1, nerror)
}
func TestOverrides(t *testing.T) {
v := Variables{}
s := ""
os.Setenv("CORE_TEST_STRING", "foobaz")
v.Register(value.NewString(&s, "foobar"), "string", "CORE_TEST_STRING", nil, "a string", false, false)
v.Merge()
overrides := v.Overrides()
require.ElementsMatch(t, []string{"string"}, overrides)
}
func TestDisquise(t *testing.T) {
v := Variables{}
s := ""
v.Register(value.NewString(&s, "foobar"), "string", "", nil, "a string", false, true)
v.Log("info", "string", "hello %s", "world")
require.Equal(t, 1, len(v.logs))
require.Equal(t, "hello world", v.logs[0].message)
require.Equal(t, "info", v.logs[0].level)
require.Equal(t, Variable{
Value: "***",
Name: "string",
EnvName: "",
Description: "a string",
Merged: false,
}, v.logs[0].variable)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -356,7 +356,7 @@ func (p *parser) Parse(line string) uint64 {
if p.collector.IsCollectableIP(p.process.input[i].IP) { if p.collector.IsCollectableIP(p.process.input[i].IP) {
p.collector.Activate("") p.collector.Activate("")
p.collector.Ingress("", int64(p.stats.input[i].diff.size)*1024) p.collector.Ingress("", int64(p.stats.input[i].diff.size))
} }
} }
} }
@@ -373,19 +373,18 @@ func (p *parser) Parse(line string) uint64 {
if p.collector.IsCollectableIP(p.process.output[i].IP) { if p.collector.IsCollectableIP(p.process.output[i].IP) {
p.collector.Activate("") p.collector.Activate("")
p.collector.Egress("", int64(p.stats.output[i].diff.size)*1024) p.collector.Egress("", int64(p.stats.output[i].diff.size))
} }
} }
} }
// Calculate if any of the processed frames staled. // Calculate if any of the processed frames staled.
// If one number of frames in an output is the same as // If one number of frames in an output is the same as before, then pFrames becomes 0.
// before, then pFrames becomes 0. pFrames := p.stats.main.diff.frame
var pFrames uint64 = 0
pFrames = p.stats.main.diff.frame
if isFFmpegProgress { if isFFmpegProgress {
// Only consider the outputs
pFrames = 1
for i := range p.stats.output { for i := range p.stats.output {
pFrames *= p.stats.output[i].diff.frame pFrames *= p.stats.output[i].diff.frame
} }
@@ -411,7 +410,7 @@ func (p *parser) parseDefaultProgress(line string) error {
if matches = p.re.size.FindStringSubmatch(line); matches != nil { if matches = p.re.size.FindStringSubmatch(line); matches != nil {
if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil { if x, err := strconv.ParseUint(matches[1], 10, 64); err == nil {
p.progress.ffmpeg.Size = x p.progress.ffmpeg.Size = x * 1024
} }
} }
@@ -486,6 +485,26 @@ func (p *parser) parseFFmpegProgress(line string) error {
return fmt.Errorf("output length mismatch (have: %d, want: %d)", len(progress.Output), len(p.process.output)) return fmt.Errorf("output length mismatch (have: %d, want: %d)", len(progress.Output), len(p.process.output))
} }
if progress.Size == 0 {
progress.Size = progress.SizeKB * 1024
}
for i, io := range progress.Input {
if io.Size == 0 {
io.Size = io.SizeKB * 1024
}
progress.Input[i].Size = io.Size
}
for i, io := range progress.Output {
if io.Size == 0 {
io.Size = io.SizeKB * 1024
}
progress.Output[i].Size = io.Size
}
p.progress.ffmpeg = progress p.progress.ffmpeg = progress
return nil return nil

View File

@@ -1,11 +1,11 @@
package parse package parse
type statsData struct { type statsData struct {
frame uint64 frame uint64 // counter
packet uint64 packet uint64 // counter
size uint64 // kbytes size uint64 // bytes
dup uint64 dup uint64 // counter
drop uint64 drop uint64 // counter
} }
type stats struct { type stats struct {

View File

@@ -44,9 +44,9 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
type ffmpegAVstreamIO struct { type ffmpegAVstreamIO struct {
State string `json:"state"` State string `json:"state"`
Packet uint64 `json:"packet"` Packet uint64 `json:"packet"` // counter
Time uint64 `json:"time"` Time uint64 `json:"time"`
Size uint64 `json:"size_kb"` Size uint64 `json:"size_kb"` // kbytes
} }
func (avio *ffmpegAVstreamIO) export() app.AVstreamIO { func (avio *ffmpegAVstreamIO) export() app.AVstreamIO {
@@ -54,7 +54,7 @@ func (avio *ffmpegAVstreamIO) export() app.AVstreamIO {
State: avio.State, State: avio.State,
Packet: avio.Packet, Packet: avio.Packet,
Time: avio.Time, Time: avio.Time,
Size: avio.Size, Size: avio.Size * 1024,
} }
} }
@@ -93,12 +93,15 @@ type ffmpegProgressIO struct {
// common // common
Index uint64 `json:"index"` Index uint64 `json:"index"`
Stream uint64 `json:"stream"` Stream uint64 `json:"stream"`
Size uint64 `json:"size_kb"` // kbytes SizeKB uint64 `json:"size_kb"` // kbytes
Bitrate float64 `json:"-"` // kbit/s Size uint64 `json:"size_bytes"` // bytes
Frame uint64 `json:"frame"` Bitrate float64 `json:"-"` // bit/s
Packet uint64 `json:"packet"` Frame uint64 `json:"frame"` // counter
FPS float64 `json:"-"` Keyframe uint64 `json:"keyframe"` // counter
PPS float64 `json:"-"` Packet uint64 `json:"packet"` // counter
Extradata uint64 `json:"extradata_size_bytes"` // bytes
FPS float64 `json:"-"` // rate, frames per second
PPS float64 `json:"-"` // rate, packets per second
// video // video
Quantizer float64 `json:"q"` Quantizer float64 `json:"q"`
@@ -108,28 +111,36 @@ func (io *ffmpegProgressIO) exportTo(progress *app.ProgressIO) {
progress.Index = io.Index progress.Index = io.Index
progress.Stream = io.Stream progress.Stream = io.Stream
progress.Frame = io.Frame progress.Frame = io.Frame
progress.Keyframe = io.Keyframe
progress.Packet = io.Packet progress.Packet = io.Packet
progress.FPS = io.FPS progress.FPS = io.FPS
progress.PPS = io.PPS progress.PPS = io.PPS
progress.Quantizer = io.Quantizer progress.Quantizer = io.Quantizer
progress.Size = io.Size * 1024 progress.Bitrate = io.Bitrate
progress.Bitrate = io.Bitrate * 1024 progress.Extradata = io.Extradata
if io.Size == 0 {
progress.Size = io.SizeKB * 1024
} else {
progress.Size = io.Size
}
} }
type ffmpegProgress struct { type ffmpegProgress struct {
Input []ffmpegProgressIO `json:"inputs"` Input []ffmpegProgressIO `json:"inputs"`
Output []ffmpegProgressIO `json:"outputs"` Output []ffmpegProgressIO `json:"outputs"`
Frame uint64 `json:"frame"` Frame uint64 `json:"frame"` // counter
Packet uint64 `json:"packet"` Packet uint64 `json:"packet"` // counter
FPS float64 `json:"-"` FPS float64 `json:"-"` // rate, frames per second
PPS float64 `json:"-"` PPS float64 `json:"-"` // rate, packets per second
Quantizer float64 `json:"q"` Quantizer float64 `json:"q"`
Size uint64 `json:"size_kb"` // kbytes SizeKB uint64 `json:"size_kb"` // kbytes
Bitrate float64 `json:"-"` // kbit/s Size uint64 `json:"size_bytes"` // bytes
Bitrate float64 `json:"-"` // bit/s
Time Duration `json:"time"` Time Duration `json:"time"`
Speed float64 `json:"speed"` Speed float64 `json:"speed"`
Drop uint64 `json:"drop"` Drop uint64 `json:"drop"` // counter
Dup uint64 `json:"dup"` Dup uint64 `json:"dup"` // counter
} }
func (p *ffmpegProgress) exportTo(progress *app.Progress) { func (p *ffmpegProgress) exportTo(progress *app.Progress) {
@@ -138,13 +149,18 @@ func (p *ffmpegProgress) exportTo(progress *app.Progress) {
progress.FPS = p.FPS progress.FPS = p.FPS
progress.PPS = p.PPS progress.PPS = p.PPS
progress.Quantizer = p.Quantizer progress.Quantizer = p.Quantizer
progress.Size = p.Size * 1024
progress.Time = p.Time.Seconds() progress.Time = p.Time.Seconds()
progress.Bitrate = p.Bitrate * 1024 progress.Bitrate = p.Bitrate
progress.Speed = p.Speed progress.Speed = p.Speed
progress.Drop = p.Drop progress.Drop = p.Drop
progress.Dup = p.Dup progress.Dup = p.Dup
if p.Size == 0 {
progress.Size = p.SizeKB * 1024
} else {
progress.Size = p.Size
}
for i := range p.Input { for i := range p.Input {
if len(progress.Input) <= i { if len(progress.Input) <= i {
break break

View File

@@ -4,6 +4,9 @@ import (
"github.com/gobwas/glob" "github.com/gobwas/glob"
) )
// Match returns whether the name matches the glob pattern, also considering
// one or several optionnal separator. An error is only returned if the pattern
// is invalid.
func Match(pattern, name string, separators ...rune) (bool, error) { func Match(pattern, name string, separators ...rune) (bool, error) {
g, err := glob.Compile(pattern, separators...) g, err := glob.Compile(pattern, separators...)
if err != nil { if err != nil {

91
go.mod
View File

@@ -3,30 +3,33 @@ module github.com/datarhei/core/v16
go 1.18 go 1.18
require ( require (
github.com/99designs/gqlgen v0.17.16 github.com/99designs/gqlgen v0.17.20
github.com/Masterminds/semver/v3 v3.1.1 github.com/Masterminds/semver/v3 v3.1.1
github.com/atrox/haikunatorgo/v2 v2.0.1 github.com/atrox/haikunatorgo/v2 v2.0.1
github.com/caddyserver/certmagic v0.16.2 github.com/caddyserver/certmagic v0.17.2
github.com/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845 github.com/datarhei/gosrt v0.3.1
github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759 github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759
github.com/go-playground/validator/v10 v10.11.0 github.com/go-playground/validator/v10 v10.11.1
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v4 v4.4.2 github.com/golang-jwt/jwt/v4 v4.4.3
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/invopop/jsonschema v0.4.0 github.com/invopop/jsonschema v0.4.0
github.com/joho/godotenv v1.4.0 github.com/joho/godotenv v1.4.0
github.com/labstack/echo/v4 v4.9.0 github.com/labstack/echo/v4 v4.9.1
github.com/lithammer/shortuuid/v4 v4.0.0 github.com/lithammer/shortuuid/v4 v4.0.0
github.com/mattn/go-isatty v0.0.16 github.com/mattn/go-isatty v0.0.17
github.com/minio/minio-go/v7 v7.0.47
github.com/prep/average v0.0.0-20200506183628-d26c465f48c3 github.com/prep/average v0.0.0-20200506183628-d26c465f48c3
github.com/prometheus/client_golang v1.13.0 github.com/prometheus/client_golang v1.14.0
github.com/shirou/gopsutil/v3 v3.22.8 github.com/shirou/gopsutil/v3 v3.23.3
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.2
github.com/swaggo/echo-swagger v1.3.4 github.com/swaggo/echo-swagger v1.3.5
github.com/swaggo/swag v1.8.5 github.com/swaggo/swag v1.8.7
github.com/vektah/gqlparser/v2 v2.5.0 github.com/vektah/gqlparser/v2 v2.5.1
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 go.uber.org/zap v1.24.0
golang.org/x/mod v0.7.0
golang.org/x/net v0.7.0
) )
require ( require (
@@ -34,13 +37,14 @@ require (
github.com/agnivade/levenshtein v1.1.1 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.7 // indirect github.com/go-openapi/spec v0.20.8 // indirect
github.com/go-openapi/swag v0.22.3 // indirect github.com/go-openapi/swag v0.22.3 // 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
@@ -50,42 +54,53 @@ require (
github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/iancoleman/orderedmap v0.2.0 // indirect github.com/iancoleman/orderedmap v0.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.11 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/labstack/gommon v0.3.1 // indirect github.com/klauspost/compress v1.15.15 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/libdns/libdns v0.2.1 // indirect github.com/libdns/libdns v0.2.1 // indirect
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281 // indirect github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mholt/acmez v1.0.4 // indirect github.com/mholt/acmez v1.0.4 // indirect
github.com/miekg/dns v1.1.46 // indirect github.com/miekg/dns v1.1.50 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/prometheus/client_model v0.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/common v0.37.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.8.1 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shoenig/go-m1cpu v0.1.4 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.5.0 // indirect github.com/tklauser/numcpus v0.6.0 // indirect
github.com/urfave/cli/v2 v2.8.1 // indirect github.com/urfave/cli/v2 v2.8.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.6.0 // indirect go.uber.org/goleak v1.1.12 // indirect
go.uber.org/zap v1.21.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect golang.org/x/crypto v0.5.0 // indirect
golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 // indirect golang.org/x/sys v0.6.0 // indirect
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd // indirect golang.org/x/text v0.7.0 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.3.0 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect golang.org/x/tools v0.4.0 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

596
go.sum
View File

@@ -1,41 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/99designs/gqlgen v0.17.20 h1:O7WzccIhKB1dm+7g6dhQcULINftfiLSBg2l/mwbpJMw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/99designs/gqlgen v0.17.20/go.mod h1:Mja2HI23kWT1VRH09hvWshFgOzKswpO20o4ScpJIES4=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/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/99designs/gqlgen v0.17.16 h1:tTIw/cQ/uvf3iXIb2I6YSkdaDkmHmH2W2eZkVe0IVLA=
github.com/99designs/gqlgen v0.17.16/go.mod h1:dnJdUkgfh8iw8CEx2hhTdgTQO/GvVWKLcm/kult5gwI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
@@ -46,11 +12,6 @@ github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaW
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
@@ -61,27 +22,18 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4= github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caddyserver/certmagic v0.16.2 h1:k2n3LkkUG3aMUK/kckMuF9/0VFo+0FtMX3drPYESbmQ= github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE=
github.com/caddyserver/certmagic v0.16.2/go.mod h1:PgLIr/dSJa+WA7t7z6Je5xuS/e5A/GFCPHRuZ1QP+MQ= github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.1/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/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845 h1:nlVb4EVMwdVUwH6e10WZrx4lW0n2utnlE+4ILMPyD5o= github.com/datarhei/gosrt v0.3.1 h1:9A75hIvnY74IUFyeguqYXh1lsGF8Qt8fjxJS2Ewr12Q=
github.com/datarhei/gosrt v0.2.1-0.20220817080252-d44df04a3845/go.mod h1:wyoTu+DG45XRuCgEq/y+R8nhZCrJbOyQKn+SwNrNVZ8= github.com/datarhei/gosrt v0.3.1/go.mod h1:M2nl2WPrawncUc1FtUBK6gZX4tpZRC7FqL8NjOdBZV0=
github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759 h1:h8NyekuQSDvLIsZVTV172m5/RVArXkEM/cnHaUzszQU= github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759 h1:h8NyekuQSDvLIsZVTV172m5/RVArXkEM/cnHaUzszQU=
github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759/go.mod h1:Jcw/6jZDQQmPx8A7INEkXmuEF7E9jjBbSTfVSLwmiQw= github.com/datarhei/joy4 v0.0.0-20220914170649-23c70d207759/go.mod h1:Jcw/6jZDQQmPx8A7INEkXmuEF7E9jjBbSTfVSLwmiQw=
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=
@@ -89,22 +41,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -114,8 +53,8 @@ github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/a
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU=
github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
@@ -126,107 +65,50 @@ github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb
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=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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.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.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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 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/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.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.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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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 v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/invopop/jsonschema v0.4.0 h1:Yuy/unfgCnfV5Wl7H0HgFufp/rlurqPOOuacqyByrws= github.com/invopop/jsonschema v0.4.0 h1:Yuy/unfgCnfV5Wl7H0HgFufp/rlurqPOOuacqyByrws=
github.com/invopop/jsonschema v0.4.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= github.com/invopop/jsonschema v0.4.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/cpuid/v2 v2.0.11 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A= github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@@ -235,11 +117,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y=
github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo=
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
@@ -248,8 +131,8 @@ github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281 h1:aczX6NMOtt6L4YT0fQvKkDK6LZEtdOso9sUH89V1+P0= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281/go.mod h1:lc+czkgO/8F7puNki5jk8QyujbfK1LOT7Wl0ON2hxyk= github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@@ -257,28 +140,32 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.47 h1:sLiuCKGSIcn/MI6lREmTzX91DX/oRau4ia0j6e6eOSs=
github.com/minio/minio-go/v7 v7.0.47/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
@@ -286,94 +173,82 @@ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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.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/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prep/average v0.0.0-20200506183628-d26c465f48c3 h1:Y7qCvg282QmlyrVQuL2fgGwebuw7zvfnRym09r+dUGc= github.com/prep/average v0.0.0-20200506183628-d26c465f48c3 h1:Y7qCvg282QmlyrVQuL2fgGwebuw7zvfnRym09r+dUGc=
github.com/prep/average v0.0.0-20200506183628-d26c465f48c3/go.mod h1:0ZE5gcyWKS151WBDIpmLshHY0l+3edpuKnBUWVVbWKk= github.com/prep/average v0.0.0-20200506183628-d26c465f48c3/go.mod h1:0ZE5gcyWKS151WBDIpmLshHY0l+3edpuKnBUWVVbWKk=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil/v3 v3.22.8 h1:a4s3hXogo5mE2PfdfJIonDbstO/P+9JszdfhAHSzD9Y= github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
github.com/shirou/gopsutil/v3 v3.22.8/go.mod h1:s648gW4IywYzUfE/KjXxUsqrqx/T2xO5VqOXxONeRfI= github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
github.com/shoenig/go-m1cpu v0.1.4 h1:SZPIgRM2sEF9NJy50mRHu9PKGwxyyTTJIWvCtgVbozs=
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
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.1.1/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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
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/swaggo/echo-swagger v1.3.4 h1:8B+yVqjVm7cMy4QBLRUuRaOzrTVAqZahcrgrOSdpC5I= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/swaggo/echo-swagger v1.3.4/go.mod h1:vh8QAdbHtTXwTSaWzc1Nby7zMYJd/g0FwQyArmrFHA8= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/swaggo/echo-swagger v1.3.5 h1:kCx1wvX5AKhjI6Ykt48l3PTsfL9UD40ZROOx/tYzWyY=
github.com/swaggo/echo-swagger v1.3.5/go.mod h1:3IMHd2Z8KftdWFEEjGmv6QpWj370LwMCOfovuh7vF34=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.8.5 h1:7NgtfXsXE+jrcOwRyiftGKW7Ppydj7tZiVenuRf1fE4= github.com/swaggo/swag v1.8.7 h1:2K9ivTD3teEO+2fXV6zrZKDqk5IuU2aJtBDo8U7omWU=
github.com/swaggo/swag v1.8.5/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= github.com/swaggo/swag v1.8.7/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.5.0 h1:ooe7gN0fg6myJ0EKoTAf5hebTZrH52px3New/D9iJ+A= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKoflh+RQjo=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4= github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY= github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vektah/gqlparser/v2 v2.5.0 h1:GwEwy7AJsqPWrey0bHnn+3JLaHLZVT66wY/+O+Tf9SU= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/vektah/gqlparser/v2 v2.5.0/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4=
github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -383,174 +258,69 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
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-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/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.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/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-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-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-20190613194153-d28f0bde5980/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-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-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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 h1:1WGATo9HAhkWMbfyuVU0tEFP88OIkUvwaHFveQPvzCQ= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220907135653-1e95f45603a7/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
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-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-20201207232520-09787c993a3a/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 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/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-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/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-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/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-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -559,160 +329,49 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
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.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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/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-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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/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.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
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.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/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/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/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/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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -720,13 +379,12 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.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=
@@ -734,13 +392,3 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -6,7 +6,7 @@ import (
type AVstreamIO struct { type AVstreamIO struct {
State string `json:"state" enums:"running,idle" jsonschema:"enum=running,enum=idle"` State string `json:"state" enums:"running,idle" jsonschema:"enum=running,enum=idle"`
Packet uint64 `json:"packet"` Packet uint64 `json:"packet" format:"uint64"`
Time uint64 `json:"time"` Time uint64 `json:"time"`
Size uint64 `json:"size_kb"` Size uint64 `json:"size_kb"`
} }
@@ -25,11 +25,11 @@ func (i *AVstreamIO) Unmarshal(io *app.AVstreamIO) {
type AVstream struct { type AVstream struct {
Input AVstreamIO `json:"input"` Input AVstreamIO `json:"input"`
Output AVstreamIO `json:"output"` Output AVstreamIO `json:"output"`
Aqueue uint64 `json:"aqueue"` Aqueue uint64 `json:"aqueue" format:"uint64"`
Queue uint64 `json:"queue"` Queue uint64 `json:"queue" format:"uint64"`
Dup uint64 `json:"dup"` Dup uint64 `json:"dup" format:"uint64"`
Drop uint64 `json:"drop"` Drop uint64 `json:"drop" format:"uint64"`
Enc uint64 `json:"enc"` Enc uint64 `json:"enc" format:"uint64"`
Looping bool `json:"looping"` Looping bool `json:"looping"`
Duplicating bool `json:"duplicating"` Duplicating bool `json:"duplicating"`
GOP string `json:"gop"` GOP string `json:"gop"`

View File

@@ -4,8 +4,16 @@ import (
"time" "time"
"github.com/datarhei/core/v16/config" "github.com/datarhei/core/v16/config"
v1config "github.com/datarhei/core/v16/config/v1"
v2config "github.com/datarhei/core/v16/config/v2"
) )
// ConfigVersion is used to only unmarshal the version field in order
// find out which SetConfig should be used.
type ConfigVersion struct {
Version int64 `json:"version"`
}
// ConfigData embeds config.Data // ConfigData embeds config.Data
type ConfigData struct { type ConfigData struct {
config.Data config.Data
@@ -22,11 +30,68 @@ type Config struct {
Overrides []string `json:"overrides"` Overrides []string `json:"overrides"`
} }
type SetConfigV1 struct {
v1config.Data
}
// NewSetConfigV1 creates a new SetConfigV1 based on the current
// config with downgrading.
func NewSetConfigV1(cfg *config.Config) SetConfigV1 {
v2data, _ := config.DowngradeV3toV2(&cfg.Data)
v1data, _ := v2config.DowngradeV2toV1(v2data)
data := SetConfigV1{
Data: *v1data,
}
return data
}
// MergeTo merges the v1 config into the current config.
func (s *SetConfigV1) MergeTo(cfg *config.Config) {
v2data, _ := config.DowngradeV3toV2(&cfg.Data)
v2config.MergeV1ToV2(v2data, &s.Data)
config.MergeV2toV3(&cfg.Data, v2data)
}
type SetConfigV2 struct {
v2config.Data
}
// NewSetConfigV2 creates a new SetConfigV2 based on the current
// config with downgrading.
func NewSetConfigV2(cfg *config.Config) SetConfigV2 {
v2data, _ := config.DowngradeV3toV2(&cfg.Data)
data := SetConfigV2{
Data: *v2data,
}
return data
}
// MergeTo merges the v2 config into the current config.
func (s *SetConfigV2) MergeTo(cfg *config.Config) {
config.MergeV2toV3(&cfg.Data, &s.Data)
}
// SetConfig embeds config.Data. It is used to send a new config to the server. // SetConfig embeds config.Data. It is used to send a new config to the server.
type SetConfig struct { type SetConfig struct {
config.Data config.Data
} }
// NewSetConfig converts a config.Config into a SetConfig in order to prepopulate
// a SetConfig with the current values. The uploaded config can have missing fields that
// will be filled with the current values after unmarshalling the JSON.
func NewSetConfig(cfg *config.Config) SetConfig {
data := SetConfig{
cfg.Data,
}
return data
}
// MergeTo merges a sent config into a config.Config // MergeTo merges a sent config into a config.Config
func (rscfg *SetConfig) MergeTo(cfg *config.Config) { func (rscfg *SetConfig) MergeTo(cfg *config.Config) {
cfg.ID = rscfg.ID cfg.ID = rscfg.ID
@@ -51,18 +116,7 @@ func (rscfg *SetConfig) MergeTo(cfg *config.Config) {
cfg.Router = rscfg.Router cfg.Router = rscfg.Router
} }
// NewSetConfig converts a config.Config into a RestreamerSetConfig in order to prepopulate // Unmarshal converts a config.Config to a Config.
// a RestreamerSetConfig with the current values. The uploaded config can have missing fields that
// will be filled with the current values after unmarshalling the JSON.
func NewSetConfig(cfg *config.Config) SetConfig {
data := SetConfig{
cfg.Data,
}
return data
}
// Unmarshal converts a config.Config to a RestreamerConfig.
func (c *Config) Unmarshal(cfg *config.Config) { func (c *Config) Unmarshal(cfg *config.Config) {
if cfg == nil { if cfg == nil {
return return

View File

@@ -8,7 +8,7 @@ import (
// Error represents an error response of the API // Error represents an error response of the API
type Error struct { type Error struct {
Code int `json:"code" jsonschema:"required"` Code int `json:"code" jsonschema:"required" format:"int"`
Message string `json:"message" jsonschema:""` Message string `json:"message" jsonschema:""`
Details []string `json:"details" jsonschema:""` Details []string `json:"details" jsonschema:""`
} }

View File

@@ -3,6 +3,13 @@ package api
// FileInfo represents informatiion about a file on a filesystem // FileInfo represents informatiion about a file on a filesystem
type FileInfo struct { type FileInfo struct {
Name string `json:"name" jsonschema:"minLength=1"` Name string `json:"name" jsonschema:"minLength=1"`
Size int64 `json:"size_bytes" jsonschema:"minimum=0"` Size int64 `json:"size_bytes" jsonschema:"minimum=0" format:"int64"`
LastMod int64 `json:"last_modified" jsonschema:"minimum=0"` LastMod int64 `json:"last_modified" jsonschema:"minimum=0" format:"int64"`
}
// FilesystemInfo represents information about a filesystem
type FilesystemInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Mount string `json:"mount"`
} }

View File

@@ -19,8 +19,8 @@ type MetricsQueryMetric struct {
} }
type MetricsQuery struct { type MetricsQuery struct {
Timerange int64 `json:"timerange_sec"` Timerange int64 `json:"timerange_sec" format:"int64"`
Interval int64 `json:"interval_sec"` Interval int64 `json:"interval_sec" format:"int64"`
Metrics []MetricsQueryMetric `json:"metrics"` Metrics []MetricsQueryMetric `json:"metrics"`
} }
@@ -51,8 +51,8 @@ func (v MetricsResponseValue) MarshalJSON() ([]byte, error) {
} }
type MetricsResponse struct { type MetricsResponse struct {
Timerange int64 `json:"timerange_sec"` Timerange int64 `json:"timerange_sec" format:"int64"`
Interval int64 `json:"interval_sec"` Interval int64 `json:"interval_sec" format:"int64"`
Metrics []MetricsResponseMetric `json:"metrics"` Metrics []MetricsResponseMetric `json:"metrics"`
} }

View File

@@ -4,9 +4,9 @@ import "github.com/datarhei/core/v16/playout"
type PlayoutStatusIO struct { type PlayoutStatusIO struct {
State string `json:"state" enums:"running,idle" jsonschema:"enum=running,enum=idle"` State string `json:"state" enums:"running,idle" jsonschema:"enum=running,enum=idle"`
Packet uint64 `json:"packet"` Packet uint64 `json:"packet" format:"uint64"`
Time uint64 `json:"time"` Time uint64 `json:"time" format:"uint64"`
Size uint64 `json:"size_kb"` Size uint64 `json:"size_kb" format:"uint64"`
} }
func (i *PlayoutStatusIO) Unmarshal(io playout.StatusIO) { func (i *PlayoutStatusIO) Unmarshal(io playout.StatusIO) {
@@ -33,12 +33,12 @@ func (s *PlayoutStatusSwap) Unmarshal(swap playout.StatusSwap) {
type PlayoutStatus struct { type PlayoutStatus struct {
ID string `json:"id"` ID string `json:"id"`
Address string `json:"url"` Address string `json:"url"`
Stream uint64 `json:"stream"` Stream uint64 `json:"stream" format:"uint64"`
Queue uint64 `json:"queue"` Queue uint64 `json:"queue" format:"uint64"`
AQueue uint64 `json:"aqueue"` AQueue uint64 `json:"aqueue" format:"uint64"`
Dup uint64 `json:"dup"` Dup uint64 `json:"dup" format:"uint64"`
Drop uint64 `json:"drop"` Drop uint64 `json:"drop" format:"uint64"`
Enc uint64 `json:"enc"` Enc uint64 `json:"enc" format:"uint64"`
Looping bool `json:"looping"` Looping bool `json:"looping"`
Duplicating bool `json:"duplicating"` Duplicating bool `json:"duplicating"`
GOP string `json:"gop"` GOP string `json:"gop"`

View File

@@ -11,8 +11,8 @@ type ProbeIO struct {
// common // common
Address string `json:"url"` Address string `json:"url"`
Format string `json:"format"` Format string `json:"format"`
Index uint64 `json:"index"` Index uint64 `json:"index" format:"uint64"`
Stream uint64 `json:"stream"` Stream uint64 `json:"stream" format:"uint64"`
Language string `json:"language"` Language string `json:"language"`
Type string `json:"type"` Type string `json:"type"`
Codec string `json:"codec"` Codec string `json:"codec"`
@@ -23,13 +23,13 @@ type ProbeIO struct {
// video // video
FPS json.Number `json:"fps" swaggertype:"number" jsonschema:"type=number"` FPS json.Number `json:"fps" swaggertype:"number" jsonschema:"type=number"`
Pixfmt string `json:"pix_fmt"` Pixfmt string `json:"pix_fmt"`
Width uint64 `json:"width"` Width uint64 `json:"width" format:"uint64"`
Height uint64 `json:"height"` Height uint64 `json:"height" format:"uint64"`
// audio // audio
Sampling uint64 `json:"sampling_hz"` Sampling uint64 `json:"sampling_hz" format:"uint64"`
Layout string `json:"layout"` Layout string `json:"layout"`
Channels uint64 `json:"channels"` Channels uint64 `json:"channels" format:"uint64"`
} }
func (i *ProbeIO) Unmarshal(io *app.ProbeIO) { func (i *ProbeIO) Unmarshal(io *app.ProbeIO) {

View File

@@ -13,7 +13,7 @@ type Process struct {
ID string `json:"id" jsonschema:"minLength=1"` ID string `json:"id" jsonschema:"minLength=1"`
Type string `json:"type" jsonschema:"enum=ffmpeg"` Type string `json:"type" jsonschema:"enum=ffmpeg"`
Reference string `json:"reference"` Reference string `json:"reference"`
CreatedAt int64 `json:"created_at" jsonschema:"minimum=0"` CreatedAt int64 `json:"created_at" jsonschema:"minimum=0" format:"int64"`
Config *ProcessConfig `json:"config,omitempty"` Config *ProcessConfig `json:"config,omitempty"`
State *ProcessState `json:"state,omitempty"` State *ProcessState `json:"state,omitempty"`
Report *ProcessReport `json:"report,omitempty"` Report *ProcessReport `json:"report,omitempty"`
@@ -30,15 +30,15 @@ type ProcessConfigIO struct {
type ProcessConfigIOCleanup struct { type ProcessConfigIOCleanup struct {
Pattern string `json:"pattern" validate:"required"` Pattern string `json:"pattern" validate:"required"`
MaxFiles uint `json:"max_files"` MaxFiles uint `json:"max_files" format:"uint"`
MaxFileAge uint `json:"max_file_age_seconds"` MaxFileAge uint `json:"max_file_age_seconds" format:"uint"`
PurgeOnDelete bool `json:"purge_on_delete"` PurgeOnDelete bool `json:"purge_on_delete"`
} }
type ProcessConfigLimits struct { type ProcessConfigLimits struct {
CPU float64 `json:"cpu_usage" jsonschema:"minimum=0,maximum=100"` CPU float64 `json:"cpu_usage" jsonschema:"minimum=0,maximum=100"`
Memory uint64 `json:"memory_mbytes" jsonschema:"minimum=0"` Memory uint64 `json:"memory_mbytes" jsonschema:"minimum=0" format:"uint64"`
WaitFor uint64 `json:"waitfor_seconds" jsonschema:"minimum=0"` WaitFor uint64 `json:"waitfor_seconds" jsonschema:"minimum=0" format:"uint64"`
} }
// ProcessConfig represents the configuration of an ffmpeg process // ProcessConfig represents the configuration of an ffmpeg process
@@ -50,9 +50,9 @@ type ProcessConfig struct {
Output []ProcessConfigIO `json:"output" validate:"required"` Output []ProcessConfigIO `json:"output" validate:"required"`
Options []string `json:"options"` Options []string `json:"options"`
Reconnect bool `json:"reconnect"` Reconnect bool `json:"reconnect"`
ReconnectDelay uint64 `json:"reconnect_delay_seconds"` ReconnectDelay uint64 `json:"reconnect_delay_seconds" format:"uint64"`
Autostart bool `json:"autostart"` Autostart bool `json:"autostart"`
StaleTimeout uint64 `json:"stale_timeout_seconds"` StaleTimeout uint64 `json:"stale_timeout_seconds" format:"uint64"`
Limits ProcessConfigLimits `json:"limits"` Limits ProcessConfigLimits `json:"limits"`
} }
@@ -188,7 +188,7 @@ func (cfg *ProcessConfig) Unmarshal(c *app.Config) {
// ProcessReportHistoryEntry represents the logs of a run of a restream process // ProcessReportHistoryEntry represents the logs of a run of a restream process
type ProcessReportHistoryEntry struct { type ProcessReportHistoryEntry struct {
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at" format:"int64"`
Prelude []string `json:"prelude"` Prelude []string `json:"prelude"`
Log [][2]string `json:"log"` Log [][2]string `json:"log"`
} }
@@ -235,11 +235,11 @@ func (report *ProcessReport) Unmarshal(l *app.Log) {
type ProcessState struct { type ProcessState struct {
Order string `json:"order" jsonschema:"enum=start,enum=stop"` Order string `json:"order" jsonschema:"enum=start,enum=stop"`
State string `json:"exec" jsonschema:"enum=finished,enum=starting,enum=running,enum=finishing,enum=killed,enum=failed"` State string `json:"exec" jsonschema:"enum=finished,enum=starting,enum=running,enum=finishing,enum=killed,enum=failed"`
Runtime int64 `json:"runtime_seconds" jsonschema:"minimum=0"` Runtime int64 `json:"runtime_seconds" jsonschema:"minimum=0" format:"int64"`
Reconnect int64 `json:"reconnect_seconds"` Reconnect int64 `json:"reconnect_seconds" format:"int64"`
LastLog string `json:"last_logline"` LastLog string `json:"last_logline"`
Progress *Progress `json:"progress"` Progress *Progress `json:"progress"`
Memory uint64 `json:"memory_bytes"` Memory uint64 `json:"memory_bytes" format:"uint64"`
CPU json.Number `json:"cpu_usage" swaggertype:"number" jsonschema:"type=number"` CPU json.Number `json:"cpu_usage" swaggertype:"number" jsonschema:"type=number"`
Command []string `json:"command"` Command []string `json:"command"`
} }

View File

@@ -13,29 +13,31 @@ type ProgressIO struct {
Address string `json:"address" jsonschema:"minLength=1"` Address string `json:"address" jsonschema:"minLength=1"`
// General // General
Index uint64 `json:"index"` Index uint64 `json:"index" format:"uint64"`
Stream uint64 `json:"stream"` Stream uint64 `json:"stream" format:"uint64"`
Format string `json:"format"` Format string `json:"format"`
Type string `json:"type"` Type string `json:"type"`
Codec string `json:"codec"` Codec string `json:"codec"`
Coder string `json:"coder"` Coder string `json:"coder"`
Frame uint64 `json:"frame"` Frame uint64 `json:"frame" format:"uint64"`
Keyframe uint64 `json:"keyframe" format:"uint64"`
FPS json.Number `json:"fps" swaggertype:"number" jsonschema:"type=number"` FPS json.Number `json:"fps" swaggertype:"number" jsonschema:"type=number"`
Packet uint64 `json:"packet"` Packet uint64 `json:"packet" format:"uint64"`
PPS json.Number `json:"pps" swaggertype:"number" jsonschema:"type=number"` PPS json.Number `json:"pps" swaggertype:"number" jsonschema:"type=number"`
Size uint64 `json:"size_kb"` // kbytes Size uint64 `json:"size_kb" format:"uint64"` // kbytes
Bitrate json.Number `json:"bitrate_kbit" swaggertype:"number" jsonschema:"type=number"` // kbit/s Bitrate json.Number `json:"bitrate_kbit" swaggertype:"number" jsonschema:"type=number"` // kbit/s
Extradata uint64 `json:"extradata_size_bytes" format:"uint64"` // bytes
// Video // Video
Pixfmt string `json:"pix_fmt,omitempty"` Pixfmt string `json:"pix_fmt,omitempty"`
Quantizer json.Number `json:"q,omitempty" swaggertype:"number" jsonschema:"type=number"` Quantizer json.Number `json:"q,omitempty" swaggertype:"number" jsonschema:"type=number"`
Width uint64 `json:"width,omitempty"` Width uint64 `json:"width,omitempty" format:"uint64"`
Height uint64 `json:"height,omitempty"` Height uint64 `json:"height,omitempty" format:"uint64"`
// Audio // Audio
Sampling uint64 `json:"sampling_hz,omitempty"` Sampling uint64 `json:"sampling_hz,omitempty" format:"uint64"`
Layout string `json:"layout,omitempty"` Layout string `json:"layout,omitempty"`
Channels uint64 `json:"channels,omitempty"` Channels uint64 `json:"channels,omitempty" format:"uint64"`
// avstream // avstream
AVstream *AVstream `json:"avstream"` AVstream *AVstream `json:"avstream"`
@@ -56,11 +58,13 @@ func (i *ProgressIO) Unmarshal(io *app.ProgressIO) {
i.Codec = io.Codec i.Codec = io.Codec
i.Coder = io.Coder i.Coder = io.Coder
i.Frame = io.Frame i.Frame = io.Frame
i.Keyframe = io.Keyframe
i.FPS = json.Number(fmt.Sprintf("%.3f", io.FPS)) i.FPS = json.Number(fmt.Sprintf("%.3f", io.FPS))
i.Packet = io.Packet i.Packet = io.Packet
i.PPS = json.Number(fmt.Sprintf("%.3f", io.PPS)) i.PPS = json.Number(fmt.Sprintf("%.3f", io.PPS))
i.Size = io.Size / 1024 i.Size = io.Size / 1024
i.Bitrate = json.Number(fmt.Sprintf("%.3f", io.Bitrate/1024)) i.Bitrate = json.Number(fmt.Sprintf("%.3f", io.Bitrate/1024))
i.Extradata = io.Extradata
i.Pixfmt = io.Pixfmt i.Pixfmt = io.Pixfmt
i.Quantizer = json.Number(fmt.Sprintf("%.3f", io.Quantizer)) i.Quantizer = json.Number(fmt.Sprintf("%.3f", io.Quantizer))
i.Width = io.Width i.Width = io.Width
@@ -79,16 +83,16 @@ func (i *ProgressIO) Unmarshal(io *app.ProgressIO) {
type Progress struct { type Progress struct {
Input []ProgressIO `json:"inputs"` Input []ProgressIO `json:"inputs"`
Output []ProgressIO `json:"outputs"` Output []ProgressIO `json:"outputs"`
Frame uint64 `json:"frame"` Frame uint64 `json:"frame" format:"uint64"`
Packet uint64 `json:"packet"` Packet uint64 `json:"packet" format:"uint64"`
FPS json.Number `json:"fps" swaggertype:"number" jsonschema:"type=number"` FPS json.Number `json:"fps" swaggertype:"number" jsonschema:"type=number"`
Quantizer json.Number `json:"q" swaggertype:"number" jsonschema:"type=number"` Quantizer json.Number `json:"q" swaggertype:"number" jsonschema:"type=number"`
Size uint64 `json:"size_kb"` // kbytes Size uint64 `json:"size_kb" format:"uint64"` // kbytes
Time json.Number `json:"time" swaggertype:"number" jsonschema:"type=number"` Time json.Number `json:"time" swaggertype:"number" jsonschema:"type=number"`
Bitrate json.Number `json:"bitrate_kbit" swaggertype:"number" jsonschema:"type=number"` // kbit/s Bitrate json.Number `json:"bitrate_kbit" swaggertype:"number" jsonschema:"type=number"` // kbit/s
Speed json.Number `json:"speed" swaggertype:"number" jsonschema:"type=number"` Speed json.Number `json:"speed" swaggertype:"number" jsonschema:"type=number"`
Drop uint64 `json:"drop"` Drop uint64 `json:"drop" format:"uint64"`
Dup uint64 `json:"dup"` Dup uint64 `json:"dup" format:"uint64"`
} }
// Unmarshal converts a restreamer Progress to a Progress in API representation // Unmarshal converts a restreamer Progress to a Progress in API representation

View File

@@ -8,9 +8,9 @@ import (
// SessionStats are the accumulated numbers for the session summary // SessionStats are the accumulated numbers for the session summary
type SessionStats struct { type SessionStats struct {
TotalSessions uint64 `json:"sessions"` TotalSessions uint64 `json:"sessions" format:"uint64"`
TotalRxBytes uint64 `json:"traffic_rx_mb"` TotalRxBytes uint64 `json:"traffic_rx_mb" format:"uint64"`
TotalTxBytes uint64 `json:"traffic_tx_mb"` TotalTxBytes uint64 `json:"traffic_tx_mb" format:"uint64"`
} }
// SessionPeers is for the grouping by peers in the summary // SessionPeers is for the grouping by peers in the summary
@@ -24,12 +24,12 @@ type SessionPeers struct {
type Session struct { type Session struct {
ID string `json:"id"` ID string `json:"id"`
Reference string `json:"reference"` Reference string `json:"reference"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at" format:"int64"`
Location string `json:"local"` Location string `json:"local"`
Peer string `json:"remote"` Peer string `json:"remote"`
Extra string `json:"extra"` Extra string `json:"extra"`
RxBytes uint64 `json:"bytes_rx"` RxBytes uint64 `json:"bytes_rx" format:"uint64"`
TxBytes uint64 `json:"bytes_tx"` TxBytes uint64 `json:"bytes_tx" format:"uint64"`
RxBitrate json.Number `json:"bandwidth_rx_kbit" swaggertype:"number" jsonschema:"type=number"` // kbit/s RxBitrate json.Number `json:"bandwidth_rx_kbit" swaggertype:"number" jsonschema:"type=number"` // kbit/s
TxBitrate json.Number `json:"bandwidth_tx_kbit" swaggertype:"number" jsonschema:"type=number"` // kbit/s TxBitrate json.Number `json:"bandwidth_tx_kbit" swaggertype:"number" jsonschema:"type=number"` // kbit/s
} }
@@ -50,10 +50,10 @@ func (s *Session) Unmarshal(sess session.Session) {
// SessionSummaryActive represents the currently active sessions // SessionSummaryActive represents the currently active sessions
type SessionSummaryActive struct { type SessionSummaryActive struct {
SessionList []Session `json:"list"` SessionList []Session `json:"list"`
Sessions uint64 `json:"sessions"` Sessions uint64 `json:"sessions" format:"uint64"`
RxBitrate json.Number `json:"bandwidth_rx_mbit" swaggertype:"number" jsonschema:"type=number"` // mbit/s RxBitrate json.Number `json:"bandwidth_rx_mbit" swaggertype:"number" jsonschema:"type=number"` // mbit/s
TxBitrate json.Number `json:"bandwidth_tx_mbit" swaggertype:"number" jsonschema:"type=number"` // mbit/s TxBitrate json.Number `json:"bandwidth_tx_mbit" swaggertype:"number" jsonschema:"type=number"` // mbit/s
MaxSessions uint64 `json:"max_sessions"` MaxSessions uint64 `json:"max_sessions" format:"uint64"`
MaxRxBitrate json.Number `json:"max_bandwidth_rx_mbit" swaggertype:"number" jsonschema:"type=number"` // mbit/s MaxRxBitrate json.Number `json:"max_bandwidth_rx_mbit" swaggertype:"number" jsonschema:"type=number"` // mbit/s
MaxTxBitrate json.Number `json:"max_bandwidth_tx_mbit" swaggertype:"number" jsonschema:"type=number"` // mbit/s MaxTxBitrate json.Number `json:"max_bandwidth_tx_mbit" swaggertype:"number" jsonschema:"type=number"` // mbit/s
} }

View File

@@ -6,98 +6,118 @@ import (
// SRTStatistics represents the statistics of a SRT connection // SRTStatistics represents the statistics of a SRT connection
type SRTStatistics struct { type SRTStatistics struct {
MsTimeStamp uint64 `json:"timestamp_ms"` // The time elapsed, in milliseconds, since the SRT socket has been created MsTimeStamp uint64 `json:"timestamp_ms" format:"uint64"` // The time elapsed, in milliseconds, since the SRT socket has been created
// Accumulated // Accumulated
PktSent uint64 `json:"sent_pkt"` // The total number of sent DATA packets, including retransmitted packets PktSent uint64 `json:"sent_pkt" format:"uint64"` // The total number of sent DATA packets, including retransmitted packets
PktRecv uint64 `json:"recv_pkt"` // The total number of received DATA packets, including retransmitted packets PktRecv uint64 `json:"recv_pkt" format:"uint64"` // The total number of received DATA packets, including retransmitted packets
PktSentUnique uint64 `json:"sent_unique_pkt"` // The total number of unique DATA packets sent by the SRT sender PktSentUnique uint64 `json:"sent_unique_pkt" format:"uint64"` // The total number of unique DATA packets sent by the SRT sender
PktRecvUnique uint64 `json:"recv_unique_pkt"` // The total number of unique original, retransmitted or recovered by the packet filter DATA packets received in time, decrypted without errors and, as a result, scheduled for delivery to the upstream application by the SRT receiver. PktRecvUnique uint64 `json:"recv_unique_pkt" format:"uint64"` // The total number of unique original, retransmitted or recovered by the packet filter DATA packets received in time, decrypted without errors and, as a result, scheduled for delivery to the upstream application by the SRT receiver.
PktSndLoss uint64 `json:"send_loss_pkt"` // The total number of data packets considered or reported as lost at the sender side. Does not correspond to the packets detected as lost at the receiver side. PktSndLoss uint64 `json:"send_loss_pkt" format:"uint64"` // The total number of data packets considered or reported as lost at the sender side. Does not correspond to the packets detected as lost at the receiver side.
PktRcvLoss uint64 `json:"recv_loss_pkt"` // The total number of SRT DATA packets detected as presently missing (either reordered or lost) at the receiver side PktRcvLoss uint64 `json:"recv_loss_pkt" format:"uint64"` // The total number of SRT DATA packets detected as presently missing (either reordered or lost) at the receiver side
PktRetrans uint64 `json:"sent_retrans_pkt"` // The total number of retransmitted packets sent by the SRT sender PktRetrans uint64 `json:"sent_retrans_pkt" format:"uint64"` // The total number of retransmitted packets sent by the SRT sender
PktRcvRetrans uint64 `json:"recv_retran_pkts"` // The total number of retransmitted packets registered at the receiver side PktRcvRetrans uint64 `json:"recv_retran_pkts" format:"uint64"` // The total number of retransmitted packets registered at the receiver side
PktSentACK uint64 `json:"sent_ack_pkt"` // The total number of sent ACK (Acknowledgement) control packets PktSentACK uint64 `json:"sent_ack_pkt" format:"uint64"` // The total number of sent ACK (Acknowledgement) control packets
PktRecvACK uint64 `json:"recv_ack_pkt"` // The total number of received ACK (Acknowledgement) control packets PktRecvACK uint64 `json:"recv_ack_pkt" format:"uint64"` // The total number of received ACK (Acknowledgement) control packets
PktSentNAK uint64 `json:"sent_nak_pkt"` // The total number of sent NAK (Negative Acknowledgement) control packets PktSentNAK uint64 `json:"sent_nak_pkt" format:"uint64"` // The total number of sent NAK (Negative Acknowledgement) control packets
PktRecvNAK uint64 `json:"recv_nak_pkt"` // The total number of received NAK (Negative Acknowledgement) control packets PktRecvNAK uint64 `json:"recv_nak_pkt" format:"uint64"` // The total number of received NAK (Negative Acknowledgement) control packets
PktSentKM uint64 `json:"send_km_pkt"` // The total number of sent KM (Key Material) control packets PktSentKM uint64 `json:"send_km_pkt" format:"uint64"` // The total number of sent KM (Key Material) control packets
PktRecvKM uint64 `json:"recv_km_pkt"` // The total number of received KM (Key Material) control packets PktRecvKM uint64 `json:"recv_km_pkt" format:"uint64"` // The total number of received KM (Key Material) control packets
UsSndDuration uint64 `json:"send_duration_us"` // The total accumulated time in microseconds, during which the SRT sender has some data to transmit, including packets that have been sent, but not yet acknowledged UsSndDuration uint64 `json:"send_duration_us" format:"uint64"` // The total accumulated time in microseconds, during which the SRT sender has some data to transmit, including packets that have been sent, but not yet acknowledged
PktSndDrop uint64 `json:"send_drop_pkt"` // The total number of dropped by the SRT sender DATA packets that have no chance to be delivered in time PktSndDrop uint64 `json:"send_drop_pkt" format:"uint64"` // The total number of dropped by the SRT sender DATA packets that have no chance to be delivered in time
PktRcvDrop uint64 `json:"recv_drop_pkt"` // The total number of dropped by the SRT receiver and, as a result, not delivered to the upstream application DATA packets PktRcvDrop uint64 `json:"recv_drop_pkt" format:"uint64"` // The total number of dropped by the SRT receiver and, as a result, not delivered to the upstream application DATA packets
PktRcvUndecrypt uint64 `json:"recv_undecrypt_pkt"` // The total number of packets that failed to be decrypted at the receiver side PktRcvUndecrypt uint64 `json:"recv_undecrypt_pkt" format:"uint64"` // The total number of packets that failed to be decrypted at the receiver side
ByteSent uint64 `json:"sent_bytes"` // Same as pktSent, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) ByteSent uint64 `json:"sent_bytes" format:"uint64"` // Same as pktSent, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)
ByteRecv uint64 `json:"recv_bytes"` // Same as pktRecv, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) ByteRecv uint64 `json:"recv_bytes" format:"uint64"` // Same as pktRecv, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)
ByteSentUnique uint64 `json:"sent_unique__bytes"` // Same as pktSentUnique, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) ByteSentUnique uint64 `json:"sent_unique_bytes" format:"uint64"` // Same as pktSentUnique, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)
ByteRecvUnique uint64 `json:"recv_unique_bytes"` // Same as pktRecvUnique, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) ByteRecvUnique uint64 `json:"recv_unique_bytes" format:"uint64"` // Same as pktRecvUnique, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)
ByteRcvLoss uint64 `json:"recv_loss__bytes"` // Same as pktRcvLoss, but expressed in bytes, including payload and all the headers (IP, TCP, SRT), bytes for the presently missing (either reordered or lost) packets' payloads are estimated based on the average packet size ByteRcvLoss uint64 `json:"recv_loss_bytes" format:"uint64"` // Same as pktRcvLoss, but expressed in bytes, including payload and all the headers (IP, TCP, SRT), bytes for the presently missing (either reordered or lost) packets' payloads are estimated based on the average packet size
ByteRetrans uint64 `json:"sent_retrans_bytes"` // Same as pktRetrans, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) ByteRetrans uint64 `json:"sent_retrans_bytes" format:"uint64"` // Same as pktRetrans, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)
ByteSndDrop uint64 `json:"send_drop_bytes"` // Same as pktSndDrop, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) ByteSndDrop uint64 `json:"send_drop_bytes" format:"uint64"` // Same as pktSndDrop, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)
ByteRcvDrop uint64 `json:"recv_drop_bytes"` // Same as pktRcvDrop, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) ByteRcvDrop uint64 `json:"recv_drop_bytes" format:"uint64"` // Same as pktRcvDrop, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)
ByteRcvUndecrypt uint64 `json:"recv_undecrypt_bytes"` // Same as pktRcvUndecrypt, but expressed in bytes, including payload and all the headers (IP, TCP, SRT) ByteRcvUndecrypt uint64 `json:"recv_undecrypt_bytes" format:"uint64"` // Same as pktRcvUndecrypt, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)
// Instantaneous // Instantaneous
UsPktSndPeriod float64 `json:"pkt_send_period_us"` // Current minimum time interval between which consecutive packets are sent, in microseconds UsPktSndPeriod float64 `json:"pkt_send_period_us"` // Current minimum time interval between which consecutive packets are sent, in microseconds
PktFlowWindow uint64 `json:"flow_window_pkt"` // The maximum number of packets that can be "in flight" PktFlowWindow uint64 `json:"flow_window_pkt" format:"uint64"` // The maximum number of packets that can be "in flight"
PktFlightSize uint64 `json:"flight_size_pkt"` // The number of packets in flight PktFlightSize uint64 `json:"flight_size_pkt" format:"uint64"` // The number of packets in flight
MsRTT float64 `json:"rtt_ms"` // Smoothed round-trip time (SRTT), an exponentially-weighted moving average (EWMA) of an endpoint's RTT samples, in milliseconds MsRTT float64 `json:"rtt_ms"` // Smoothed round-trip time (SRTT), an exponentially-weighted moving average (EWMA) of an endpoint's RTT samples, in milliseconds
MbpsBandwidth float64 `json:"bandwidth_mbit"` // Estimated bandwidth of the network link, in Mbps MbpsBandwidth float64 `json:"bandwidth_mbit"` // Estimated bandwidth of the network link, in Mbps
ByteAvailSndBuf uint64 `json:"avail_send_buf_bytes"` // The available space in the sender's buffer, in bytes ByteAvailSndBuf uint64 `json:"avail_send_buf_bytes" format:"uint64"` // The available space in the sender's buffer, in bytes
ByteAvailRcvBuf uint64 `json:"avail_recv_buf_bytes"` // The available space in the receiver's buffer, in bytes ByteAvailRcvBuf uint64 `json:"avail_recv_buf_bytes" format:"uint64"` // The available space in the receiver's buffer, in bytes
MbpsMaxBW float64 `json:"max_bandwidth_mbit"` // Transmission bandwidth limit, in Mbps MbpsMaxBW float64 `json:"max_bandwidth_mbit"` // Transmission bandwidth limit, in Mbps
ByteMSS uint64 `json:"mss_bytes"` // Maximum Segment Size (MSS), in bytes ByteMSS uint64 `json:"mss_bytes" format:"uint64"` // Maximum Segment Size (MSS), in bytes
PktSndBuf uint64 `json:"send_buf_pkt"` // The number of packets in the sender's buffer that are already scheduled for sending or even possibly sent, but not yet acknowledged PktSndBuf uint64 `json:"send_buf_pkt" format:"uint64"` // The number of packets in the sender's buffer that are already scheduled for sending or even possibly sent, but not yet acknowledged
ByteSndBuf uint64 `json:"send_buf_bytes"` // Instantaneous (current) value of pktSndBuf, but expressed in bytes, including payload and all headers (IP, TCP, SRT) ByteSndBuf uint64 `json:"send_buf_bytes" format:"uint64"` // Instantaneous (current) value of pktSndBuf, but expressed in bytes, including payload and all headers (IP, TCP, SRT)
MsSndBuf uint64 `json:"send_buf_ms"` // The timespan (msec) of packets in the sender's buffer (unacknowledged packets) MsSndBuf uint64 `json:"send_buf_ms" format:"uint64"` // The timespan (msec) of packets in the sender's buffer (unacknowledged packets)
MsSndTsbPdDelay uint64 `json:"send_tsbpd_delay_ms"` // Timestamp-based Packet Delivery Delay value of the peer MsSndTsbPdDelay uint64 `json:"send_tsbpd_delay_ms" format:"uint64"` // Timestamp-based Packet Delivery Delay value of the peer
PktRcvBuf uint64 `json:"recv_buf_pkt"` // The number of acknowledged packets in receiver's buffer PktRcvBuf uint64 `json:"recv_buf_pkt" format:"uint64"` // The number of acknowledged packets in receiver's buffer
ByteRcvBuf uint64 `json:"recv_buf_bytes"` // Instantaneous (current) value of pktRcvBuf, expressed in bytes, including payload and all headers (IP, TCP, SRT) ByteRcvBuf uint64 `json:"recv_buf_bytes" format:"uint64"` // Instantaneous (current) value of pktRcvBuf, expressed in bytes, including payload and all headers (IP, TCP, SRT)
MsRcvBuf uint64 `json:"recv_buf_ms"` // The timespan (msec) of acknowledged packets in the receiver's buffer MsRcvBuf uint64 `json:"recv_buf_ms" format:"uint64"` // The timespan (msec) of acknowledged packets in the receiver's buffer
MsRcvTsbPdDelay uint64 `json:"recv_tsbpd_delay_ms"` // Timestamp-based Packet Delivery Delay value set on the socket via SRTO_RCVLATENCY or SRTO_LATENCY MsRcvTsbPdDelay uint64 `json:"recv_tsbpd_delay_ms" format:"uint64"` // Timestamp-based Packet Delivery Delay value set on the socket via SRTO_RCVLATENCY or SRTO_LATENCY
PktReorderTolerance uint64 `json:"reorder_tolerance_pkt"` // Instant value of the packet reorder tolerance PktReorderTolerance uint64 `json:"reorder_tolerance_pkt" format:"uint64"` // Instant value of the packet reorder tolerance
PktRcvAvgBelatedTime uint64 `json:"pkt_recv_avg_belated_time_ms"` // Accumulated difference between the current time and the time-to-play of a packet that is received late PktRcvAvgBelatedTime uint64 `json:"pkt_recv_avg_belated_time_ms" format:"uint64"` // Accumulated difference between the current time and the time-to-play of a packet that is received late
} }
// Unmarshal converts the SRT statistics into API representation // Unmarshal converts the SRT statistics into API representation
func (s *SRTStatistics) Unmarshal(ss *gosrt.Statistics) { func (s *SRTStatistics) Unmarshal(ss *gosrt.Statistics) {
s.MsTimeStamp = ss.MsTimeStamp s.MsTimeStamp = ss.MsTimeStamp
s.PktSent = ss.PktSent s.PktSent = ss.Accumulated.PktSent
s.PktRecv = ss.PktRecv s.PktRecv = ss.Accumulated.PktRecv
s.PktSentUnique = ss.PktSentUnique s.PktSentUnique = ss.Accumulated.PktSentUnique
s.PktRecvUnique = ss.PktRecvUnique s.PktRecvUnique = ss.Accumulated.PktRecvUnique
s.PktSndLoss = ss.PktSndLoss s.PktSndLoss = ss.Accumulated.PktSendLoss
s.PktRcvLoss = ss.PktRcvLoss s.PktRcvLoss = ss.Accumulated.PktRecvLoss
s.PktRetrans = ss.PktRetrans s.PktRetrans = ss.Accumulated.PktRetrans
s.PktRcvRetrans = ss.PktRcvRetrans s.PktRcvRetrans = ss.Accumulated.PktRecvRetrans
s.PktSentACK = ss.PktSentACK s.PktSentACK = ss.Accumulated.PktSentACK
s.PktRecvACK = ss.PktRecvACK s.PktRecvACK = ss.Accumulated.PktRecvACK
s.PktSentNAK = ss.PktSentNAK s.PktSentNAK = ss.Accumulated.PktSentNAK
s.PktRecvNAK = ss.PktRecvNAK s.PktRecvNAK = ss.Accumulated.PktRecvNAK
s.PktSentKM = ss.PktSentKM s.PktSentKM = ss.Accumulated.PktSentKM
s.PktRecvKM = ss.PktRecvKM s.PktRecvKM = ss.Accumulated.PktRecvKM
s.UsSndDuration = ss.UsSndDuration s.UsSndDuration = ss.Accumulated.UsSndDuration
s.PktSndDrop = ss.PktSndDrop s.PktSndDrop = ss.Accumulated.PktSendDrop
s.PktRcvDrop = ss.PktRcvDrop s.PktRcvDrop = ss.Accumulated.PktRecvDrop
s.PktRcvUndecrypt = ss.PktRcvUndecrypt s.PktRcvUndecrypt = ss.Accumulated.PktRecvUndecrypt
s.ByteSent = ss.ByteSent s.ByteSent = ss.Accumulated.ByteSent
s.ByteRecv = ss.ByteRecv s.ByteRecv = ss.Accumulated.ByteRecv
s.ByteSentUnique = ss.ByteSentUnique s.ByteSentUnique = ss.Accumulated.ByteSentUnique
s.ByteRecvUnique = ss.ByteRecvUnique s.ByteRecvUnique = ss.Accumulated.ByteRecvUnique
s.ByteRcvLoss = ss.ByteRcvLoss s.ByteRcvLoss = ss.Accumulated.ByteRecvLoss
s.ByteRetrans = ss.ByteRetrans s.ByteRetrans = ss.Accumulated.ByteRetrans
s.ByteSndDrop = ss.ByteSndDrop s.ByteSndDrop = ss.Accumulated.ByteSendDrop
s.ByteRcvDrop = ss.ByteRcvDrop s.ByteRcvDrop = ss.Accumulated.ByteRecvDrop
s.ByteRcvUndecrypt = ss.ByteRcvUndecrypt s.ByteRcvUndecrypt = ss.Accumulated.ByteRecvUndecrypt
s.UsPktSndPeriod = ss.Instantaneous.UsPktSendPeriod
s.PktFlowWindow = ss.Instantaneous.PktFlowWindow
s.PktFlightSize = ss.Instantaneous.PktFlightSize
s.MsRTT = ss.Instantaneous.MsRTT
s.MbpsBandwidth = ss.Instantaneous.MbpsLinkCapacity
s.ByteAvailSndBuf = ss.Instantaneous.ByteAvailSendBuf
s.ByteAvailRcvBuf = ss.Instantaneous.ByteAvailRecvBuf
s.MbpsMaxBW = ss.Instantaneous.MbpsMaxBW
s.ByteMSS = ss.Instantaneous.ByteMSS
s.PktSndBuf = ss.Instantaneous.PktSendBuf
s.ByteSndBuf = ss.Instantaneous.ByteSendBuf
s.MsSndBuf = ss.Instantaneous.MsSendBuf
s.MsSndTsbPdDelay = ss.Instantaneous.MsSendTsbPdDelay
s.PktRcvBuf = ss.Instantaneous.PktRecvBuf
s.ByteRcvBuf = ss.Instantaneous.ByteRecvBuf
s.MsRcvBuf = ss.Instantaneous.MsRecvBuf
s.MsRcvTsbPdDelay = ss.Instantaneous.MsRecvTsbPdDelay
s.PktReorderTolerance = ss.Instantaneous.PktReorderTolerance
s.PktRcvAvgBelatedTime = ss.Instantaneous.PktRecvAvgBelatedTime
} }
type SRTLog struct { type SRTLog struct {
Timestamp int64 `json:"ts"` Timestamp int64 `json:"ts" format:"int64"`
Message []string `json:"msg"` Message []string `json:"msg"`
} }

View File

@@ -1,7 +1,7 @@
package api package api
type WidgetProcess struct { type WidgetProcess struct {
CurrentSessions uint64 `json:"current_sessions"` CurrentSessions uint64 `json:"current_sessions" format:"uint64"`
TotalSessions uint64 `json:"total_sessions"` TotalSessions uint64 `json:"total_sessions" format:"uint64"`
Uptime int64 `json:"uptime"` Uptime int64 `json:"uptime"`
} }

View File

@@ -2,6 +2,7 @@ package fs
import ( import (
"io" "io"
gofs "io/fs"
"time" "time"
"github.com/datarhei/core/v16/cluster" "github.com/datarhei/core/v16/cluster"
@@ -15,14 +16,18 @@ type Filesystem interface {
type filesystem struct { type filesystem struct {
fs.Filesystem fs.Filesystem
what string name string
cluster cluster.ClusterReader cluster cluster.ClusterReader
} }
func NewClusterFS(what string, fs fs.Filesystem, cluster cluster.Cluster) Filesystem { func NewClusterFS(name string, fs fs.Filesystem, cluster cluster.Cluster) Filesystem {
if cluster == nil {
return fs
}
f := &filesystem{ f := &filesystem{
Filesystem: fs, Filesystem: fs,
what: what, name: name,
cluster: cluster, cluster: cluster,
} }
@@ -36,7 +41,7 @@ func (fs *filesystem) Open(path string) fs.File {
} }
// Check if the file is available in the cluster // Check if the file is available in the cluster
data, err := fs.cluster.GetFile(fs.what + ":" + path) data, err := fs.cluster.GetFile(fs.name + ":" + path)
if err != nil { if err != nil {
return nil return nil
} }
@@ -63,6 +68,10 @@ func (f *file) Stat() (fs.FileInfo, error) {
return f, nil return f, nil
} }
func (f *file) Mode() gofs.FileMode {
return gofs.FileMode(gofs.ModePerm)
}
func (f *file) Size() int64 { func (f *file) Size() int64 {
return 0 return 0
} }

25
http/fs/fs.go Normal file
View File

@@ -0,0 +1,25 @@
package fs
import (
"github.com/datarhei/core/v16/http/cache"
"github.com/datarhei/core/v16/io/fs"
)
type FS struct {
Name string
Mountpoint string
AllowWrite bool
EnableAuth bool
Username string
Password string
DefaultFile string
DefaultContentType string
Gzip bool
Filesystem fs.Filesystem
Cache cache.Cacher
}

View File

@@ -1,11 +1,14 @@
package api package api
import ( import (
"io"
"net/http" "net/http"
"time"
"github.com/datarhei/core/v16/config" cfgstore "github.com/datarhei/core/v16/config/store"
cfgvars "github.com/datarhei/core/v16/config/vars"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -13,11 +16,11 @@ import (
// The ConfigHandler type provides handler functions for reading and manipulating // The ConfigHandler type provides handler functions for reading and manipulating
// the current config. // the current config.
type ConfigHandler struct { type ConfigHandler struct {
store config.Store store cfgstore.Store
} }
// NewConfig return a new Config type. You have to provide a valid config store. // NewConfig return a new Config type. You have to provide a valid config store.
func NewConfig(store config.Store) *ConfigHandler { func NewConfig(store cfgstore.Store) *ConfigHandler {
return &ConfigHandler{ return &ConfigHandler{
store: store, store: store,
} }
@@ -26,6 +29,7 @@ func NewConfig(store config.Store) *ConfigHandler {
// Get returns the currently active Restreamer configuration // Get returns the currently active Restreamer configuration
// @Summary Retrieve the currently active Restreamer configuration // @Summary Retrieve the currently active Restreamer configuration
// @Description Retrieve the currently active Restreamer configuration // @Description Retrieve the currently active Restreamer configuration
// @Tags v16.7.2
// @ID config-3-get // @ID config-3-get
// @Produce json // @Produce json
// @Success 200 {object} api.Config // @Success 200 {object} api.Config
@@ -43,6 +47,7 @@ func (p *ConfigHandler) Get(c echo.Context) error {
// Set will set the given configuration as new active configuration // Set will set the given configuration as new active configuration
// @Summary Update the current Restreamer configuration // @Summary Update the current Restreamer configuration
// @Description Update the current Restreamer configuration by providing a complete or partial configuration. Fields that are not provided will not be changed. // @Description Update the current Restreamer configuration by providing a complete or partial configuration. Fields that are not provided will not be changed.
// @Tags v16.7.2
// @ID config-3-set // @ID config-3-set
// @Accept json // @Accept json
// @Produce json // @Produce json
@@ -53,25 +58,80 @@ func (p *ConfigHandler) Get(c echo.Context) error {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /api/v3/config [put] // @Router /api/v3/config [put]
func (p *ConfigHandler) Set(c echo.Context) error { func (p *ConfigHandler) Set(c echo.Context) error {
version := api.ConfigVersion{}
req := c.Request()
body, err := io.ReadAll(req.Body)
if err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
if err := json.Unmarshal(body, &version); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", json.FormatError(body, err))
}
cfg := p.store.Get() cfg := p.store.Get()
cfgActive := p.store.GetActive()
// Set the current config as default config value. This will // Copy the timestamp of when this config has been used
// allow to set a partial config without destroying the other cfg.LoadedAt = cfgActive.LoadedAt
// values.
setConfig := api.NewSetConfig(cfg)
if err := util.ShouldBindJSON(c, &setConfig); err != nil { // For each version, set the current config as default config value. This will
// allow to set a partial config without destroying the other values.
if version.Version == 1 {
// Downgrade to v1 in order to have a populated v1 config
v1SetConfig := api.NewSetConfigV1(cfg)
if err := json.Unmarshal(body, &v1SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", json.FormatError(body, err))
}
if err := c.Validate(v1SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err) return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
} }
// Merge it into the current config // Merge it into the current config
setConfig.MergeTo(cfg) v1SetConfig.MergeTo(cfg)
} else if version.Version == 2 {
// Downgrade to v2 in order to have a populated v2 config
v2SetConfig := api.NewSetConfigV2(cfg)
if err := json.Unmarshal(body, &v2SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", json.FormatError(body, err))
}
if err := c.Validate(v2SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
// Merge it into the current config
v2SetConfig.MergeTo(cfg)
} else if version.Version == 3 {
v3SetConfig := api.NewSetConfig(cfg)
if err := json.Unmarshal(body, &v3SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", json.FormatError(body, err))
}
if err := c.Validate(v3SetConfig); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
}
// Merge it into the current config
v3SetConfig.MergeTo(cfg)
} else {
return api.Err(http.StatusBadRequest, "Invalid config version", "version %d", version.Version)
}
cfg.CreatedAt = time.Now()
cfg.UpdatedAt = cfg.CreatedAt
// Now we make a copy from the config and merge it with the environment // Now we make a copy from the config and merge it with the environment
// variables. If this configuration is valid, we will store the un-merged // variables. If this configuration is valid, we will store the un-merged
// one to disk. // one to disk.
mergedConfig := config.NewConfigFrom(cfg) mergedConfig := cfg.Clone()
mergedConfig.Merge() mergedConfig.Merge()
// Validate the new merged config // Validate the new merged config
@@ -79,7 +139,7 @@ func (p *ConfigHandler) Set(c echo.Context) error {
if mergedConfig.HasErrors() { if mergedConfig.HasErrors() {
errors := make(map[string][]string) errors := make(map[string][]string)
mergedConfig.Messages(func(level string, v config.Variable, message string) { mergedConfig.Messages(func(level string, v cfgvars.Variable, message string) {
if level != "error" { if level != "error" {
return return
} }
@@ -105,14 +165,15 @@ func (p *ConfigHandler) Set(c echo.Context) error {
// Reload will reload the currently active configuration // Reload will reload the currently active configuration
// @Summary Reload the currently active configuration // @Summary Reload the currently active configuration
// @Description Reload the currently active configuration. This will trigger a restart of the Restreamer. // @Description Reload the currently active configuration. This will trigger a restart of the Core.
// @Tags v16.7.2
// @ID config-3-reload // @ID config-3-reload
// @Produce plain // @Produce json
// @Success 200 {string} string "OK" // @Success 200 {string} string
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /api/v3/config/reload [get] // @Router /api/v3/config/reload [get]
func (p *ConfigHandler) Reload(c echo.Context) error { func (p *ConfigHandler) Reload(c echo.Context) error {
p.store.Reload() p.store.Reload()
return c.String(http.StatusOK, "OK") return c.JSON(http.StatusOK, "OK")
} }

View File

@@ -4,28 +4,43 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"testing" "testing"
"github.com/datarhei/core/v16/config" "github.com/datarhei/core/v16/config"
"github.com/datarhei/core/v16/config/store"
v1 "github.com/datarhei/core/v16/config/v1"
"github.com/datarhei/core/v16/http/mock" "github.com/datarhei/core/v16/http/mock"
"github.com/datarhei/core/v16/io/fs"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
) )
func getDummyConfigRouter() *echo.Echo { func getDummyConfigRouter(t *testing.T) (*echo.Echo, store.Store) {
router := mock.DummyEcho() router := mock.DummyEcho()
config := config.NewDummyStore() memfs, err := fs.NewMemFilesystem(fs.MemConfig{})
require.NoError(t, err)
_, _, err = memfs.WriteFileReader("./mime.types", strings.NewReader("xxxxx"))
require.NoError(t, err)
_, _, err = memfs.WriteFileReader("/bin/ffmpeg", strings.NewReader("xxxxx"))
require.NoError(t, err)
config, err := store.NewJSON(memfs, "/config.json", nil)
require.NoError(t, err)
handler := NewConfig(config) handler := NewConfig(config)
router.Add("GET", "/", handler.Get) router.Add("GET", "/", handler.Get)
router.Add("PUT", "/", handler.Set) router.Add("PUT", "/", handler.Set)
return router return router, config
} }
func TestConfigGet(t *testing.T) { func TestConfigGet(t *testing.T) {
router := getDummyConfigRouter() router, _ := getDummyConfigRouter(t)
mock.Request(t, http.StatusOK, router, "GET", "/", nil) mock.Request(t, http.StatusOK, router, "GET", "/", nil)
@@ -33,29 +48,96 @@ func TestConfigGet(t *testing.T) {
} }
func TestConfigSetConflict(t *testing.T) { func TestConfigSetConflict(t *testing.T) {
router := getDummyConfigRouter() router, _ := getDummyConfigRouter(t)
cfg := config.New(nil)
cfg.Storage.MimeTypes = "/path/to/mime.types"
var data bytes.Buffer var data bytes.Buffer
encoder := json.NewEncoder(&data) encoder := json.NewEncoder(&data)
encoder.Encode(config.New()) encoder.Encode(cfg)
mock.Request(t, http.StatusConflict, router, "PUT", "/", &data) mock.Request(t, http.StatusConflict, router, "PUT", "/", &data)
} }
func TestConfigSet(t *testing.T) { func TestConfigSet(t *testing.T) {
router := getDummyConfigRouter() router, store := getDummyConfigRouter(t)
storedcfg := store.Get()
require.Equal(t, []string{}, storedcfg.Host.Name)
var data bytes.Buffer var data bytes.Buffer
encoder := json.NewEncoder(&data)
cfg := config.New() // Setting a new v3 config
cfg.FFmpeg.Binary = "true" cfg := config.New(nil)
cfg.DB.Dir = "." cfg.DB.Dir = "."
cfg.Storage.Disk.Dir = "." cfg.Storage.Disk.Dir = "."
cfg.Storage.MimeTypes = "" cfg.Storage.Disk.Cache.Types.Allow = []string{".aaa"}
cfg.Storage.Disk.Cache.Types.Block = []string{".zzz"}
cfg.Host.Name = []string{"foobar.com"}
encoder := json.NewEncoder(&data)
encoder.Encode(cfg) encoder.Encode(cfg)
mock.Request(t, http.StatusOK, router, "PUT", "/", &data) mock.Request(t, http.StatusOK, router, "PUT", "/", &data)
storedcfg = store.Get()
require.Equal(t, []string{"foobar.com"}, storedcfg.Host.Name)
require.Equal(t, []string{".aaa"}, cfg.Storage.Disk.Cache.Types.Allow)
require.Equal(t, []string{".zzz"}, cfg.Storage.Disk.Cache.Types.Block)
require.Equal(t, "cert@datarhei.com", cfg.TLS.Email)
// Setting a complete v1 config
cfgv1 := v1.New(nil)
cfgv1.DB.Dir = "."
cfgv1.Storage.Disk.Dir = "."
cfgv1.Storage.Disk.Cache.Types = []string{".bbb"}
cfgv1.Host.Name = []string{"foobar.com"}
data.Reset()
encoder.Encode(cfgv1)
mock.Request(t, http.StatusOK, router, "PUT", "/", &data)
storedcfg = store.Get()
require.Equal(t, []string{"foobar.com"}, storedcfg.Host.Name)
require.Equal(t, []string{".bbb"}, storedcfg.Storage.Disk.Cache.Types.Allow)
require.Equal(t, []string{".zzz"}, storedcfg.Storage.Disk.Cache.Types.Block)
require.Equal(t, "cert@datarhei.com", cfg.TLS.Email)
// Setting a partial v1 config
type customconfig struct {
Version int `json:"version"`
Storage struct {
Disk struct {
Cache struct {
Types []string `json:"types"`
} `json:"cache"`
} `json:"disk"`
} `json:"storage"`
}
customcfg := customconfig{
Version: 1,
}
customcfg.Storage.Disk.Cache.Types = []string{".ccc"}
data.Reset()
encoder.Encode(customcfg)
mock.Request(t, http.StatusOK, router, "PUT", "/", &data)
storedcfg = store.Get()
require.Equal(t, []string{"foobar.com"}, storedcfg.Host.Name)
require.Equal(t, []string{".ccc"}, storedcfg.Storage.Disk.Cache.Types.Allow)
require.Equal(t, []string{".zzz"}, storedcfg.Storage.Disk.Cache.Types.Block)
require.Equal(t, "cert@datarhei.com", cfg.TLS.Email)
} }

View File

@@ -1,211 +0,0 @@
package api
import (
"net/http"
"path/filepath"
"sort"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/cache"
"github.com/datarhei/core/v16/http/handler"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/io/fs"
"github.com/labstack/echo/v4"
)
// The DiskFSHandler type provides handlers for manipulating a filesystem
type DiskFSHandler struct {
cache cache.Cacher
filesystem fs.Filesystem
handler *handler.DiskFSHandler
}
// NewDiskFS return a new DiskFS type. You have to provide a filesystem to act on and optionally
// a Cacher where files will be purged from if the Cacher is related to the filesystem.
func NewDiskFS(fs fs.Filesystem, cache cache.Cacher) *DiskFSHandler {
return &DiskFSHandler{
cache: cache,
filesystem: fs,
handler: handler.NewDiskFS(fs, cache),
}
}
// GetFile returns the file at the given path
// @Summary Fetch a file from the filesystem
// @Description Fetch a file from the filesystem. The contents of that file are returned.
// @ID diskfs-3-get-file
// @Produce application/data
// @Produce json
// @Param path path string true "Path to file"
// @Success 200 {file} byte
// @Success 301 {string} string
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/disk/{path} [get]
func (h *DiskFSHandler) GetFile(c echo.Context) error {
path := util.PathWildcardParam(c)
mimeType := c.Response().Header().Get(echo.HeaderContentType)
c.Response().Header().Del(echo.HeaderContentType)
file := h.filesystem.Open(path)
if file == nil {
return api.Err(http.StatusNotFound, "File not found", path)
}
stat, _ := file.Stat()
if stat.IsDir() {
return api.Err(http.StatusNotFound, "File not found", path)
}
defer file.Close()
c.Response().Header().Set("Last-Modified", stat.ModTime().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
if path, ok := stat.IsLink(); ok {
path = filepath.Clean("/" + path)
if path[0] == '/' {
path = path[1:]
}
return c.Redirect(http.StatusMovedPermanently, path)
}
c.Response().Header().Set(echo.HeaderContentType, mimeType)
if c.Request().Method == "HEAD" {
return c.Blob(http.StatusOK, "application/data", nil)
}
return c.Stream(http.StatusOK, "application/data", file)
}
// PutFile adds or overwrites a file at the given path
// @Summary Add a file to the filesystem
// @Description Writes or overwrites a file on the filesystem
// @ID diskfs-3-put-file
// @Accept application/data
// @Produce text/plain
// @Produce json
// @Param path path string true "Path to file"
// @Param data body []byte true "File data"
// @Success 201 {string} string
// @Success 204 {string} string
// @Failure 507 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/disk/{path} [put]
func (h *DiskFSHandler) PutFile(c echo.Context) error {
path := util.PathWildcardParam(c)
c.Response().Header().Del(echo.HeaderContentType)
req := c.Request()
_, created, err := h.filesystem.Store(path, req.Body)
if err != nil {
return api.Err(http.StatusBadRequest, "%s", err)
}
if h.cache != nil {
h.cache.Delete(path)
}
c.Response().Header().Set("Content-Location", req.URL.RequestURI())
if created {
return c.String(http.StatusCreated, path)
}
return c.NoContent(http.StatusNoContent)
}
// DeleteFile removes a file from the filesystem
// @Summary Remove a file from the filesystem
// @Description Remove a file from the filesystem
// @ID diskfs-3-delete-file
// @Produce text/plain
// @Param path path string true "Path to file"
// @Success 200 {string} string
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/disk/{path} [delete]
func (h *DiskFSHandler) DeleteFile(c echo.Context) error {
path := util.PathWildcardParam(c)
c.Response().Header().Del(echo.HeaderContentType)
size := h.filesystem.Delete(path)
if size < 0 {
return api.Err(http.StatusNotFound, "File not found", path)
}
if h.cache != nil {
h.cache.Delete(path)
}
return c.String(http.StatusOK, "OK")
}
// ListFiles lists all files on the filesystem
// @Summary List all files on the filesystem
// @Description List all files on the filesystem. The listing can be ordered by name, size, or date of last modification in ascending or descending order.
// @ID diskfs-3-list-files
// @Produce json
// @Param glob query string false "glob pattern for file names"
// @Param sort query string false "none, name, size, lastmod"
// @Param order query string false "asc, desc"
// @Success 200 {array} api.FileInfo
// @Security ApiKeyAuth
// @Router /api/v3/fs/disk [get]
func (h *DiskFSHandler) ListFiles(c echo.Context) error {
pattern := util.DefaultQuery(c, "glob", "")
sortby := util.DefaultQuery(c, "sort", "none")
order := util.DefaultQuery(c, "order", "asc")
files := h.filesystem.List(pattern)
var sortFunc func(i, j int) bool
switch sortby {
case "name":
if order == "desc" {
sortFunc = func(i, j int) bool { return files[i].Name() > files[j].Name() }
} else {
sortFunc = func(i, j int) bool { return files[i].Name() < files[j].Name() }
}
case "size":
if order == "desc" {
sortFunc = func(i, j int) bool { return files[i].Size() > files[j].Size() }
} else {
sortFunc = func(i, j int) bool { return files[i].Size() < files[j].Size() }
}
default:
if order == "asc" {
sortFunc = func(i, j int) bool { return files[i].ModTime().Before(files[j].ModTime()) }
} else {
sortFunc = func(i, j int) bool { return files[i].ModTime().After(files[j].ModTime()) }
}
}
sort.Slice(files, sortFunc)
fileinfos := []api.FileInfo{}
for _, f := range files {
if f.IsDir() {
continue
}
fileinfos = append(fileinfos, api.FileInfo{
Name: f.Name(),
Size: f.Size(),
LastMod: f.ModTime().Unix(),
})
}
return c.JSON(http.StatusOK, fileinfos)
}

View File

@@ -0,0 +1,151 @@
package api
import (
"net/http"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/labstack/echo/v4"
)
type FSConfig struct {
Type string
Mountpoint string
Handler *handler.FSHandler
}
// The FSHandler type provides handlers for manipulating a filesystem
type FSHandler struct {
filesystems map[string]FSConfig
}
// NewFS return a new FSHanlder type. You have to provide a filesystem to act on.
func NewFS(filesystems map[string]FSConfig) *FSHandler {
return &FSHandler{
filesystems: filesystems,
}
}
// GetFileAPI returns the file at the given path
// @Summary Fetch a file from a filesystem
// @Description Fetch a file from a filesystem
// @Tags v16.7.2
// @ID filesystem-3-get-file
// @Produce application/data
// @Produce json
// @Param storage path string true "Name of the filesystem"
// @Param filepath path string true "Path to file"
// @Success 200 {file} byte
// @Success 301 {string} string
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/{storage}/{filepath} [get]
func (h *FSHandler) GetFile(c echo.Context) error {
name := util.PathParam(c, "name")
config, ok := h.filesystems[name]
if !ok {
return api.Err(http.StatusNotFound, "File not found", "unknown filesystem: %s", name)
}
return config.Handler.GetFile(c)
}
// PutFileAPI adds or overwrites a file at the given path
// @Summary Add a file to a filesystem
// @Description Writes or overwrites a file on a filesystem
// @Tags v16.7.2
// @ID filesystem-3-put-file
// @Accept application/data
// @Produce text/plain
// @Produce json
// @Param storage path string true "Name of the filesystem"
// @Param filepath path string true "Path to file"
// @Param data body []byte true "File data"
// @Success 201 {string} string
// @Success 204 {string} string
// @Failure 507 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/{storage}/{filepath} [put]
func (h *FSHandler) PutFile(c echo.Context) error {
name := util.PathParam(c, "name")
config, ok := h.filesystems[name]
if !ok {
return api.Err(http.StatusNotFound, "File not found", "unknown filesystem: %s", name)
}
return config.Handler.PutFile(c)
}
// DeleteFileAPI removes a file from a filesystem
// @Summary Remove a file from a filesystem
// @Description Remove a file from a filesystem
// @Tags v16.7.2
// @ID filesystem-3-delete-file
// @Produce text/plain
// @Param storage path string true "Name of the filesystem"
// @Param filepath path string true "Path to file"
// @Success 200 {string} string
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/{storage}/{filepath} [delete]
func (h *FSHandler) DeleteFile(c echo.Context) error {
name := util.PathParam(c, "name")
config, ok := h.filesystems[name]
if !ok {
return api.Err(http.StatusNotFound, "File not found", "unknown filesystem: %s", name)
}
return config.Handler.DeleteFile(c)
}
// ListFiles lists all files on a filesystem
// @Summary List all files on a filesystem
// @Description List all files on a filesystem. The listing can be ordered by name, size, or date of last modification in ascending or descending order.
// @Tags v16.7.2
// @ID filesystem-3-list-files
// @Produce json
// @Param storage path string true "Name of the filesystem"
// @Param glob query string false "glob pattern for file names"
// @Param sort query string false "none, name, size, lastmod"
// @Param order query string false "asc, desc"
// @Success 200 {array} api.FileInfo
// @Security ApiKeyAuth
// @Router /api/v3/fs/{storage} [get]
func (h *FSHandler) ListFiles(c echo.Context) error {
name := util.PathParam(c, "name")
config, ok := h.filesystems[name]
if !ok {
return api.Err(http.StatusNotFound, "File not found", "unknown filesystem: %s", name)
}
return config.Handler.ListFiles(c)
}
// List lists all registered filesystems
// @Summary List all registered filesystems
// @Description Listall registered filesystems
// @Tags v16.12.0
// @ID filesystem-3-list
// @Produce json
// @Success 200 {array} api.FilesystemInfo
// @Security ApiKeyAuth
// @Router /api/v3/fs [get]
func (h *FSHandler) List(c echo.Context) error {
fss := []api.FilesystemInfo{}
for name, config := range h.filesystems {
fss = append(fss, api.FilesystemInfo{
Name: name,
Type: config.Type,
Mount: config.Mountpoint,
})
}
return c.JSON(http.StatusOK, fss)
}

View File

@@ -31,6 +31,7 @@ func NewLog(buffer log.BufferWriter) *LogHandler {
// Log returns the last log lines of the Restreamer application // Log returns the last log lines of the Restreamer application
// @Summary Application log // @Summary Application log
// @Description Get the last log lines of the Restreamer application // @Description Get the last log lines of the Restreamer application
// @Tags v16.7.2
// @ID log-3 // @ID log-3
// @Param format query string false "Format of the list of log events (*console, raw)" // @Param format query string false "Format of the list of log events (*console, raw)"
// @Produce json // @Produce json

View File

@@ -1,172 +0,0 @@
package api
import (
"io"
"net/http"
"net/url"
"sort"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/io/fs"
"github.com/labstack/echo/v4"
)
// The MemFSHandler type provides handlers for manipulating a filesystem
type MemFSHandler struct {
filesystem fs.Filesystem
handler *handler.MemFSHandler
}
// NewMemFS return a new MemFS type. You have to provide a filesystem to act on.
func NewMemFS(fs fs.Filesystem) *MemFSHandler {
return &MemFSHandler{
filesystem: fs,
handler: handler.NewMemFS(fs),
}
}
// GetFileAPI returns the file at the given path
// @Summary Fetch a file from the memory filesystem
// @Description Fetch a file from the memory filesystem
// @ID memfs-3-get-file
// @Produce application/data
// @Produce json
// @Param path path string true "Path to file"
// @Success 200 {file} byte
// @Success 301 {string} string
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/mem/{path} [get]
func (h *MemFSHandler) GetFile(c echo.Context) error {
return h.handler.GetFile(c)
}
// PutFileAPI adds or overwrites a file at the given path
// @Summary Add a file to the memory filesystem
// @Description Writes or overwrites a file on the memory filesystem
// @ID memfs-3-put-file
// @Accept application/data
// @Produce text/plain
// @Produce json
// @Param path path string true "Path to file"
// @Param data body []byte true "File data"
// @Success 201 {string} string
// @Success 204 {string} string
// @Failure 507 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/mem/{path} [put]
func (h *MemFSHandler) PutFile(c echo.Context) error {
return h.handler.PutFile(c)
}
// DeleteFileAPI removes a file from the filesystem
// @Summary Remove a file from the memory filesystem
// @Description Remove a file from the memory filesystem
// @ID memfs-3-delete-file
// @Produce text/plain
// @Param path path string true "Path to file"
// @Success 200 {string} string
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/mem/{path} [delete]
func (h *MemFSHandler) DeleteFile(c echo.Context) error {
return h.handler.DeleteFile(c)
}
// PatchFile creates a symbolic link to a file in the filesystem
// @Summary Create a link to a file in the memory filesystem
// @Description Create a link to a file in the memory filesystem. The file linked to has to exist.
// @ID memfs-3-patch
// @Accept application/data
// @Produce text/plain
// @Produce json
// @Param path path string true "Path to file"
// @Param url body string true "Path to the file to link to"
// @Success 201 {string} string
// @Failure 400 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/fs/mem/{path} [patch]
func (h *MemFSHandler) PatchFile(c echo.Context) error {
path := util.PathWildcardParam(c)
c.Response().Header().Del(echo.HeaderContentType)
req := c.Request()
body, err := io.ReadAll(req.Body)
if err != nil {
return api.Err(http.StatusBadRequest, "Failed reading request body", "%s", err)
}
u, err := url.Parse(string(body))
if err != nil {
return api.Err(http.StatusBadRequest, "Body doesn't contain a valid path", "%s", err)
}
if err := h.filesystem.Symlink(u.Path, path); err != nil {
return api.Err(http.StatusBadRequest, "Failed to create symlink", "%s", err)
}
c.Response().Header().Set("Content-Location", req.URL.RequestURI())
return c.String(http.StatusCreated, "")
}
// ListFiles lists all files on the filesystem
// @Summary List all files on the memory filesystem
// @Description List all files on the memory filesystem. The listing can be ordered by name, size, or date of last modification in ascending or descending order.
// @ID memfs-3-list-files
// @Produce json
// @Param glob query string false "glob pattern for file names"
// @Param sort query string false "none, name, size, lastmod"
// @Param order query string false "asc, desc"
// @Success 200 {array} api.FileInfo
// @Security ApiKeyAuth
// @Router /api/v3/fs/mem [get]
func (h *MemFSHandler) ListFiles(c echo.Context) error {
pattern := util.DefaultQuery(c, "glob", "")
sortby := util.DefaultQuery(c, "sort", "none")
order := util.DefaultQuery(c, "order", "asc")
files := h.filesystem.List(pattern)
var sortFunc func(i, j int) bool
switch sortby {
case "name":
if order == "desc" {
sortFunc = func(i, j int) bool { return files[i].Name() > files[j].Name() }
} else {
sortFunc = func(i, j int) bool { return files[i].Name() < files[j].Name() }
}
case "size":
if order == "desc" {
sortFunc = func(i, j int) bool { return files[i].Size() > files[j].Size() }
} else {
sortFunc = func(i, j int) bool { return files[i].Size() < files[j].Size() }
}
default:
if order == "asc" {
sortFunc = func(i, j int) bool { return files[i].ModTime().Before(files[j].ModTime()) }
} else {
sortFunc = func(i, j int) bool { return files[i].ModTime().After(files[j].ModTime()) }
}
}
sort.Slice(files, sortFunc)
var fileinfos []api.FileInfo = make([]api.FileInfo, len(files))
for i, f := range files {
fileinfos[i] = api.FileInfo{
Name: f.Name(),
Size: f.Size(),
LastMod: f.ModTime().Unix(),
}
}
return c.JSON(http.StatusOK, fileinfos)
}

View File

@@ -32,6 +32,7 @@ func NewMetrics(config MetricsConfig) *MetricsHandler {
// Describe the known metrics // Describe the known metrics
// @Summary List all known metrics with their description and labels // @Summary List all known metrics with their description and labels
// @Description List all known metrics with their description and labels // @Description List all known metrics with their description and labels
// @Tags v16.10.0
// @ID metrics-3-describe // @ID metrics-3-describe
// @Produce json // @Produce json
// @Success 200 {array} api.MetricsDescription // @Success 200 {array} api.MetricsDescription
@@ -60,6 +61,7 @@ func (r *MetricsHandler) Describe(c echo.Context) error {
// Query the collected metrics // Query the collected metrics
// @Summary Query the collected metrics // @Summary Query the collected metrics
// @Description Query the collected metrics // @Description Query the collected metrics
// @Tags v16.7.2
// @ID metrics-3-metrics // @ID metrics-3-metrics
// @Accept json // @Accept json
// @Produce json // @Produce json

View File

@@ -31,6 +31,7 @@ func NewPlayout(restream restream.Restreamer) *PlayoutHandler {
// Status return the current playout status // Status return the current playout status
// @Summary Get the current playout status // @Summary Get the current playout status
// @Description Get the current playout status of an input of a process // @Description Get the current playout status of an input of a process
// @Tags v16.7.2
// @ID process-3-playout-status // @ID process-3-playout-status
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -84,6 +85,7 @@ func (h *PlayoutHandler) Status(c echo.Context) error {
// Keyframe returns the last keyframe // Keyframe returns the last keyframe
// @Summary Get the last keyframe // @Summary Get the last keyframe
// @Description Get the last keyframe of an input of a process. The extension of the name determines the return type. // @Description Get the last keyframe of an input of a process. The extension of the name determines the return type.
// @Tags v16.7.2
// @ID process-3-playout-keyframe // @ID process-3-playout-keyframe
// @Produce image/jpeg // @Produce image/jpeg
// @Produce image/png // @Produce image/png
@@ -133,6 +135,7 @@ func (h *PlayoutHandler) Keyframe(c echo.Context) error {
// EncodeErrorframe encodes the errorframe // EncodeErrorframe encodes the errorframe
// @Summary Encode the errorframe // @Summary Encode the errorframe
// @Description Immediately encode the errorframe (if available and looping) // @Description Immediately encode the errorframe (if available and looping)
// @Tags v16.7.2
// @ID process-3-playout-errorframencode // @ID process-3-playout-errorframencode
// @Produce text/plain // @Produce text/plain
// @Produce json // @Produce json
@@ -173,6 +176,7 @@ func (h *PlayoutHandler) EncodeErrorframe(c echo.Context) error {
// SetErrorframe sets an errorframe // SetErrorframe sets an errorframe
// @Summary Upload an error frame // @Summary Upload an error frame
// @Description Upload an error frame which will be encoded immediately // @Description Upload an error frame which will be encoded immediately
// @Tags v16.7.2
// @ID process-3-playout-errorframe // @ID process-3-playout-errorframe
// @Produce text/plain // @Produce text/plain
// @Produce json // @Produce json
@@ -221,6 +225,7 @@ func (h *PlayoutHandler) SetErrorframe(c echo.Context) error {
// ReopenInput closes the current input stream // ReopenInput closes the current input stream
// @Summary Close the current input stream // @Summary Close the current input stream
// @Description Close the current input stream such that it will be automatically re-opened // @Description Close the current input stream such that it will be automatically re-opened
// @Tags v16.7.2
// @ID process-3-playout-reopen-input // @ID process-3-playout-reopen-input
// @Produce plain // @Produce plain
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -260,6 +265,7 @@ func (h *PlayoutHandler) ReopenInput(c echo.Context) error {
// SetStream replaces the current stream // SetStream replaces the current stream
// @Summary Switch to a new stream // @Summary Switch to a new stream
// @Description Replace the current stream with the one from the given URL. The switch will only happen if the stream parameters match. // @Description Replace the current stream with the one from the given URL. The switch will only happen if the stream parameters match.
// @Tags v16.7.2
// @ID process-3-playout-stream // @ID process-3-playout-stream
// @Produce text/plain // @Produce text/plain
// @Produce json // @Produce json

View File

@@ -27,6 +27,7 @@ func NewRestream(restream restream.Restreamer) *RestreamHandler {
// Add adds a new process // Add adds a new process
// @Summary Add a new process // @Summary Add a new process
// @Description Add a new FFmpeg process // @Description Add a new FFmpeg process
// @Tags v16.7.2
// @ID process-3-add // @ID process-3-add
// @Accept json // @Accept json
// @Produce json // @Produce json
@@ -50,7 +51,7 @@ func (h *RestreamHandler) Add(c echo.Context) error {
return api.Err(http.StatusBadRequest, "Unsupported process type", "Supported process types are: ffmpeg") return api.Err(http.StatusBadRequest, "Unsupported process type", "Supported process types are: ffmpeg")
} }
if len(process.Input) == 0 && len(process.Output) == 0 { if len(process.Input) == 0 || len(process.Output) == 0 {
return api.Err(http.StatusBadRequest, "At least one input and one output need to be defined") return api.Err(http.StatusBadRequest, "At least one input and one output need to be defined")
} }
@@ -68,6 +69,7 @@ func (h *RestreamHandler) Add(c echo.Context) error {
// GetAll returns all known processes // GetAll returns all known processes
// @Summary List all known processes // @Summary List all known processes
// @Description List all known processes. Use the query parameter to filter the listed processes. // @Description List all known processes. Use the query parameter to filter the listed processes.
// @Tags v16.7.2
// @ID process-3-get-all // @ID process-3-get-all
// @Produce json // @Produce json
// @Param filter query string false "Comma separated list of fields (config, state, report, metadata) that will be part of the output. If empty, all fields will be part of the output." // @Param filter query string false "Comma separated list of fields (config, state, report, metadata) that will be part of the output. If empty, all fields will be part of the output."
@@ -118,6 +120,7 @@ func (h *RestreamHandler) GetAll(c echo.Context) error {
// Get returns the process with the given ID // Get returns the process with the given ID
// @Summary List a process by its ID // @Summary List a process by its ID
// @Description List a process by its ID. Use the filter parameter to specifiy the level of detail of the output. // @Description List a process by its ID. Use the filter parameter to specifiy the level of detail of the output.
// @Tags v16.7.2
// @ID process-3-get // @ID process-3-get
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -141,6 +144,7 @@ func (h *RestreamHandler) Get(c echo.Context) error {
// Delete deletes the process with the given ID // Delete deletes the process with the given ID
// @Summary Delete a process by its ID // @Summary Delete a process by its ID
// @Description Delete a process by its ID // @Description Delete a process by its ID
// @Tags v16.7.2
// @ID process-3-delete // @ID process-3-delete
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -164,7 +168,8 @@ func (h *RestreamHandler) Delete(c echo.Context) error {
// Update replaces an existing process // Update replaces an existing process
// @Summary Replace an existing process // @Summary Replace an existing process
// @Description Replace an existing process // @Description Replace an existing process.
// @Tags v16.7.2
// @ID process-3-update // @ID process-3-update
// @Accept json // @Accept json
// @Produce json // @Produce json
@@ -184,6 +189,14 @@ func (h *RestreamHandler) Update(c echo.Context) error {
Autostart: true, Autostart: true,
} }
current, err := h.restream.GetProcess(id)
if err != nil {
return api.Err(http.StatusNotFound, "Process not found", "%s", id)
}
// Prefill the config with the current values
process.Unmarshal(current.Config)
if err := util.ShouldBindJSON(c, &process); err != nil { if err := util.ShouldBindJSON(c, &process); err != nil {
return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err) return api.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
} }
@@ -206,6 +219,7 @@ func (h *RestreamHandler) Update(c echo.Context) error {
// Command issues a command to a process // Command issues a command to a process
// @Summary Issue a command to a process // @Summary Issue a command to a process
// @Description Issue a command to a process: start, stop, reload, restart // @Description Issue a command to a process: start, stop, reload, restart
// @Tags v16.7.2
// @ID process-3-command // @ID process-3-command
// @Accept json // @Accept json
// @Produce json // @Produce json
@@ -248,6 +262,7 @@ func (h *RestreamHandler) Command(c echo.Context) error {
// GetConfig returns the configuration of a process // GetConfig returns the configuration of a process
// @Summary Get the configuration of a process // @Summary Get the configuration of a process
// @Description Get the configuration of a process. This is the configuration as provided by Add or Update. // @Description Get the configuration of a process. This is the configuration as provided by Add or Update.
// @Tags v16.7.2
// @ID process-3-get-config // @ID process-3-get-config
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -272,7 +287,8 @@ func (h *RestreamHandler) GetConfig(c echo.Context) error {
// GetState returns the current state of a process // GetState returns the current state of a process
// @Summary Get the state of a process // @Summary Get the state of a process
// @Description Get the state and progress data of a process // @Description Get the state and progress data of a process.
// @Tags v16.7.2
// @ID process-3-get-state // @ID process-3-get-state
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -297,7 +313,8 @@ func (h *RestreamHandler) GetState(c echo.Context) error {
// GetReport return the current log and the log history of a process // GetReport return the current log and the log history of a process
// @Summary Get the logs of a process // @Summary Get the logs of a process
// @Description Get the logs and the log history of a process // @Description Get the logs and the log history of a process.
// @Tags v16.7.2
// @ID process-3-get-report // @ID process-3-get-report
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -322,7 +339,8 @@ func (h *RestreamHandler) GetReport(c echo.Context) error {
// Probe probes a process // Probe probes a process
// @Summary Probe a process // @Summary Probe a process
// @Description Probe an existing process to get a detailed stream information on the inputs // @Description Probe an existing process to get a detailed stream information on the inputs.
// @Tags v16.7.2
// @ID process-3-probe // @ID process-3-probe
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -342,7 +360,8 @@ func (h *RestreamHandler) Probe(c echo.Context) error {
// Skills returns the detected FFmpeg capabilities // Skills returns the detected FFmpeg capabilities
// @Summary FFmpeg capabilities // @Summary FFmpeg capabilities
// @Description List all detected FFmpeg capabilities // @Description List all detected FFmpeg capabilities.
// @Tags v16.7.2
// @ID skills-3 // @ID skills-3
// @Produce json // @Produce json
// @Success 200 {object} api.Skills // @Success 200 {object} api.Skills
@@ -359,7 +378,8 @@ func (h *RestreamHandler) Skills(c echo.Context) error {
// ReloadSkills will refresh the FFmpeg capabilities // ReloadSkills will refresh the FFmpeg capabilities
// @Summary Refresh FFmpeg capabilities // @Summary Refresh FFmpeg capabilities
// @Description Refresh the available FFmpeg capabilities // @Description Refresh the available FFmpeg capabilities.
// @Tags v16.7.2
// @ID skills-3-reload // @ID skills-3-reload
// @Produce json // @Produce json
// @Success 200 {object} api.Skills // @Success 200 {object} api.Skills
@@ -378,6 +398,7 @@ func (h *RestreamHandler) ReloadSkills(c echo.Context) error {
// GetProcessMetadata returns the metadata stored with a process // GetProcessMetadata returns the metadata stored with a process
// @Summary Retrieve JSON metadata stored with a process under a key // @Summary Retrieve JSON metadata stored with a process under a key
// @Description Retrieve the previously stored JSON metadata under the given key. If the key is empty, all metadata will be returned. // @Description Retrieve the previously stored JSON metadata under the given key. If the key is empty, all metadata will be returned.
// @Tags v16.7.2
// @ID process-3-get-process-metadata // @ID process-3-get-process-metadata
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -402,6 +423,7 @@ func (h *RestreamHandler) GetProcessMetadata(c echo.Context) error {
// SetProcessMetadata stores metadata with a process // SetProcessMetadata stores metadata with a process
// @Summary Add JSON metadata with a process under the given key // @Summary Add JSON metadata with a process under the given key
// @Description Add arbitrary JSON metadata under the given key. If the key exists, all already stored metadata with this key will be overwritten. If the key doesn't exist, it will be created. // @Description Add arbitrary JSON metadata under the given key. If the key exists, all already stored metadata with this key will be overwritten. If the key doesn't exist, it will be created.
// @Tags v16.7.2
// @ID process-3-set-process-metadata // @ID process-3-set-process-metadata
// @Produce json // @Produce json
// @Param id path string true "Process ID" // @Param id path string true "Process ID"
@@ -436,6 +458,7 @@ func (h *RestreamHandler) SetProcessMetadata(c echo.Context) error {
// GetMetadata returns the metadata stored with the Restreamer // GetMetadata returns the metadata stored with the Restreamer
// @Summary Retrieve JSON metadata from a key // @Summary Retrieve JSON metadata from a key
// @Description Retrieve the previously stored JSON metadata under the given key. If the key is empty, all metadata will be returned. // @Description Retrieve the previously stored JSON metadata under the given key. If the key is empty, all metadata will be returned.
// @Tags v16.7.2
// @ID metadata-3-get // @ID metadata-3-get
// @Produce json // @Produce json
// @Param key path string true "Key for data store" // @Param key path string true "Key for data store"
@@ -458,6 +481,7 @@ func (h *RestreamHandler) GetMetadata(c echo.Context) error {
// SetMetadata stores metadata with the Restreamer // SetMetadata stores metadata with the Restreamer
// @Summary Add JSON metadata under the given key // @Summary Add JSON metadata under the given key
// @Description Add arbitrary JSON metadata under the given key. If the key exists, all already stored metadata with this key will be overwritten. If the key doesn't exist, it will be created. // @Description Add arbitrary JSON metadata under the given key. If the key exists, all already stored metadata with this key will be overwritten. If the key doesn't exist, it will be created.
// @Tags v16.7.2
// @ID metadata-3-set // @ID metadata-3-set
// @Produce json // @Produce json
// @Param key path string true "Key for data store" // @Param key path string true "Key for data store"

View File

@@ -23,7 +23,8 @@ func NewRTMP(rtmp rtmp.Server) *RTMPHandler {
// ListChannels lists all currently publishing RTMP streams // ListChannels lists all currently publishing RTMP streams
// @Summary List all publishing RTMP streams // @Summary List all publishing RTMP streams
// @Description List all currently publishing RTMP streams // @Description List all currently publishing RTMP streams.
// @Tags v16.7.2
// @ID rtmp-3-list-channels // @ID rtmp-3-list-channels
// @Produce json // @Produce json
// @Success 200 {array} api.RTMPChannel // @Success 200 {array} api.RTMPChannel

View File

@@ -25,7 +25,8 @@ func NewSession(registry session.RegistryReader) *SessionHandler {
// Summary returns a summary of all active and past sessions // Summary returns a summary of all active and past sessions
// @Summary Get a summary of all active and past sessions // @Summary Get a summary of all active and past sessions
// @Description Get a summary of all active and past sessions of the given collector // @Description Get a summary of all active and past sessions of the given collector.
// @Tags v16.7.2
// @ID session-3-summary // @ID session-3-summary
// @Produce json // @Produce json
// @Security ApiKeyAuth // @Security ApiKeyAuth
@@ -49,7 +50,8 @@ func (s *SessionHandler) Summary(c echo.Context) error {
// Active returns a list of active sessions // Active returns a list of active sessions
// @Summary Get a minimal summary of all active sessions // @Summary Get a minimal summary of all active sessions
// @Description Get a minimal summary of all active sessions (i.e. number of sessions, bandwidth) // @Description Get a minimal summary of all active sessions (i.e. number of sessions, bandwidth).
// @Tags v16.7.2
// @ID session-3-current // @ID session-3-current
// @Produce json // @Produce json
// @Security ApiKeyAuth // @Security ApiKeyAuth

View File

@@ -24,6 +24,7 @@ func NewSRT(srt srt.Server) *SRTHandler {
// ListChannels lists all currently publishing SRT streams // ListChannels lists all currently publishing SRT streams
// @Summary List all publishing SRT treams // @Summary List all publishing SRT treams
// @Description List all currently publishing SRT streams. This endpoint is EXPERIMENTAL and may change in future. // @Description List all currently publishing SRT streams. This endpoint is EXPERIMENTAL and may change in future.
// @Tags v16.9.0
// @ID srt-3-list-channels // @ID srt-3-list-channels
// @Produce json // @Produce json
// @Success 200 {array} []api.SRTChannel // @Success 200 {array} []api.SRTChannel

View File

@@ -2,6 +2,7 @@ package api
import ( import (
"net/http" "net/http"
"strings"
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util" "github.com/datarhei/core/v16/http/handler/util"
@@ -33,6 +34,7 @@ func NewWidget(config WidgetConfig) *WidgetHandler {
// Get returns minimal public statistics about a process // Get returns minimal public statistics about a process
// @Summary Fetch minimal statistics about a process // @Summary Fetch minimal statistics about a process
// @Description Fetch minimal statistics about a process, which is not protected by any auth. // @Description Fetch minimal statistics about a process, which is not protected by any auth.
// @Tags v16.7.2
// @ID widget-3-get // @ID widget-3-get
// @Produce json // @Produce json
// @Param id path string true "ID of a process" // @Param id path string true "ID of a process"
@@ -73,13 +75,19 @@ func (w *WidgetHandler) Get(c echo.Context) error {
summary := collector.Summary() summary := collector.Summary()
for _, session := range summary.Active { for _, session := range summary.Active {
if session.Reference == process.Reference { if !strings.HasPrefix(session.Reference, process.Reference) {
data.CurrentSessions++ continue
}
} }
if s, ok := summary.Summary.References[process.Reference]; ok { data.CurrentSessions++
data.TotalSessions = s.TotalSessions }
for reference, s := range summary.Summary.References {
if !strings.HasPrefix(reference, process.Reference) {
continue
}
data.TotalSessions += s.TotalSessions
} }
return c.JSON(http.StatusOK, data) return c.JSON(http.StatusOK, data)

View File

@@ -1,94 +0,0 @@
package handler
import (
"net/http"
"path/filepath"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/cache"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/io/fs"
"github.com/labstack/echo/v4"
)
// The DiskFSHandler type provides handlers for manipulating a filesystem
type DiskFSHandler struct {
cache cache.Cacher
filesystem fs.Filesystem
}
// NewDiskFS return a new DiskFS type. You have to provide a filesystem to act on and optionally
// a Cacher where files will be purged from if the Cacher is related to the filesystem.
func NewDiskFS(fs fs.Filesystem, cache cache.Cacher) *DiskFSHandler {
return &DiskFSHandler{
cache: cache,
filesystem: fs,
}
}
// GetFile returns the file at the given path
// @Summary Fetch a file from the filesystem
// @Description Fetch a file from the filesystem. If the file is a directory, a index.html is returned, if it exists.
// @ID diskfs-get-file
// @Produce application/data
// @Produce json
// @Param path path string true "Path to file"
// @Success 200 {file} byte
// @Success 301 {string} string
// @Failure 404 {object} api.Error
// @Router /{path} [get]
func (h *DiskFSHandler) GetFile(c echo.Context) error {
path := util.PathWildcardParam(c)
mimeType := c.Response().Header().Get(echo.HeaderContentType)
c.Response().Header().Del(echo.HeaderContentType)
file := h.filesystem.Open(path)
if file == nil {
return api.Err(http.StatusNotFound, "File not found", path)
}
stat, err := file.Stat()
if err != nil {
return api.Err(http.StatusNotFound, "File not found", path)
}
if stat.IsDir() {
path = filepath.Join(path, "index.html")
file.Close()
file = h.filesystem.Open(path)
if file == nil {
return api.Err(http.StatusNotFound, "File not found", path)
}
stat, err = file.Stat()
if err != nil {
return api.Err(http.StatusNotFound, "File not found", path)
}
}
defer file.Close()
c.Response().Header().Set("Last-Modified", stat.ModTime().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
if path, ok := stat.IsLink(); ok {
path = filepath.Clean("/" + path)
if path[0] == '/' {
path = path[1:]
}
return c.Redirect(http.StatusMovedPermanently, path)
}
c.Response().Header().Set(echo.HeaderContentType, mimeType)
if c.Request().Method == "HEAD" {
return c.Blob(http.StatusOK, "application/data", nil)
}
return c.Stream(http.StatusOK, "application/data", file)
}

179
http/handler/filesystem.go Normal file
View File

@@ -0,0 +1,179 @@
package handler
import (
"net/http"
"path/filepath"
"sort"
"strings"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/fs"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/labstack/echo/v4"
)
// The FSHandler type provides handlers for manipulating a filesystem
type FSHandler struct {
fs fs.FS
}
// NewFS return a new FSHandler type. You have to provide a filesystem to act on.
func NewFS(fs fs.FS) *FSHandler {
return &FSHandler{
fs: fs,
}
}
func (h *FSHandler) GetFile(c echo.Context) error {
path := util.PathWildcardParam(c)
mimeType := c.Response().Header().Get(echo.HeaderContentType)
c.Response().Header().Del(echo.HeaderContentType)
file := h.fs.Filesystem.Open(path)
if file == nil {
return api.Err(http.StatusNotFound, "File not found", path)
}
stat, _ := file.Stat()
if len(h.fs.DefaultFile) != 0 {
if stat.IsDir() {
path = filepath.Join(path, h.fs.DefaultFile)
file.Close()
file = h.fs.Filesystem.Open(path)
if file == nil {
return api.Err(http.StatusNotFound, "File not found", path)
}
stat, _ = file.Stat()
}
}
defer file.Close()
c.Response().Header().Set("Last-Modified", stat.ModTime().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
if path, ok := stat.IsLink(); ok {
path = filepath.Clean("/" + path)
if path[0] == '/' {
path = path[1:]
}
return c.Redirect(http.StatusMovedPermanently, path)
}
c.Response().Header().Set(echo.HeaderContentType, mimeType)
if c.Request().Method == "HEAD" {
return c.Blob(http.StatusOK, "application/data", nil)
}
return c.Stream(http.StatusOK, "application/data", file)
}
func (h *FSHandler) PutFile(c echo.Context) error {
path := util.PathWildcardParam(c)
c.Response().Header().Del(echo.HeaderContentType)
req := c.Request()
_, created, err := h.fs.Filesystem.WriteFileReader(path, req.Body)
if err != nil {
return api.Err(http.StatusBadRequest, "Bad request", "%s", err)
}
if h.fs.Cache != nil {
h.fs.Cache.Delete(path)
if len(h.fs.DefaultFile) != 0 {
if strings.HasSuffix(path, "/"+h.fs.DefaultFile) {
path := strings.TrimSuffix(path, h.fs.DefaultFile)
h.fs.Cache.Delete(path)
}
}
}
c.Response().Header().Set("Content-Location", req.URL.RequestURI())
if created {
return c.String(http.StatusCreated, "")
}
return c.NoContent(http.StatusNoContent)
}
func (h *FSHandler) DeleteFile(c echo.Context) error {
path := util.PathWildcardParam(c)
c.Response().Header().Del(echo.HeaderContentType)
size := h.fs.Filesystem.Remove(path)
if h.fs.Cache != nil {
h.fs.Cache.Delete(path)
if len(h.fs.DefaultFile) != 0 {
if strings.HasSuffix(path, "/"+h.fs.DefaultFile) {
path := strings.TrimSuffix(path, h.fs.DefaultFile)
h.fs.Cache.Delete(path)
}
}
}
if size < 0 {
return api.Err(http.StatusNotFound, "File not found", path)
}
return c.String(http.StatusOK, "Deleted: "+path)
}
func (h *FSHandler) ListFiles(c echo.Context) error {
pattern := util.DefaultQuery(c, "glob", "")
sortby := util.DefaultQuery(c, "sort", "none")
order := util.DefaultQuery(c, "order", "asc")
files := h.fs.Filesystem.List("/", pattern)
var sortFunc func(i, j int) bool
switch sortby {
case "name":
if order == "desc" {
sortFunc = func(i, j int) bool { return files[i].Name() > files[j].Name() }
} else {
sortFunc = func(i, j int) bool { return files[i].Name() < files[j].Name() }
}
case "size":
if order == "desc" {
sortFunc = func(i, j int) bool { return files[i].Size() > files[j].Size() }
} else {
sortFunc = func(i, j int) bool { return files[i].Size() < files[j].Size() }
}
default:
if order == "asc" {
sortFunc = func(i, j int) bool { return files[i].ModTime().Before(files[j].ModTime()) }
} else {
sortFunc = func(i, j int) bool { return files[i].ModTime().After(files[j].ModTime()) }
}
}
sort.Slice(files, sortFunc)
var fileinfos []api.FileInfo = make([]api.FileInfo, len(files))
for i, f := range files {
fileinfos[i] = api.FileInfo{
Name: f.Name(),
Size: f.Size(),
LastMod: f.ModTime().Unix(),
}
}
return c.JSON(http.StatusOK, fileinfos)
}

View File

@@ -1,133 +0,0 @@
package handler
import (
"net/http"
"path/filepath"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/io/fs"
"github.com/labstack/echo/v4"
)
// The MemFSHandler type provides handlers for manipulating a filesystem
type MemFSHandler struct {
filesystem fs.Filesystem
}
// NewMemFS return a new MemFS type. You have to provide a filesystem to act on.
func NewMemFS(fs fs.Filesystem) *MemFSHandler {
return &MemFSHandler{
filesystem: fs,
}
}
// GetFile returns the file at the given path
// @Summary Fetch a file from the memory filesystem
// @Description Fetch a file from the memory filesystem
// @ID memfs-get-file
// @Produce application/data
// @Produce json
// @Param path path string true "Path to file"
// @Success 200 {file} byte
// @Success 301 {string} string
// @Failure 404 {object} api.Error
// @Router /memfs/{path} [get]
func (h *MemFSHandler) GetFile(c echo.Context) error {
path := util.PathWildcardParam(c)
mimeType := c.Response().Header().Get(echo.HeaderContentType)
c.Response().Header().Del(echo.HeaderContentType)
file := h.filesystem.Open(path)
if file == nil {
return api.Err(http.StatusNotFound, "File not found", path)
}
defer file.Close()
stat, err := file.Stat()
if err != nil {
return api.Err(http.StatusInternalServerError, "File.Stat() failed", "%s", err)
}
c.Response().Header().Set("Last-Modified", stat.ModTime().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
if path, ok := stat.IsLink(); ok {
path = filepath.Clean("/" + path)
if path[0] == '/' {
path = path[1:]
}
return c.Redirect(http.StatusMovedPermanently, path)
}
c.Response().Header().Set(echo.HeaderContentType, mimeType)
if c.Request().Method == "HEAD" {
return c.Blob(http.StatusOK, "application/data", nil)
}
return c.Stream(http.StatusOK, "application/data", file)
}
// PutFile adds or overwrites a file at the given path
// @Summary Add a file to the memory filesystem
// @Description Writes or overwrites a file on the memory filesystem
// @ID memfs-put-file
// @Accept application/data
// @Produce text/plain
// @Produce json
// @Param path path string true "Path to file"
// @Param data body []byte true "File data"
// @Success 201 {string} string
// @Success 204 {string} string
// @Failure 507 {object} api.Error
// @Security BasicAuth
// @Router /memfs/{path} [put]
func (h *MemFSHandler) PutFile(c echo.Context) error {
path := util.PathWildcardParam(c)
c.Response().Header().Del(echo.HeaderContentType)
req := c.Request()
_, created, err := h.filesystem.Store(path, req.Body)
if err != nil {
return api.Err(http.StatusBadRequest, "%s", err)
}
c.Response().Header().Set("Content-Location", req.URL.RequestURI())
if created {
return c.String(http.StatusCreated, "")
}
return c.NoContent(http.StatusNoContent)
}
// DeleteFile removes a file from the filesystem
// @Summary Remove a file from the memory filesystem
// @Description Remove a file from the memory filesystem
// @ID memfs-delete-file
// @Produce text/plain
// @Param path path string true "Path to file"
// @Success 200 {string} string
// @Failure 404 {object} api.Error
// @Security BasicAuth
// @Router /memfs/{path} [delete]
func (h *MemFSHandler) DeleteFile(c echo.Context) error {
path := util.PathWildcardParam(c)
c.Response().Header().Del(echo.HeaderContentType)
size := h.filesystem.Delete(path)
if size < 0 {
return api.Err(http.StatusNotFound, "File not found", path)
}
return c.String(http.StatusOK, "Deleted: "+path)
}

View File

@@ -57,31 +57,18 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
if req.Method != "GET" { if req.Method != "GET" {
res.Header().Set("X-Cache", "SKIP ONLYGET") res.Header().Set("X-Cache", "SKIP ONLYGET")
return next(c)
if err := next(c); err != nil {
c.Error(err)
} }
return nil
}
res.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.0f", config.Cache.TTL().Seconds()))
key := strings.TrimPrefix(req.URL.Path, config.Prefix) key := strings.TrimPrefix(req.URL.Path, config.Prefix)
if !config.Cache.IsExtensionCacheable(path.Ext(req.URL.Path)) { if !config.Cache.IsExtensionCacheable(path.Ext(req.URL.Path)) {
res.Header().Set("X-Cache", "SKIP EXT") res.Header().Set("X-Cache", "SKIP EXT")
return next(c)
if err := next(c); err != nil {
c.Error(err)
}
return nil
} }
if obj, expireIn, _ := config.Cache.Get(key); obj == nil { if obj, expireIn, _ := config.Cache.Get(key); obj == nil {
// cache miss // cache miss
writer := res.Writer writer := res.Writer
w := &cacheWriter{ w := &cacheWriter{
@@ -105,6 +92,7 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
if res.Status != 200 { if res.Status != 200 {
res.Header().Set("X-Cache", "SKIP NOTOK") res.Header().Set("X-Cache", "SKIP NOTOK")
res.Writer.WriteHeader(res.Status)
return nil return nil
} }
@@ -112,6 +100,7 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
if !config.Cache.IsSizeCacheable(size) { if !config.Cache.IsSizeCacheable(size) {
res.Header().Set("X-Cache", "SKIP TOOBIG") res.Header().Set("X-Cache", "SKIP TOOBIG")
res.Writer.WriteHeader(res.Status)
return nil return nil
} }
@@ -123,11 +112,13 @@ func NewWithConfig(config Config) echo.MiddlewareFunc {
if err := config.Cache.Put(key, o, size); err != nil { if err := config.Cache.Put(key, o, size); err != nil {
res.Header().Set("X-Cache", "SKIP TOOBIG") res.Header().Set("X-Cache", "SKIP TOOBIG")
res.Writer.WriteHeader(res.Status)
return nil return nil
} }
res.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.0f", expireIn.Seconds())) res.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.0f", expireIn.Seconds()))
res.Header().Set("X-Cache", "MISS") res.Header().Set("X-Cache", "MISS")
res.Writer.WriteHeader(res.Status)
} else { } else {
// cache hit // cache hit
o := obj.(*cacheObject) o := obj.(*cacheObject)
@@ -190,7 +181,5 @@ func (w *cacheWriter) WriteHeader(code int) {
} }
func (w *cacheWriter) Write(body []byte) (int, error) { func (w *cacheWriter) Write(body []byte) (int, error) {
n, err := w.body.Write(body) return w.body.Write(body)
return n, err
} }

100
http/middleware/cache/cache_test.go vendored Normal file
View File

@@ -0,0 +1,100 @@
package cache
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/datarhei/core/v16/http/cache"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
)
func TestCache(t *testing.T) {
c, err := cache.NewLRUCache(cache.LRUConfig{
TTL: 300 * time.Second,
MaxSize: 0,
MaxFileSize: 16,
AllowExtensions: []string{".js"},
BlockExtensions: []string{".ts"},
Logger: nil,
})
require.NoError(t, err)
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/found.js", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)
handler := NewWithConfig(Config{
Cache: c,
})(func(c echo.Context) error {
if c.Request().URL.Path == "/found.js" {
c.Response().Write([]byte("test"))
} else if c.Request().URL.Path == "/toobig.js" {
c.Response().Write([]byte("testtesttesttesttest"))
} else if c.Request().URL.Path == "/blocked.ts" {
c.Response().Write([]byte("blocked"))
}
c.Response().WriteHeader(http.StatusNotFound)
return nil
})
handler(ctx)
require.Equal(t, "test", rec.Body.String())
require.Equal(t, 200, rec.Result().StatusCode)
require.Equal(t, "MISS", rec.Result().Header.Get("x-cache"))
rec = httptest.NewRecorder()
ctx = e.NewContext(req, rec)
handler(ctx)
require.Equal(t, "test", rec.Body.String())
require.Equal(t, 200, rec.Result().StatusCode)
require.Equal(t, "HIT", rec.Result().Header.Get("x-cache")[:3])
req = httptest.NewRequest(http.MethodGet, "/notfound.js", nil)
rec = httptest.NewRecorder()
ctx = e.NewContext(req, rec)
handler(ctx)
require.Equal(t, 404, rec.Result().StatusCode)
require.Equal(t, "SKIP NOTOK", rec.Result().Header.Get("x-cache"))
req = httptest.NewRequest(http.MethodGet, "/toobig.js", nil)
rec = httptest.NewRecorder()
ctx = e.NewContext(req, rec)
handler(ctx)
require.Equal(t, "testtesttesttesttest", rec.Body.String())
require.Equal(t, 200, rec.Result().StatusCode)
require.Equal(t, "SKIP TOOBIG", rec.Result().Header.Get("x-cache"))
req = httptest.NewRequest(http.MethodGet, "/blocked.ts", nil)
rec = httptest.NewRecorder()
ctx = e.NewContext(req, rec)
handler(ctx)
require.Equal(t, "blocked", rec.Body.String())
require.Equal(t, 200, rec.Result().StatusCode)
require.Equal(t, "SKIP EXT", rec.Result().Header.Get("x-cache"))
req = httptest.NewRequest(http.MethodPost, "/found.js", nil)
rec = httptest.NewRecorder()
ctx = e.NewContext(req, rec)
handler(ctx)
require.Equal(t, "test", rec.Body.String())
require.Equal(t, 200, rec.Result().StatusCode)
require.Equal(t, "SKIP ONLYGET", rec.Result().Header.Get("x-cache"))
}

Some files were not shown because too many files have changed in this diff Show More