diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000..c960bfa --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,21 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base", + ":timezone(Asia/Tokyo)", + ":combinePatchMinorReleases", + ":prHourlyLimitNone", + ":prConcurrentLimit10", + "group:recommended", + "group:allNonMajor", + "schedule:weekly" + ], + "dependencyDashboard": false, + "packageRules": [ + { + "matchUpdateTypes": ["minor", "patch", "pin", "digest"], + "platformAutomerge": true, + "automerge": true + } + ] +} \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..02c07f9 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,30 @@ +name: ci +on: + push: + branches-ignore: + - "master" + tags-ignore: + - "*" + +jobs: + build: + name: ci + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: setup go + uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + cache: true + cache-dependency-path: ./go.sum + - run: go version + - run: go fmt . + - uses: dominikh/staticcheck-action@v1 + with: + version: "2023.1.5" + install-go: false + - name: Test + run: make test + - name: Build + run: make diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..11a7d8e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: release + +on: + push: + tags: + - "*" + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + cache: false + + - run: go install github.com/tcnksm/ghr@latest + + - name: Build + run: | + GOOS=linux GOARCH=amd64 go build -o dist/sora-archive-uploader_linux_amd64 cmd/sora-archive-uploader/main.go + GOOS=darwin GOARCH=amd64 go build -o dist/sora-archive-uploader_darwin_amd64 cmd/sora-archive-uploader/main.go + GOOS=darwin GOARCH=arm64 go build -o dist/sora-archive-uploader_darwin_arm64 cmd/sora-archive-uploader/main.go + gzip dist/* + + - name: Release + run: | + ghr -t "${{ secrets.GITHUB_TOKEN }}" -u "${{ github.repository_owner }}" -r "sora-archive-uploader" --replace "${GITHUB_REF##*/}" dist/ diff --git a/.gitignore b/.gitignore index 53a8a5e..dc62d1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ bin/* -config.toml -*.jsonl \ No newline at end of file +config.ini +*.jsonl diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..71a72b0 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,12 @@ +# 変更履歴 + +- CHANGE + - 下位互換のない変更 +- UPDATE + - 下位互換がある変更 +- ADD + - 下位互換がある追加 +- FIX + - バグ修正 + +## develop diff --git a/Makefile b/Makefile index f3c1139..25d8e79 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,7 @@ -VERSION := 2022.1.0 -REVISION := $(shell git rev-parse --short HEAD) -BUILD_DATE := $(shell date -u "+%Y-%m-%dT%H:%M:%SZ") -LDFLAGS := "-X main.version=$(VERSION) -X main.revision=$(REVISION) -X main.buildDate=$(BUILD_DATE)" -LDFLAGS_PROD := "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" +.PHONY: all test -export GO1111MODULE=on -export CWD=$(dir $(abspath $(lastword $(MAKEFILE_LIST)))) - -.PHONY: all sora-archive-uploader-dev sora-archive-uploader-prod -all: sora-archive-uploader-dev - -sora-archive-uploader-dev: cmd/sora-archive-uploader/main.go - go build -race -ldflags $(LDFLAGS) -o bin/$@ $< - -sora-archive-uploader-prod: cmd/sora-archive-uploader/main.go - go build -ldflags $(LDFLAGS_PROD) -o bin/$@ $< +all: + go build -o bin/sora-archive-uploader cmd/sora-archive-uploader/main.go test: - go test -v + go test -race -v ./s3 \ No newline at end of file diff --git a/README.md b/README.md index aa8ff78..94c404a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Sora Archive Uploader + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) + ## About Shiguredo's open source software @@ -46,17 +48,17 @@ Sora Cloud では出力されたファイルをオブジェクトストレージ ## まずは使ってみる -config.toml に必要な情報を設定してください。 +config.ini に必要な情報を設定してください。 ```console -$ cp config.example.com config.toml +$ cp config_example.ini config.ini ``` make でビルドして実行します。 ```console $ make -$ ./bin/sora-archive-uploader-dev -C config.toml +$ ./bin/sora-archive-uploader-dev -C config.ini ``` ## Discord @@ -71,7 +73,7 @@ https://discord.gg/shiguredo - オープンソースでの公開が前提 - 可能であれば企業名の公開 - - 公開が難しい場合は `企業名非公開` と書かせていただきます + - 公開が難しい場合は `企業名非公開` と書かせていただきます ### 機能 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..f970d5b --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2023.1.0 \ No newline at end of file diff --git a/cmd/sora-archive-uploader/main.go b/cmd/sora-archive-uploader/main.go index fcfdcac..fcdb834 100644 --- a/cmd/sora-archive-uploader/main.go +++ b/cmd/sora-archive-uploader/main.go @@ -4,35 +4,23 @@ import ( "flag" "fmt" "log" - "os" archive "github.com/shiguredo/sora-archive-uploader" ) -var ( - version string - revision string - buildDate string - - versionText = `sora-archive-uploader build info. -version: %s -revision: %s -build date: %s -` -) - func main() { - configFilePath := flag.String("C", "config.toml", "Config file path") - var v bool - flag.BoolVar(&v, "version", false, "Show version") + // /bin/sora-archive-uploader -V + showVersion := flag.Bool("V", false, "バージョン") + + // /bin/sora-archive-uploader -C ./config.ini + configFilePath := flag.String("C", "./config.ini", "Config file path") flag.Parse() - if v { - fmt.Printf(versionText, version, revision, buildDate) - os.Exit(0) + if *showVersion { + fmt.Printf("Sora Archive Uploader version %s\n", archive.Version) + return } - log.Printf("sora-archive-uploader version:%s revision:%s build_date:%s", version, revision, buildDate) log.Printf("config file path: %s", *configFilePath) archive.Run(configFilePath) } diff --git a/config.go b/config.go index 4cb93b4..a16a3d9 100644 --- a/config.go +++ b/config.go @@ -1,56 +1,79 @@ package archive import ( - "github.com/BurntSushi/toml" + _ "embed" + + "gopkg.in/ini.v1" +) + +//go:embed VERSION +var Version string + +const ( + DefaultLogDir = "." + DefaultLogName = "sora-archive-uploader.jsonl" + + // megabytes + DefaultLogRotateMaxSize = 200 + DefaultLogRotateMaxBackups = 7 + // days + DefaultLogRotateMaxAge = 30 ) type Config struct { - Debug bool `toml:"debug"` + Debug bool `ini:"debug"` - LogDir string `toml:"log_dir"` - LogName string `toml:"log_name"` - LogStdOut bool `toml:"log_std_out"` - LogRotateMaxSize int `toml:"log_rotate_max_size"` - LogRotateMaxBackups int `toml:"log_rotate_max_backups"` - LogRotateMaxAge int `toml:"log_rotate_max_age"` - LogRotateCompress bool `toml:"log_rotate_compress"` + LogDir string `ini:"log_dir"` + LogName string `ini:"log_name"` + LogStdout bool `ini:"log_stdout"` - ObjectStorageEndpoint string `toml:"object_storage_endpoint"` - ObjectStorageBucketName string `toml:"object_storage_bucket_name"` - ObjectStorageAccessKeyID string `toml:"object_storage_access_key_id"` - ObjectStorageSecretAccessKey string `toml:"object_storage_secret_access_key"` + LogRotateMaxSize int `ini:"log_rotate_max_size"` + LogRotateMaxBackups int `ini:"log_rotate_max_backups"` + LogRotateMaxAge int `ini:"log_rotate_max_age"` + LogRotateCompress bool `ini:"log_rotate_compress"` - SoraArchiveDirFullPath string `toml:"archive_dir_full_path"` - SoraEvacuateDirFullPath string `toml:"evacuate_dir_full_path"` + ObjectStorageEndpoint string `ini:"object_storage_endpoint"` + ObjectStorageBucketName string `ini:"object_storage_bucket_name"` + ObjectStorageAccessKeyID string `ini:"object_storage_access_key_id"` + ObjectStorageSecretAccessKey string `ini:"object_storage_secret_access_key"` - UploadWorkers int `toml:"upload_workers"` + SoraArchiveDirFullPath string `ini:"archive_dir_full_path"` + SoraEvacuateDirFullPath string `ini:"evacuate_dir_full_path"` - UploadedFileCacheSize int `toml:"uploaded_file_cache_size"` + UploadWorkers int `ini:"upload_workers"` - WebhookEndpointURL string `toml:"webhook_endpoint_url"` - WebhookEndpointHealthCheckURL string `toml:"webhook_endpoint_health_check_url"` + // 1 ファイルあたりのアップロードレート制限 + UploadFileRateLimitMbps int `ini:"upload_file_rate_limit_mbps"` - WebhookTypeHeaderName string `toml:"webhook_type_header_name"` - WebhookTypeArchiveUploaded string `toml:"webhook_type_archive_uploaded"` - WebhookTypeSplitArchiveUploaded string `toml:"webhook_type_split_archive_uploaded"` - WebhookTypeSplitArchiveEndUploaded string `toml:"webhook_type_split_archive_end_uploaded"` - WebhookTypeReportUploaded string `toml:"webhook_type_report_uploaded"` + UploadedFileCacheSize int `ini:"uploaded_file_cache_size"` - WebhookBasicAuthUsername string `toml:"webhook_basic_auth_username"` - WebhookBasicAuthPassword string `toml:"webhook_basic_auth_password"` + WebhookEndpointURL string `ini:"webhook_endpoint_url"` + WebhookEndpointHealthCheckURL string `ini:"webhook_endpoint_health_check_url"` - WebhookRequestTimeoutS int32 `toml:"webhook_request_timeout_s"` + WebhookTypeHeaderName string `ini:"webhook_type_header_name"` + WebhookTypeArchiveUploaded string `ini:"webhook_type_archive_uploaded"` + WebhookTypeSplitArchiveUploaded string `ini:"webhook_type_split_archive_uploaded"` + WebhookTypeSplitArchiveEndUploaded string `ini:"webhook_type_split_archive_end_uploaded"` + WebhookTypeReportUploaded string `ini:"webhook_type_report_uploaded"` - WebhookTlsVerifyCacertPath string `toml:"webhook_tls_verify_cacert_path"` - WebhookTlsFullchainPath string `toml:"webhook_tls_fullchain_path"` - WebhookTlsPrivkeyPath string `toml:"webhook_tls_privkey_path"` + WebhookBasicAuthUsername string `ini:"webhook_basic_auth_username"` + WebhookBasicAuthPassword string `ini:"webhook_basic_auth_password"` + + WebhookRequestTimeoutS int32 `ini:"webhook_request_timeout_s"` + + WebhookTLSVerifyCacertPath string `ini:"webhook_tls_verify_cacert_path"` + WebhookTLSFullchainPath string `ini:"webhook_tls_fullchain_path"` + WebhookTLSPrivkeyPath string `ini:"webhook_tls_privkey_path"` } -func initConfig(data []byte, config interface{}) error { - if err := toml.Unmarshal(data, config); err != nil { - return err +func newConfig(configFilePath string) (*Config, error) { + config := new(Config) + iniConfig, err := ini.InsensitiveLoad(configFilePath) + if err != nil { + return nil, err } - - // TODO: 初期値 - return nil + if err := iniConfig.StrictMapTo(config); err != nil { + return nil, err + } + return config, nil } diff --git a/config.example.toml b/config_example.ini similarity index 69% rename from config.example.toml rename to config_example.ini index 07893aa..c75e5a2 100644 --- a/config.example.toml +++ b/config_example.ini @@ -1,9 +1,9 @@ debug = false # Sora の録画アーカイブディレクトリのフルパス -archive_dir_full_path = "" +# archive_dir_full_path = /path/to/archive # アップロードに失敗した際の待避ディレクトリのフルパス -evacuate_dir_full_path = "" +# evacuate_dir_full_path = /path/to/evacuate # 同時アップロード数 upload_workers = 4 @@ -14,11 +14,15 @@ upload_workers = 4 # 大きめのキャッシュサイズを設定することをおすすめします。 uploaded_file_cache_size = 32 -# [log] +# 1 ファイルあたりのアップロード速度制限 +# 0 または 10 以上 +# 0 の場合は制限しません +# upload_file_rate_limit_mbps = 0 -log_dir = "." -log_name = "sora-archive-uploader.jsonl" -log_std_out = true +# [log] +log_dir = . +log_name = sora-archive-uploader.jsonl +log_stdout = true # MB log_rotate_max_size = 200 @@ -30,16 +34,16 @@ log_rotate_compress = false # [object_storage] # アップロード先の S3 または S3 互換オブジェクトストレージの設定 -object_storage_endpoint = "" -object_storage_bucket_name = "" -object_storage_access_key_id = "" -object_storage_secret_access_key = "" +# object_storage_endpoint = https://s3.example.com +# object_storage_bucket_name = bucket-name +# object_storage_access_key_id = access-key-id +# object_storage_secret_access_key = secret-access-key # オブジェクトストレージにアップロードが完了した際に通知するウェブフック # [webhook] # 空文字列の場合はウェブフックは飛ばさない -webhook_endpoint_url = "" +# webhook_endpoint_url = https://example.com/webhook # ウェブフックリクエストのタイムアウト時間 (秒) webhook_request_timeout_s = 30 @@ -53,12 +57,12 @@ webhook_type_report_uploaded = "recording-report.uploaded" # ウェブフックのベーシック認証 # 空文字はベーシック認証を行わない -webhook_basic_auth_username = "" -webhook_basic_auth_password = "" +# webhook_basic_auth_username = username +# webhook_basic_auth_password = password # webhook で HTTPS を利用する場合にサーバーの証明書をベリファイする場合に指定 # 指定しない場合は OS のものを利用し、サーバー名までは検証しません -webhook_tls_verify_cacert_path = "" +# webhook_tls_verify_cacert_path = /path/to/cacert.pem # webhook で mTLS を利用する場合に指定します -webhook_tls_fullchain_path = "" -webhook_tls_privkey_path = "" \ No newline at end of file +# webhook_tls_fullchain_path = /path/to/fullchain.pem +# webhook_tls_privkey_path = /path/to/privkey.pem diff --git a/gatekeeper.go b/gatekeeper.go index 3d1849c..459a740 100644 --- a/gatekeeper.go +++ b/gatekeeper.go @@ -32,7 +32,7 @@ func (ru *RecordingUnit) run() { zlog.Debug(). Str("recording_id", ru.recordingID). Int32("counter", ru.counter). - Msgf("RUN-RECORDING-UNIT") + Msg("RUN-RECORDING-UNIT") } func (ru *RecordingUnit) done() { @@ -40,7 +40,7 @@ func (ru *RecordingUnit) done() { zlog.Debug(). Str("recording_id", ru.recordingID). Int32("counter", ru.counter). - Msgf("DONE-RECORDING-UNIT") + Msg("DONE-RECORDING-UNIT") } func (ru *RecordingUnit) canProcessAndSetReportFile(reportFile string) bool { diff --git a/go.mod b/go.mod index e17395b..fe3c3f7 100644 --- a/go.mod +++ b/go.mod @@ -1,39 +1,37 @@ module github.com/shiguredo/sora-archive-uploader -go 1.19 +go 1.21 require ( - github.com/BurntSushi/toml v1.3.2 + github.com/conduitio/bwlimit v0.1.0 github.com/google/uuid v1.3.1 github.com/minio/minio-go/v7 v7.0.63 github.com/rs/zerolog v1.31.0 - github.com/shiguredo/lumberjack/v3 v3.0.0 github.com/shogo82148/go-clockwork-base32 v1.1.0 github.com/stretchr/testify v1.8.4 + gopkg.in/ini.v1 v1.67.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.17.1 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - golang.org/x/crypto v0.12.0 // indirect - golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.12.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cc6ee5e..d56410b 100644 --- a/go.sum +++ b/go.sum @@ -1,89 +1,32 @@ -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.3.0 h1:Ws8e5YmnrGEHzZEzg0YvK/7COGYtTC5PbaH9oSSbgfA= -github.com/BurntSushi/toml v1.3.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.3.1 h1:rHnDkSK+/g6DlREUK73PkmIs60pqrnuduK+JmP++JmU= -github.com/BurntSushi/toml v1.3.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/conduitio/bwlimit v0.1.0 h1:x3ijON0TSghQob4tFKaEvKixFmYKfVJQeSpXluC2JvE= +github.com/conduitio/bwlimit v0.1.0/go.mod h1:E+ASZ1/5L33MTb8hJTERs5Xnmh6Ulq3jbRh7LrdbXWU= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= -github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= -github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= -github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g= +github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= -github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= -github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.45 h1:g4IeM9M9pW/Lo8AGGNOjBZYlvmtlE1N5TQEYWXRWzIs= -github.com/minio/minio-go/v7 v7.0.45/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= -github.com/minio/minio-go/v7 v7.0.49 h1:dE5DfOtnXMXCjr/HWI6zN9vCrY6Sv666qhhiwUMvGV4= -github.com/minio/minio-go/v7 v7.0.49/go.mod h1:UI34MvQEiob3Cf/gGExGMmzugkM/tNgbFypNDy5LMVc= -github.com/minio/minio-go/v7 v7.0.50 h1:4IL4V8m/kI90ZL6GupCARZVrBv8/XrcKcJhaJ3iz68k= -github.com/minio/minio-go/v7 v7.0.50/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= -github.com/minio/minio-go/v7 v7.0.51 h1:eSewrwc23TqUDEH8aw8Bwp4f+JDdozRrPWcKR7DZhmY= -github.com/minio/minio-go/v7 v7.0.51/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= -github.com/minio/minio-go/v7 v7.0.52 h1:8XhG36F6oKQUDDSuz6dY3rioMzovKjW40W6ANuN0Dps= -github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= -github.com/minio/minio-go/v7 v7.0.53 h1:qtPyQ+b0Cc1ums3LsnVMAYULPNdAGz8qdX8R2zl9XMU= -github.com/minio/minio-go/v7 v7.0.53/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU= -github.com/minio/minio-go/v7 v7.0.56 h1:pkZplIEHu8vinjkmhsexcXpWth2tjVLphrTZx6fBVZY= -github.com/minio/minio-go/v7 v7.0.56/go.mod h1:NUDy4A4oXPq1l2yK6LTSvCEzAMeIcoz9lcj5dbzSrRE= -github.com/minio/minio-go/v7 v7.0.57 h1:xsFiOiWjpC1XAGbFEUOzj1/gMXGz7ljfxifwcb/5YXU= -github.com/minio/minio-go/v7 v7.0.57/go.mod h1:NUDy4A4oXPq1l2yK6LTSvCEzAMeIcoz9lcj5dbzSrRE= -github.com/minio/minio-go/v7 v7.0.58 h1:B9/8Az8Om/2kX8Ys2ai2PZbBTokRE5W6P5OaqnAs6po= -github.com/minio/minio-go/v7 v7.0.58/go.mod h1:NUDy4A4oXPq1l2yK6LTSvCEzAMeIcoz9lcj5dbzSrRE= -github.com/minio/minio-go/v7 v7.0.59 h1:lxIXwsTIcQkYoEG25rUJbzpmSB/oWeVDmxFo/uWUUsw= -github.com/minio/minio-go/v7 v7.0.59/go.mod h1:NUDy4A4oXPq1l2yK6LTSvCEzAMeIcoz9lcj5dbzSrRE= -github.com/minio/minio-go/v7 v7.0.60 h1:iHkrmWyHFs/eZiWc2F/5jAHtNBAFy+HjdhMX6FkkPWc= -github.com/minio/minio-go/v7 v7.0.60/go.mod h1:NUDy4A4oXPq1l2yK6LTSvCEzAMeIcoz9lcj5dbzSrRE= -github.com/minio/minio-go/v7 v7.0.61 h1:87c+x8J3jxQ5VUGimV9oHdpjsAvy3fhneEBKuoKEVUI= -github.com/minio/minio-go/v7 v7.0.61/go.mod h1:BTu8FcrEw+HidY0zd/0eny43QnVNkXRPXrLXFuQBHXg= -github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc= -github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ= github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= -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/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -94,97 +37,40 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= -github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= -github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= -github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= -github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= -github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/shiguredo/lumberjack/v3 v3.0.0 h1:IkhnjVTpg7Fl2fNAfsMn7R+No9SrnFdN5wEWBY2KOT8= -github.com/shiguredo/lumberjack/v3 v3.0.0/go.mod h1:zrQSStRcwpVudSkotZINDGYm8x03gnEQd7M+Y0OzPp0= -github.com/shogo82148/go-clockwork-base32 v1.0.0 h1:tSLEf+tCChBr5Evbg812hYi2b+yAaZZQb9w7v5gxOn4= -github.com/shogo82148/go-clockwork-base32 v1.0.0/go.mod h1:iMSFoMZCgiXZbDyIpEjQxr4T9dH5q8agqxt9HECoiEQ= github.com/shogo82148/go-clockwork-base32 v1.1.0 h1:PEqSTiyVEKdl5ar5RHlUQjFJyJTx9XZD5dUVCLG08K0= github.com/shogo82148/go-clockwork-base32 v1.1.0/go.mod h1:iMSFoMZCgiXZbDyIpEjQxr4T9dH5q8agqxt9HECoiEQ= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= -github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -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.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= -gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging.go b/logging.go index f261919..d090f56 100644 --- a/logging.go +++ b/logging.go @@ -2,66 +2,120 @@ package archive import ( "fmt" - "io" "os" + "path/filepath" "strings" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/shiguredo/lumberjack/v3" + "gopkg.in/natefinch/lumberjack.v2" ) -func initLogger(config Config) error { +func initLogger(config *Config) error { if f, err := os.Stat(config.LogDir); os.IsNotExist(err) || !f.IsDir() { return err } logPath := fmt.Sprintf("%s/%s", config.LogDir, config.LogName) - writer := &lumberjack.Logger{ - Filename: logPath, - MaxSize: config.LogRotateMaxSize, - MaxBackups: config.LogRotateMaxBackups, - MaxAge: config.LogRotateMaxAge, - Compress: false, - } - // https://github.com/rs/zerolog/issues/77 zerolog.TimestampFunc = func() time.Time { return time.Now().UTC() } - zerolog.TimeFieldFormat = time.RFC3339Nano - - var writers io.Writer - writers = zerolog.MultiLevelWriter(writer) + zerolog.TimeFieldFormat = "2006-01-02T15:04:05.000000Z" - // ログレベル if config.Debug { zerolog.SetGlobalLevel(zerolog.DebugLevel) } else { zerolog.SetGlobalLevel(zerolog.InfoLevel) } - if config.LogStdOut { - consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "2006-01-02 15:04:05.000000Z"} - format(&consoleWriter) - writers = zerolog.MultiLevelWriter(writers, consoleWriter) - } + if config.Debug && config.LogStdout { + writer := zerolog.ConsoleWriter{ + Out: os.Stdout, + FormatTimestamp: func(i interface{}) string { + darkGray := "\x1b[90m" + reset := "\x1b[0m" + return strings.Join([]string{darkGray, i.(string), reset}, "") + }, + NoColor: false, + } + prettyFormat(&writer) + log.Logger = zerolog.New(writer).With().Caller().Timestamp().Logger() + } else if config.LogStdout { + writer := os.Stdout + log.Logger = zerolog.New(writer).With().Caller().Timestamp().Logger() + } else { + var logRotateMaxSize, logRotateMaxBackups, logRotateMaxAge int + if config.LogRotateMaxSize == 0 { + logRotateMaxSize = DefaultLogRotateMaxSize + } + if config.LogRotateMaxBackups == 0 { + logRotateMaxBackups = DefaultLogRotateMaxBackups + } + if config.LogRotateMaxAge == 0 { + logRotateMaxAge = DefaultLogRotateMaxAge + } - log.Logger = zerolog.New(writers).With().Caller().Timestamp().Logger() + writer := &lumberjack.Logger{ + Filename: logPath, + MaxSize: logRotateMaxSize, + MaxBackups: logRotateMaxBackups, + MaxAge: logRotateMaxAge, + Compress: false, + } + log.Logger = zerolog.New(writer).With().Caller().Timestamp().Logger() + } return nil } -func format(w *zerolog.ConsoleWriter) { +// 現時点での prettyFormat +// 2023-04-17 12:51:56.333485Z [INFO] config.go:102 > CONF | debug=true +func prettyFormat(w *zerolog.ConsoleWriter) { + const Reset = "\x1b[0m" + w.FormatLevel = func(i interface{}) string { - return strings.ToUpper(fmt.Sprintf("[%s]", i)) + var color, level string + // TODO: 各色を定数に置き換える + // TODO: 他の logLevel が必要な場合は追加する + switch i.(string) { + case "info": + color = "\x1b[32m" + case "error": + color = "\x1b[31m" + case "warn": + color = "\x1b[33m" + case "debug": + color = "\x1b[34m" + default: + color = "\x1b[37m" + } + + level = strings.ToUpper(i.(string)) + return fmt.Sprintf("%s[%s]%s", color, level, Reset) + } + w.FormatCaller = func(i interface{}) string { + return fmt.Sprintf("[%s]", filepath.Base(i.(string))) + } + // TODO: Caller をファイル名と行番号だけの表示で出力する + // 以下のようなフォーマットにしたい + // 2023-04-17 12:50:09.334758Z [INFO] [config.go:102] CONF | debug=true + // TODO: name=value が無い場合に | を消す方法がわからなかった + w.FormatMessage = func(i interface{}) string { + if i == nil { + return "" + } else { + return fmt.Sprintf("%s |", i) + } } w.FormatFieldName = func(i interface{}) string { - return fmt.Sprintf("%s=", i) + const Cyan = "\x1b[36m" + return fmt.Sprintf("%s%s=%s", Cyan, i, Reset) } + // TODO: カンマ区切りを同実現するかわからなかった w.FormatFieldValue = func(i interface{}) string { return fmt.Sprintf("%s", i) } diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 5474339..0000000 --- a/renovate.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ], - "dependencyDashboard": false, - "packageRules": [ - { - "matchUpdateTypes": [ - "minor", - "patch", - "pin", - "digest" - ], - "platformAutomerge": true, - "automerge": true - } - ] -} \ No newline at end of file diff --git a/runner.go b/runner.go index 9638734..74a7dda 100644 --- a/runner.go +++ b/runner.go @@ -12,10 +12,10 @@ import ( ) type Main struct { - config Config + config *Config } -func newMain(config Config) *Main { +func newMain(config *Config) *Main { return &Main{ config: config, } @@ -59,11 +59,11 @@ func (m *Main) run(ctx context.Context, cancel context.CancelFunc) error { } processContext, processContextCancel := context.WithCancel(context.Background()) - gateKeeper := newGateKeeper(&m.config) + gateKeeper := newGateKeeper(m.config) recordingFileStream := gateKeeper.run(processContext, foundFiles) uploaderManager := newUploaderManager() - _, err = uploaderManager.run(processContext, &m.config, recordingFileStream) + _, err = uploaderManager.run(processContext, m.config, recordingFileStream) if err != nil { processContextCancel() return err @@ -120,15 +120,9 @@ func (m *Main) run(ctx context.Context, cancel context.CancelFunc) error { } func Run(configFilePath *string) { - buf, err := os.ReadFile(*configFilePath) + // INI をパース + config, err := newConfig(*configFilePath) if err != nil { - // 読み込めない場合 Fatal で終了 - log.Fatal("cannot open config file, err=", err) - } - - // toml をパース - var config Config - if err := initConfig(buf, &config); err != nil { // パースに失敗した場合 Fatal で終了 log.Fatal("cannot parse config file, err=", err) } @@ -142,7 +136,7 @@ func Run(configFilePath *string) { // もしあれば mTLS の設定確認と Webhook のヘルスチェック if config.WebhookEndpointHealthCheckURL != "" { - client, err := createHttpClient(&config) + client, err := createHTTPClient(config) if err != nil { zlog.Fatal().Err(err).Msg("FAILED-CREATE-RPC-CLIENT") } @@ -157,6 +151,11 @@ func Run(configFilePath *string) { resp.Body.Close() } + // 指定は 0 (制限なし) または 10 Mbps 以上 + if (config.UploadFileRateLimitMbps > 0) && (config.UploadFileRateLimitMbps < 10) { + zlog.Fatal().Msg("UPLOAD-FILE-RATE-LIMIT-MBPS-ERROR") + } + zlog.Info().Msg("STARTED-SORA-ARCHIVE-UPLOADER") // シグナルをキャッチして停止処理 diff --git a/s3.go b/s3.go index cfa6e44..ff76b63 100644 --- a/s3.go +++ b/s3.go @@ -3,27 +3,24 @@ package archive import ( "context" "fmt" + "net" + "net/http" "net/url" "os" "path/filepath" + "time" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" - zlog "github.com/rs/zerolog/log" -) + "github.com/shiguredo/sora-archive-uploader/s3" -// config ではなくこちらにまとめる -type S3CompatibleObjectStorage struct { - Endpoint string - AccessKeyID string - SecretAccessKey string - BucketName string -} + "github.com/conduitio/bwlimit" +) func uploadJSONFile( ctx context.Context, - osConfig *S3CompatibleObjectStorage, + osConfig *s3.S3CompatibleObjectStorage, dst, filePath string, ) (string, error) { var creds *credentials.Credentials @@ -39,7 +36,7 @@ func uploadJSONFile( creds = credentials.NewIAM("") } - s3Client, err := newS3Client(osConfig.Endpoint, creds) + s3Client, err := s3.NewClient(osConfig.Endpoint, creds, nil) if err != nil { return "", err } @@ -66,11 +63,11 @@ func uploadJSONFile( fmt.Sprintf("attachment; filename=\"%s\"", filename), ) - objectUrl := fmt.Sprintf("s3://%s/%s", n.Bucket, n.Key) - return objectUrl, nil + objectURL := fmt.Sprintf("s3://%s/%s", n.Bucket, n.Key) + return objectURL, nil } -func uploadWebMFile(ctx context.Context, osConfig *S3CompatibleObjectStorage, dst, filePath string) (string, error) { +func uploadWebMFile(ctx context.Context, osConfig *s3.S3CompatibleObjectStorage, dst, filePath string) (string, error) { var creds *credentials.Credentials if (osConfig.AccessKeyID != "") || (osConfig.SecretAccessKey != "") { creds = credentials.NewStaticV4( @@ -83,7 +80,7 @@ func uploadWebMFile(ctx context.Context, osConfig *S3CompatibleObjectStorage, ds } else { creds = credentials.NewIAM("") } - s3Client, err := newS3Client(osConfig.Endpoint, creds) + s3Client, err := s3.NewClient(osConfig.Endpoint, creds, nil) if err != nil { return "", err } @@ -113,8 +110,8 @@ func uploadWebMFile(ctx context.Context, osConfig *S3CompatibleObjectStorage, ds fmt.Sprintf("attachment; filename=\"%s\"", filename), ) - objectUrl := fmt.Sprintf("s3://%s/%s", n.Bucket, n.Key) - return objectUrl, nil + objectURL := fmt.Sprintf("s3://%s/%s", n.Bucket, n.Key) + return objectURL, nil } // minio のエラーをレスポンスに復元して、リトライするためファイルを残すか対象のファイルを削除するか判断する @@ -131,39 +128,60 @@ func isFileContinuous(err error) bool { return true } -func maybeEndpointURL(endpoint string) (string, bool) { - // もし endpoint に指定されたのが endpoint_url だった場合、 - // scheme をチェックして http ならば secure = false にする - // さらに host だけを取り出して endpoint として扱う - var secure = false - u, err := url.Parse(endpoint) - // エラーがあっても無視してそのまま文字列として扱う - // エラーがないときだけ scheme チェックする - if err == nil { - switch u.Scheme { - case "http": - return u.Host, secure - case "https": - // https なので secure を true にする - secure = true - return u.Host, secure - case "": - // scheme なしの場合は secure を true にする - secure = true - return endpoint, secure - default: - // サポート外の scheme の場合はタダの文字列として扱う - } +func uploadWebMFileWithRateLimit(ctx context.Context, osConfig *s3.S3CompatibleObjectStorage, dst, filePath string, + rateLimitMpbs int) (string, error) { + var creds *credentials.Credentials + if (osConfig.AccessKeyID != "") || (osConfig.SecretAccessKey != "") { + creds = credentials.NewStaticV4( + osConfig.AccessKeyID, + osConfig.SecretAccessKey, + "", + ) + } else if (len(os.Getenv("AWS_ACCESS_KEY_ID")) > 0) && (len(os.Getenv("AWS_SECRET_ACCESS_KEY")) > 0) { + creds = credentials.NewEnvAWS() + } else { + creds = credentials.NewIAM("") + } + + // bit を byte にする + rateLimitMByteps := (bwlimit.Byte(rateLimitMpbs) * bwlimit.MiB) / 8 + + // 受信には制限をかけない + dialer := bwlimit.NewDialer(&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }, rateLimitMByteps, 0) + + transport := http.DefaultTransport + transport.(*http.Transport).DialContext = dialer.DialContext + + s3Client, err := s3.NewClient(osConfig.Endpoint, creds, &transport) + if err != nil { + return "", err + } + + fileReader, err := os.Open(filePath) + if err != nil { + return "", err + } + defer fileReader.Close() + + // Save the file stat. + fileStat, err := fileReader.Stat() + if err != nil { + return "", err + } + + // Save the file size. + fileSize := fileStat.Size() + + // 制限時にはマルチパートアップロードを行わない + n, err := s3Client.PutObject(ctx, osConfig.BucketName, dst, fileReader, fileSize, + minio.PutObjectOptions{ContentType: "application/octet-stream", DisableMultipart: true}) + if err != nil { + return "", err } - return endpoint, secure -} -func newS3Client(endpoint string, credentials *credentials.Credentials) (*minio.Client, error) { - newEndpoint, secure := maybeEndpointURL(endpoint) - return minio.New( - newEndpoint, - &minio.Options{ - Creds: credentials, - Secure: secure, - }) + objectURL := fmt.Sprintf("s3://%s/%s", n.Bucket, n.Key) + return objectURL, nil } diff --git a/s3/s3.go b/s3/s3.go new file mode 100644 index 0000000..593dcc3 --- /dev/null +++ b/s3/s3.go @@ -0,0 +1,63 @@ +package s3 + +import ( + "net/http" + "net/url" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +type S3CompatibleObjectStorage struct { + Endpoint string + AccessKeyID string + SecretAccessKey string + BucketName string +} + +func maybeEndpointURL(endpoint string) (string, bool) { + // もし endpoint に指定されたのが endpoint_url だった場合、 + // scheme をチェックして http ならば secure = false にする + // さらに host だけを取り出して endpoint として扱う + var secure = false + u, err := url.Parse(endpoint) + // エラーがあっても無視してそのまま文字列として扱う + // エラーがないときだけ scheme チェックする + if err == nil { + switch u.Scheme { + case "http": + return u.Host, secure + case "https": + // https なので secure を true にする + secure = true + return u.Host, secure + case "": + // scheme なしの場合は secure を true にする + secure = true + return endpoint, secure + default: + // サポート外の scheme の場合はタダの文字列として扱う + } + } + return endpoint, secure +} + +func NewClient(endpoint string, credentials *credentials.Credentials, transport *http.RoundTripper) (*minio.Client, error) { + newEndpoint, secure := maybeEndpointURL(endpoint) + if transport == nil { + return minio.New( + newEndpoint, + &minio.Options{ + Creds: credentials, + Secure: secure, + }) + } + + return minio.New( + newEndpoint, + &minio.Options{ + Creds: credentials, + Secure: secure, + Transport: *transport, + }) +} diff --git a/s3_test.go b/s3/s3_test.go similarity index 97% rename from s3_test.go rename to s3/s3_test.go index cd36b23..55e8d70 100644 --- a/s3_test.go +++ b/s3/s3_test.go @@ -1,4 +1,4 @@ -package archive +package s3 import ( "testing" diff --git a/sora-archive-uploader.service b/script/sora-archive-uploader.service similarity index 89% rename from sora-archive-uploader.service rename to script/sora-archive-uploader.service index 1dc1ada..01c4df8 100644 --- a/sora-archive-uploader.service +++ b/script/sora-archive-uploader.service @@ -15,7 +15,7 @@ WorkingDirectory=/home/sora/sora-archive-uploader ExecStartPre=/bin/mkdir -p /var/log/sora-archive-uploader ExecStartPre=/bin/chown -R sora:sora /var/log/sora-archive-uploader -ExecStart=/home/sora/sora-archive-uploader/bin/sora-archive-uploader-prod -C /home/sora/sora-archive-uploader/config.toml +ExecStart=/home/sora/sora-archive-uploader/bin/sora-archive-uploader-prod -C /home/sora/sora-archive-uploader/config.ini [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/sora-archive-uploader.timer b/script/sora-archive-uploader.timer similarity index 100% rename from sora-archive-uploader.timer rename to script/sora-archive-uploader.timer diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000..5f0a348 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1 @@ +checks = ["all", "-ST1000"] diff --git a/uploader.go b/uploader.go index 10636f6..a1be4f5 100644 --- a/uploader.go +++ b/uploader.go @@ -10,6 +10,7 @@ import ( "time" "github.com/google/uuid" + "github.com/shiguredo/sora-archive-uploader/s3" base32 "github.com/shogo82148/go-clockwork-base32" zlog "github.com/rs/zerolog/log" @@ -244,7 +245,7 @@ func (u Uploader) handleArchive(archiveJSONFilePath string, split bool) bool { // metadata ファイル (json) をアップロード metadataFilename := fileInfo.Name() metadataObjectKey := fmt.Sprintf("%s/%s", am.RecordingID, metadataFilename) - osConfig := &S3CompatibleObjectStorage{ + osConfig := &s3.S3CompatibleObjectStorage{ Endpoint: u.config.ObjectStorageEndpoint, BucketName: u.config.ObjectStorageBucketName, AccessKeyID: u.config.ObjectStorageAccessKeyID, @@ -291,7 +292,14 @@ func (u Uploader) handleArchive(archiveJSONFilePath string, split bool) bool { Msg("UPLOAD-METADATA-FILE-SUCCESSFULLY") webmObjectKey := fmt.Sprintf("%s/%s", am.RecordingID, webmFilename) - fileURL, err := uploadWebMFile(u.ctx, osConfig, webmObjectKey, webmFilepath) + + var fileURL string + if u.config.UploadFileRateLimitMbps == 0 { + fileURL, err = uploadWebMFile(u.ctx, osConfig, webmObjectKey, webmFilepath) + } else { + fileURL, err = uploadWebMFileWithRateLimit(u.ctx, osConfig, webmObjectKey, webmFilepath, u.config.UploadFileRateLimitMbps) + } + if err != nil { zlog.Error(). Err(err). @@ -405,7 +413,7 @@ func (u Uploader) handleReport(reportJSONFilePath string) bool { // report ファイル (json) をアップロード filename := fileInfo.Name() reportObjectKey := fmt.Sprintf("%s/%s", rr.RecordingID, filename) - osConfig := &S3CompatibleObjectStorage{ + osConfig := &s3.S3CompatibleObjectStorage{ Endpoint: u.config.ObjectStorageEndpoint, BucketName: u.config.ObjectStorageBucketName, AccessKeyID: u.config.ObjectStorageAccessKeyID, @@ -534,7 +542,7 @@ func (u Uploader) handleArchiveEnd(archiveEndJSONFilePath string) bool { // metadata ファイル (json) をアップロード filename := fileInfo.Name() objectKey := fmt.Sprintf("%s/%s", aem.RecordingID, filename) - osConfig := &S3CompatibleObjectStorage{ + osConfig := &s3.S3CompatibleObjectStorage{ Endpoint: u.config.ObjectStorageEndpoint, BucketName: u.config.ObjectStorageBucketName, AccessKeyID: u.config.ObjectStorageAccessKeyID, diff --git a/webhook.go b/webhook.go index 4601bca..d7108da 100644 --- a/webhook.go +++ b/webhook.go @@ -50,14 +50,14 @@ type WebhookArchiveEndUploaded struct { } // mTLS を組み込んだ http.Client を構築する -func createHttpClient(config *Config) (*http.Client, error) { +func createHTTPClient(config *Config) (*http.Client, error) { e, err := url.Parse(config.WebhookEndpointURL) if err != nil { return nil, err } // http または VerifyCacertPath 指定していない場合はそのまま投げる - if e.Scheme != "https" || config.WebhookTlsVerifyCacertPath == "" { + if e.Scheme != "https" || config.WebhookTLSVerifyCacertPath == "" { client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse @@ -68,7 +68,7 @@ func createHttpClient(config *Config) (*http.Client, error) { return client, nil } - CaCert, err := os.ReadFile(config.WebhookTlsVerifyCacertPath) + CaCert, err := os.ReadFile(config.WebhookTLSVerifyCacertPath) if err != nil { return nil, err } @@ -76,8 +76,8 @@ func createHttpClient(config *Config) (*http.Client, error) { caCertPool.AppendCertsFromPEM(CaCert) var certificates []tls.Certificate - if config.WebhookTlsFullchainPath != "" && config.WebhookTlsPrivkeyPath != "" { - pair, err := tls.LoadX509KeyPair(config.WebhookTlsFullchainPath, config.WebhookTlsPrivkeyPath) + if config.WebhookTLSFullchainPath != "" && config.WebhookTLSPrivkeyPath != "" { + pair, err := tls.LoadX509KeyPair(config.WebhookTLSFullchainPath, config.WebhookTLSPrivkeyPath) if err != nil { return nil, err } @@ -132,7 +132,7 @@ func (u Uploader) httpClientDo(client *http.Client, webhookType string, buf []by } func (u Uploader) postWebhook(webhookType string, buf []byte) error { - client, err := createHttpClient(u.config) + client, err := createHTTPClient(u.config) if err != nil { return err }