🏡 index : quine-global/quintodrome.git

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
}

// LocalPathToURL converts a bare OS filesystem path into an absolute file:// URL,
// applying the same slash-normalisation and per-component escaping the scanner
// relies on. It is the single source of truth for how a local path becomes a
// storage URL, shared by For and by storage tests.
func LocalPathToURL(osPath string) (url.URL, error) {
	abs, _ := filepath.Abs(osPath)
	abs = filepath.ToSlash(abs)

	// Properly escape each path component using URL standards
	pathParts := strings.Split(abs, "/")
	escapedParts := slice.Map(pathParts, func(s string) string {
		return url.PathEscape(s)
	})

	u, err := url.Parse(LocalSchemaID + "://" + strings.Join(escapedParts, "/"))
	if err != nil {
		return url.URL{}, err
	}
	return *u, nil
}

// 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, "://")

	var u *url.URL
	// Paths without schema are treated as file:// and use the default LocalStorage implementation
	if len(parts) < 2 {
		parsed, err := LocalPathToURL(uri)
		if err != nil {
			return nil, err
		}
		u = &parsed
	} else {
		parsed, err := url.Parse(uri)
		if err != nil {
			return nil, err
		}
		u = parsed
	}

	c, ok := registry[u.Scheme]
	if !ok {
		return nil, errors.New("schema '" + u.Scheme + "' not registered")
	}
	return c(*u), nil
}