Files
navidrome/core/storage/storage.go
Deluan Quintão 0da2352907 fix: improve URL path handling in local storage for special characters (#4378)
* refactor: improve URL path handling in local storage system

Enhanced the local storage implementation to properly handle URL-decoded paths
and fix issues with file paths containing special characters. Added decodedPath
field to localStorage struct to separate URL parsing concerns from file system
operations.

Key changes:
- Added decodedPath field to localStorage struct for proper URL decoding
- Modified newLocalStorage to use decoded path instead of modifying original URL
- Fixed Windows path handling to work with decoded paths
- Improved URL escaping in storage.For() to handle special characters
- Added comprehensive test suite covering URL decoding, symlink resolution,
  Windows paths, and edge cases
- Refactored test extractor to use mockTestExtractor for better isolation

This ensures that file paths with spaces, hash symbols, and other special
characters are handled correctly throughout the storage system.

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(tests): fix test file permissions and add missing tests.Init call

* refactor(tests): remove redundant test

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: URL building for Windows and remove redundant variable

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor: simplify URL path escaping in local storage

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-07-23 20:46:47 -04:00

61 lines
1.3 KiB
Go

package storage
import (
"errors"
"net/url"
"path/filepath"
"strings"
"sync"
"github.com/navidrome/navidrome/utils/slice"
)
const LocalSchemaID = "file"
type constructor func(url.URL) Storage
var (
registry = map[string]constructor{}
lock sync.RWMutex
)
func Register(schema string, c constructor) {
lock.Lock()
defer lock.Unlock()
registry[schema] = c
}
// For returns a Storage implementation for the given URI.
// It uses the schema part of the URI to find the correct registered
// Storage constructor.
// If the URI does not contain a schema, it is treated as a file:// URI.
func For(uri string) (Storage, error) {
lock.RLock()
defer lock.RUnlock()
parts := strings.Split(uri, "://")
// Paths without schema are treated as file:// and use the default LocalStorage implementation
if len(parts) < 2 {
uri, _ = filepath.Abs(uri)
uri = filepath.ToSlash(uri)
// Properly escape each path component using URL standards
pathParts := strings.Split(uri, "/")
escapedParts := slice.Map(pathParts, func(s string) string {
return url.PathEscape(s)
})
uri = LocalSchemaID + "://" + strings.Join(escapedParts, "/")
}
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
c, ok := registry[u.Scheme]
if !ok {
return nil, errors.New("schema '" + u.Scheme + "' not registered")
}
return c(*u), nil
}