Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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: Lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.11.4

- name: Test
run: |
set -euo pipefail
go test -race -json -v ./... 2>&1 | gotestfmt
16 changes: 16 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: "2"
linters:
default: none
enable:
- staticcheck
settings:
staticcheck:
checks:
- "all"
- "-ST1000"
- "-ST1003"
- "-ST1020"
- "-ST1021"
- "-ST1022"
- "-SA1019"
- "-QF1008"
44 changes: 22 additions & 22 deletions azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,27 @@ 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
}

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) {
Expand All @@ -87,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) {
Expand Down Expand Up @@ -116,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) {
Expand Down Expand Up @@ -216,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
}

Expand Down
98 changes: 98 additions & 0 deletions location_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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 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 {
s := &azureBLOBStorage{conf: &tc.conf}
got := s.location(tc.storagePath)
require.Equal(t, tc.want, got, tc.name)
requireWellFormedHTTPLocation(t, got, tc.name)
}
}

// 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, testName string) {
t.Helper()
u, err := url.Parse(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)
}
8 changes: 2 additions & 6 deletions storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading