From fcccf15c747943896c2780a6c4b3428fed9ef58f Mon Sep 17 00:00:00 2001 From: David Colburn Date: Tue, 2 Jun 2026 16:23:01 -0400 Subject: [PATCH 1/6] use urls directly for azure --- azure.go | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/azure.go b/azure.go index c3ad2a3..dc63dc7 100644 --- a/azure.go +++ b/azure.go @@ -49,22 +49,18 @@ func NewAzure(conf *AzureConfig) (Storage, error) { }, }) - sUrl := fmt.Sprintf("https://%s.blob.core.windows.net", conf.AccountName) - serviceUrl, err := url.Parse(sUrl) - if err != nil { - return nil, err - } - - cUrl := path.Join(sUrl, conf.ContainerName) - containerUrl, err := url.Parse(cUrl) - if err != nil { - return nil, err - } - + host := fmt.Sprintf("%s.blob.core.windows.net", conf.AccountName) return &azureBLOBStorage{ - conf: conf, - serviceUrl: azblob.NewServiceURL(*serviceUrl, pipeline), - containerUrl: azblob.NewContainerURL(*containerUrl, pipeline), + conf: conf, + serviceUrl: azblob.NewServiceURL(url.URL{ + Scheme: "https", + Host: host, + }, pipeline), + containerUrl: azblob.NewContainerURL(url.URL{ + Scheme: "https", + Host: host, + Path: conf.ContainerName, + }, pipeline), }, nil } From 188bec07646526677e57c15699b28c3488bdb380 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Tue, 2 Jun 2026 16:38:11 -0400 Subject: [PATCH 2/6] add unit tests --- azure.go | 18 ++- gcp.go | 11 +- local.go | 8 +- location_test.go | 338 +++++++++++++++++++++++++++++++++++++++++++++++ s3.go | 7 +- 5 files changed, 369 insertions(+), 13 deletions(-) create mode 100644 location_test.go diff --git a/azure.go b/azure.go index dc63dc7..027f885 100644 --- a/azure.go +++ b/azure.go @@ -64,12 +64,12 @@ func NewAzure(conf *AzureConfig) (Storage, error) { }, nil } -func (s *azureBLOBStorage) location(storagePath string) *url.URL { - return &url.URL{ +func (s *azureBLOBStorage) location(storagePath string) string { + return (&url.URL{ Scheme: "https", Host: s.conf.AccountName + ".blob.core.windows.net", Path: path.Join(s.conf.ContainerName, storagePath), - } + }).String() } func (s *azureBLOBStorage) UploadData(data []byte, storagePath, contentType string) (string, int64, error) { @@ -83,7 +83,7 @@ func (s *azureBLOBStorage) UploadData(data []byte, storagePath, contentType stri return "", 0, err } - return s.location(storagePath).String(), int64(len(data)), nil + return s.location(storagePath), int64(len(data)), nil } func (s *azureBLOBStorage) UploadFile(filepath, storagePath, contentType string) (string, int64, error) { @@ -112,7 +112,7 @@ func (s *azureBLOBStorage) UploadFile(filepath, storagePath, contentType string) return "", 0, err } - return s.location(storagePath).String(), stat.Size(), nil + return s.location(storagePath), stat.Size(), nil } func (s *azureBLOBStorage) ListObjects(prefix string) ([]string, error) { @@ -212,8 +212,12 @@ func (s *azureBLOBStorage) GeneratePresignedUrl(storagePath string, expiration t return "", err } - loc := s.location(storagePath) - loc.RawQuery = qp.Encode() + loc := &url.URL{ + Scheme: "https", + Host: s.conf.AccountName + ".blob.core.windows.net", + Path: path.Join(s.conf.ContainerName, storagePath), + RawQuery: qp.Encode(), + } return loc.String(), nil } diff --git a/gcp.go b/gcp.go index 877152f..b417648 100644 --- a/gcp.go +++ b/gcp.go @@ -118,8 +118,15 @@ func (s *gcpStorage) upload(reader io.Reader, storagePath, contentType string) ( return "", 0, err } - loc := url.URL{Scheme: "https", Host: s.conf.Bucket + ".storage.googleapis.com", Path: storagePath} - return loc.String(), n, nil + return s.location(storagePath), n, nil +} + +func (s *gcpStorage) location(storagePath string) string { + return (&url.URL{ + Scheme: "https", + Host: s.conf.Bucket + ".storage.googleapis.com", + Path: storagePath, + }).String() } func (s *gcpStorage) ListObjects(prefix string) ([]string, error) { diff --git a/local.go b/local.go index 53d5dd5..03677fa 100644 --- a/local.go +++ b/local.go @@ -40,7 +40,7 @@ func NewLocal(conf *LocalConfig) (Storage, error) { } func (u *localUploader) UploadFile(localPath, storagePath string, _ string) (string, int64, error) { - storagePath = path.Join(u.StorageDir, storagePath) + storagePath = u.location(storagePath) local, err := os.Open(localPath) if err != nil { @@ -69,7 +69,7 @@ func (u *localUploader) UploadFile(localPath, storagePath string, _ string) (str } func (u *localUploader) UploadData(data []byte, storagePath, _ string) (string, int64, error) { - storagePath = path.Join(u.StorageDir, storagePath) + storagePath = u.location(storagePath) if dir, _ := path.Split(storagePath); dir != "" { if err := os.MkdirAll(dir, 0755); err != nil { @@ -91,6 +91,10 @@ func (u *localUploader) UploadData(data []byte, storagePath, _ string) (string, return storagePath, int64(size), nil } +func (u *localUploader) location(storagePath string) string { + return path.Join(u.StorageDir, storagePath) +} + func (u *localUploader) ListObjects(prefix string) ([]string, error) { absPrefix := path.Join(u.StorageDir, prefix) dir, filenamePrefix := path.Split(absPrefix) diff --git a/location_test.go b/location_test.go new file mode 100644 index 0000000..43caed1 --- /dev/null +++ b/location_test.go @@ -0,0 +1,338 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAliOSSLocation(t *testing.T) { + cases := []struct { + name string + conf AliOSSConfig + storagePath string + want string + }{ + { + name: "normal", + conf: AliOSSConfig{Bucket: "mybucket", Endpoint: "oss-region.aliyuncs.com"}, + storagePath: "foo.mp4", + want: "https://mybucket.oss-region.aliyuncs.com/foo.mp4", + }, + { + name: "endpoint with https:// prefix", + conf: AliOSSConfig{Bucket: "mybucket", Endpoint: "https://oss-region.aliyuncs.com"}, + storagePath: "foo.mp4", + want: "https://mybucket.oss-region.aliyuncs.com/foo.mp4", + }, + { + name: "endpoint with http:// prefix", + conf: AliOSSConfig{Bucket: "mybucket", Endpoint: "http://oss-region.aliyuncs.com"}, + storagePath: "foo.mp4", + want: "https://mybucket.oss-region.aliyuncs.com/foo.mp4", + }, + { + name: "nested path", + conf: AliOSSConfig{Bucket: "mybucket", Endpoint: "oss-region.aliyuncs.com"}, + storagePath: "folder/sub/foo.mp4", + want: "https://mybucket.oss-region.aliyuncs.com/folder/sub/foo.mp4", + }, + { + name: "storagePath with leading slash", + conf: AliOSSConfig{Bucket: "mybucket", Endpoint: "oss-region.aliyuncs.com"}, + storagePath: "/foo.mp4", + want: "https://mybucket.oss-region.aliyuncs.com/foo.mp4", + }, + { + name: "space in key", + conf: AliOSSConfig{Bucket: "mybucket", Endpoint: "oss-region.aliyuncs.com"}, + storagePath: "folder/my file.mp4", + want: "https://mybucket.oss-region.aliyuncs.com/folder/my%20file.mp4", + }, + { + name: "unicode in key", + conf: AliOSSConfig{Bucket: "mybucket", Endpoint: "oss-region.aliyuncs.com"}, + storagePath: "café/résumé.mp4", + want: "https://mybucket.oss-region.aliyuncs.com/caf%C3%A9/r%C3%A9sum%C3%A9.mp4", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s := &aliOSSStorage{conf: &tc.conf} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got) + requireWellFormedHTTPLocation(t, got) + }) + } +} + +func TestAzureLocation(t *testing.T) { + cases := []struct { + name string + conf AzureConfig + storagePath string + want string + }{ + { + name: "normal", + conf: AzureConfig{AccountName: "acct", ContainerName: "mycontainer"}, + storagePath: "foo.mp4", + want: "https://acct.blob.core.windows.net/mycontainer/foo.mp4", + }, + { + name: "empty container (customer regression)", + conf: AzureConfig{AccountName: "acct", ContainerName: ""}, + storagePath: "recordings/production/video_call_recordings/6893038.ogg", + want: "https://acct.blob.core.windows.net/recordings/production/video_call_recordings/6893038.ogg", + }, + { + name: "container with leading slash", + conf: AzureConfig{AccountName: "acct", ContainerName: "/mycontainer"}, + storagePath: "foo.mp4", + want: "https://acct.blob.core.windows.net/mycontainer/foo.mp4", + }, + { + name: "container with trailing slash", + conf: AzureConfig{AccountName: "acct", ContainerName: "mycontainer/"}, + storagePath: "foo.mp4", + want: "https://acct.blob.core.windows.net/mycontainer/foo.mp4", + }, + { + name: "storagePath with leading slash", + conf: AzureConfig{AccountName: "acct", ContainerName: "mycontainer"}, + storagePath: "/foo.mp4", + want: "https://acct.blob.core.windows.net/mycontainer/foo.mp4", + }, + { + name: "nested path", + conf: AzureConfig{AccountName: "acct", ContainerName: "mycontainer"}, + storagePath: "a/b/c.mp4", + want: "https://acct.blob.core.windows.net/mycontainer/a/b/c.mp4", + }, + { + name: "space in key", + conf: AzureConfig{AccountName: "acct", ContainerName: "mycontainer"}, + storagePath: "my file.mp4", + want: "https://acct.blob.core.windows.net/mycontainer/my%20file.mp4", + }, + { + name: "unicode in key", + conf: AzureConfig{AccountName: "acct", ContainerName: "mycontainer"}, + storagePath: "café/résumé.mp4", + want: "https://acct.blob.core.windows.net/mycontainer/caf%C3%A9/r%C3%A9sum%C3%A9.mp4", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s := &azureBLOBStorage{conf: &tc.conf} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got) + requireWellFormedHTTPLocation(t, got) + }) + } +} + +func TestGCPLocation(t *testing.T) { + cases := []struct { + name string + conf GCPConfig + storagePath string + want string + }{ + { + name: "normal", + conf: GCPConfig{Bucket: "mybucket"}, + storagePath: "foo.mp4", + want: "https://mybucket.storage.googleapis.com/foo.mp4", + }, + { + name: "nested path", + conf: GCPConfig{Bucket: "mybucket"}, + storagePath: "a/b/c.mp4", + want: "https://mybucket.storage.googleapis.com/a/b/c.mp4", + }, + { + name: "storagePath with leading slash", + conf: GCPConfig{Bucket: "mybucket"}, + storagePath: "/foo.mp4", + want: "https://mybucket.storage.googleapis.com/foo.mp4", + }, + { + name: "space in key", + conf: GCPConfig{Bucket: "mybucket"}, + storagePath: "folder/my file.mp4", + want: "https://mybucket.storage.googleapis.com/folder/my%20file.mp4", + }, + { + name: "unicode in key", + conf: GCPConfig{Bucket: "mybucket"}, + storagePath: "café/résumé.mp4", + want: "https://mybucket.storage.googleapis.com/caf%C3%A9/r%C3%A9sum%C3%A9.mp4", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s := &gcpStorage{conf: &tc.conf} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got) + requireWellFormedHTTPLocation(t, got) + }) + } +} + +func TestS3Location(t *testing.T) { + cases := []struct { + name string + conf S3Config + storagePath string + want string + }{ + { + name: "vhost default endpoint", + conf: S3Config{Bucket: "mybucket"}, + storagePath: "foo.mp4", + want: "https://mybucket.s3.amazonaws.com/foo.mp4", + }, + { + name: "vhost custom endpoint without scheme", + conf: S3Config{Bucket: "mybucket", Endpoint: "s3.example.com"}, + storagePath: "foo.mp4", + want: "https://mybucket.s3.example.com/foo.mp4", + }, + { + name: "vhost custom endpoint with https://", + conf: S3Config{Bucket: "mybucket", Endpoint: "https://s3.example.com"}, + storagePath: "foo.mp4", + want: "https://mybucket.s3.example.com/foo.mp4", + }, + { + name: "vhost custom endpoint with http://", + conf: S3Config{Bucket: "mybucket", Endpoint: "http://s3.example.com"}, + storagePath: "foo.mp4", + want: "https://mybucket.s3.example.com/foo.mp4", + }, + { + name: "vhost nested path", + conf: S3Config{Bucket: "mybucket"}, + storagePath: "a/b/c.mp4", + want: "https://mybucket.s3.amazonaws.com/a/b/c.mp4", + }, + { + name: "vhost storagePath with leading slash", + conf: S3Config{Bucket: "mybucket"}, + storagePath: "/foo.mp4", + want: "https://mybucket.s3.amazonaws.com/foo.mp4", + }, + { + name: "vhost space in key", + conf: S3Config{Bucket: "mybucket"}, + storagePath: "folder/my file.mp4", + want: "https://mybucket.s3.amazonaws.com/folder/my%20file.mp4", + }, + { + name: "vhost unicode in key", + conf: S3Config{Bucket: "mybucket"}, + storagePath: "café/résumé.mp4", + want: "https://mybucket.s3.amazonaws.com/caf%C3%A9/r%C3%A9sum%C3%A9.mp4", + }, + { + name: "force path style default endpoint", + conf: S3Config{Bucket: "mybucket", ForcePathStyle: true}, + storagePath: "foo.mp4", + want: "https://s3.amazonaws.com/mybucket/foo.mp4", + }, + { + name: "force path style custom endpoint", + conf: S3Config{Bucket: "mybucket", Endpoint: "https://minio.example.com", ForcePathStyle: true}, + storagePath: "foo.mp4", + want: "https://minio.example.com/mybucket/foo.mp4", + }, + { + name: "force path style nested", + conf: S3Config{Bucket: "mybucket", ForcePathStyle: true}, + storagePath: "a/b/c.mp4", + want: "https://s3.amazonaws.com/mybucket/a/b/c.mp4", + }, + { + name: "force path style storagePath with leading slash", + conf: S3Config{Bucket: "mybucket", ForcePathStyle: true}, + storagePath: "/foo.mp4", + want: "https://s3.amazonaws.com/mybucket/foo.mp4", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + s := &s3Storage{conf: &tc.conf} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got) + requireWellFormedHTTPLocation(t, got) + }) + } +} + +func TestLocalLocation(t *testing.T) { + cases := []struct { + name string + storageDir string + storagePath string + want string + }{ + { + name: "normal", + storageDir: "/var/storage", + storagePath: "foo.mp4", + want: "/var/storage/foo.mp4", + }, + { + name: "nested path", + storageDir: "/var/storage", + storagePath: "a/b/c.mp4", + want: "/var/storage/a/b/c.mp4", + }, + { + name: "storagePath with leading slash", + storageDir: "/var/storage", + storagePath: "/foo.mp4", + want: "/var/storage/foo.mp4", + }, + { + name: "storageDir with trailing slash", + storageDir: "/var/storage/", + storagePath: "foo.mp4", + want: "/var/storage/foo.mp4", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + u := &localUploader{StorageDir: tc.storageDir} + require.Equal(t, tc.want, u.location(tc.storagePath)) + }) + } +} + +// requireWellFormedHTTPLocation verifies an https:// location URL is parseable, +// uses the https scheme, has a non-empty host, and contains no accidental +// double-slashes in its path (the original Azure bug). +func requireWellFormedHTTPLocation(t *testing.T, raw string) { + t.Helper() + u, err := url.Parse(raw) + require.NoError(t, err, "url should parse: %q", raw) + require.Equal(t, "https", u.Scheme, "expected https scheme: %q", raw) + require.NotEmpty(t, u.Host, "host should not be empty: %q", raw) + require.NotContains(t, u.Path, "//", "url path should not contain //: %q", raw) +} diff --git a/s3.go b/s3.go index 10caefd..b5caf30 100644 --- a/s3.go +++ b/s3.go @@ -260,6 +260,10 @@ func (s *s3Storage) upload(reader io.Reader, storagePath, contentType string) (s return "", err } + return s.location(storagePath), nil +} + +func (s *s3Storage) location(storagePath string) string { endpoint := "s3.amazonaws.com" if s.conf.Endpoint != "" { endpoint = s.conf.Endpoint @@ -275,8 +279,7 @@ func (s *s3Storage) upload(reader io.Reader, storagePath, contentType string) (s loc.Host = s.conf.Bucket + "." + endpoint loc.Path = storagePath } - - return loc.String(), nil + return loc.String() } func (s *s3Storage) ListObjects(prefix string) ([]string, error) { From 480a1b44513dbe35de5389719e8e08a4699a4338 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Tue, 2 Jun 2026 16:44:59 -0400 Subject: [PATCH 3/6] add test workflow --- .github/workflows/test.yaml | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..e715a8a --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,43 @@ +name: Test + +permissions: + contents: read + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: | + ~/go/pkg/mod + ~/go/bin + ~/.cache + key: livekit-storage + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: "go.mod" + + - name: Set up gotestfmt + run: go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@9eae5abc81d6d08f73268741ef4a8b97f29b60d8 # v2.4.1 + + - name: Download Go modules + run: go mod download + + - name: Vet + run: go vet ./... + + - name: Test + run: | + set -euo pipefail + go test -race -json -v ./... 2>&1 | gotestfmt From c069b33568364b08743eb1f92f64fa1d566d3e28 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Tue, 2 Jun 2026 16:51:43 -0400 Subject: [PATCH 4/6] vet -> lint --- .github/workflows/test.yaml | 6 ++-- location_test.go | 56 +++++++++++++++---------------------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e715a8a..6434cc3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -34,8 +34,10 @@ jobs: - name: Download Go modules run: go mod download - - name: Vet - run: go vet ./... + - name: Lint + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 + with: + version: v2.11.4 - name: Test run: | diff --git a/location_test.go b/location_test.go index 43caed1..b390b79 100644 --- a/location_test.go +++ b/location_test.go @@ -72,12 +72,10 @@ func TestAliOSSLocation(t *testing.T) { }, } for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - s := &aliOSSStorage{conf: &tc.conf} - got := s.location(tc.storagePath) - require.Equal(t, tc.want, got) - requireWellFormedHTTPLocation(t, got) - }) + s := &aliOSSStorage{conf: &tc.conf} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got, tc.name) + requireWellFormedHTTPLocation(t, got, tc.name) } } @@ -138,12 +136,10 @@ func TestAzureLocation(t *testing.T) { }, } for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - s := &azureBLOBStorage{conf: &tc.conf} - got := s.location(tc.storagePath) - require.Equal(t, tc.want, got) - requireWellFormedHTTPLocation(t, got) - }) + s := &azureBLOBStorage{conf: &tc.conf} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got, tc.name) + requireWellFormedHTTPLocation(t, got, tc.name) } } @@ -186,12 +182,10 @@ func TestGCPLocation(t *testing.T) { }, } for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - s := &gcpStorage{conf: &tc.conf} - got := s.location(tc.storagePath) - require.Equal(t, tc.want, got) - requireWellFormedHTTPLocation(t, got) - }) + s := &gcpStorage{conf: &tc.conf} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got, tc.name) + requireWellFormedHTTPLocation(t, got, tc.name) } } @@ -276,12 +270,10 @@ func TestS3Location(t *testing.T) { }, } for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - s := &s3Storage{conf: &tc.conf} - got := s.location(tc.storagePath) - require.Equal(t, tc.want, got) - requireWellFormedHTTPLocation(t, got) - }) + s := &s3Storage{conf: &tc.conf} + got := s.location(tc.storagePath) + require.Equal(t, tc.want, got, tc.name) + requireWellFormedHTTPLocation(t, got, tc.name) } } @@ -318,21 +310,19 @@ func TestLocalLocation(t *testing.T) { }, } for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - u := &localUploader{StorageDir: tc.storageDir} - require.Equal(t, tc.want, u.location(tc.storagePath)) - }) + u := &localUploader{StorageDir: tc.storageDir} + require.Equal(t, tc.want, u.location(tc.storagePath)) } } // requireWellFormedHTTPLocation verifies an https:// location URL is parseable, // uses the https scheme, has a non-empty host, and contains no accidental // double-slashes in its path (the original Azure bug). -func requireWellFormedHTTPLocation(t *testing.T, raw string) { +func requireWellFormedHTTPLocation(t *testing.T, raw string, testName string) { t.Helper() u, err := url.Parse(raw) - require.NoError(t, err, "url should parse: %q", raw) - require.Equal(t, "https", u.Scheme, "expected https scheme: %q", raw) - require.NotEmpty(t, u.Host, "host should not be empty: %q", raw) - require.NotContains(t, u.Path, "//", "url path should not contain //: %q", raw) + require.NoError(t, err, "%s: url should parse: %q", testName, raw) + require.Equal(t, "https", u.Scheme, "%s: expected https scheme: %q", testName, raw) + require.NotEmpty(t, u.Host, "%s: host should not be empty: %q", testName, raw) + require.NotContains(t, u.Path, "//", "%s: url path should not contain //: %q", testName, raw) } From 3fd869adb3df21a7cbeca39753bfff527b878722 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Tue, 2 Jun 2026 17:18:56 -0400 Subject: [PATCH 5/6] add lint config --- .golangci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8fc7a14 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,16 @@ +version: "2" +linters: + default: none + enable: + - staticcheck + settings: + staticcheck: + checks: + - "all" + - "-ST1000" + - "-ST1003" + - "-ST1020" + - "-ST1021" + - "-ST1022" + - "-SA1019" + - "-QF1008" From 147f04910d941d6f9bc09f749991a609a4417af4 Mon Sep 17 00:00:00 2001 From: David Colburn Date: Tue, 2 Jun 2026 17:21:46 -0400 Subject: [PATCH 6/6] lint fix --- storage_test.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/storage_test.go b/storage_test.go index eab581e..8f538d7 100644 --- a/storage_test.go +++ b/storage_test.go @@ -208,12 +208,8 @@ func testStorage(t *testing.T, s storage.Storage) { require.NoError(t, err) require.Len(t, items, 2) - var keys []string - for _, item := range items { - keys = append(keys, item) - } - require.True(t, hasSuffixIn(keys, pathData), "expected %s in %v", pathData, keys) - require.True(t, hasSuffixIn(keys, pathFile), "expected %s in %v", pathFile, keys) + require.True(t, hasSuffixIn(items, pathData), "expected %s in %v", pathData, items) + require.True(t, hasSuffixIn(items, pathFile), "expected %s in %v", pathFile, items) }) t.Run("DownloadData", func(t *testing.T) {