s
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Validate(name string) error {
|
||||
if name == "" {
|
||||
return errors.New("name cannot be empty")
|
||||
}
|
||||
if strings.HasPrefix(name, "/") || strings.Contains(name, "\\") {
|
||||
return errors.New("name must be a relative unix-style path")
|
||||
}
|
||||
if !strings.HasSuffix(name, ".git") {
|
||||
return errors.New("name must end with .git")
|
||||
}
|
||||
parts := strings.Split(name, "/")
|
||||
for _, part := range parts {
|
||||
if part == "" || part == "." || part == ".." {
|
||||
return errors.New("name contains an invalid path segment")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Resolve(root, name string) (string, error) {
|
||||
if err := Validate(name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
full, err := filepath.Abs(filepath.Join(root, name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
absRoot, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if full != absRoot && !strings.HasPrefix(full, absRoot+string(os.PathSeparator)) {
|
||||
return "", errors.New("repository path escapes root directory")
|
||||
}
|
||||
return full, nil
|
||||
}
|
||||
|
||||
// splitOwnerName splits a smart-HTTP/SSH repo path "owner/name.git" into its
|
||||
// IState (owner, name) form: both without the ".git" suffix. owner may itself
|
||||
// contain "/" for nested namespaces (e.g. "org/team/name.git" -> owner
|
||||
// "org/team", name "name"); name is always the final segment. IState
|
||||
// reconstructs the on-disk path / DB full-name as owner+"/"+name+".git".
|
||||
func SplitRepo(repo string) (owner, name string, err error) {
|
||||
s := strings.TrimSuffix(repo, ".git")
|
||||
i := strings.LastIndex(s, "/")
|
||||
if i < 0 {
|
||||
return "", "", errors.New("repository path must be owner/name.git")
|
||||
}
|
||||
return s[:i], s[i+1:], nil
|
||||
}
|
||||
|
||||
// parseDBURI maps a URI scheme to (driver, dsn), following simpleconsole's
|
||||
// ParseDBURI convention so the same URIs work across both services. Unexported:
|
||||
// Open is the only entry point callers need.
|
||||
//
|
||||
// sqlite://<path>[?_pragma=...] -> driver "sqlite3", modernc DSN "file:<path>?..."
|
||||
// postgres://<dsn-url> -> driver "postgres", dsn passed through verbatim
|
||||
func ParseDBURI(root, uri string) (driver, dsn string, uriNew string, err error) {
|
||||
if uri == "" {
|
||||
return "", "", "", fmt.Errorf("database URI is empty")
|
||||
}
|
||||
scheme, rest, ok := splitScheme(uri)
|
||||
if !ok {
|
||||
return "", "", "", fmt.Errorf("database URI %q has no scheme (want sqlite:// or postgres://)", uri)
|
||||
}
|
||||
switch scheme {
|
||||
case "sqlite", "sqlite3":
|
||||
path, query, _ := strings.Cut(strings.TrimPrefix(rest, "//"), "?")
|
||||
if path == "" {
|
||||
return "", "", "", fmt.Errorf("sqlite URI %q has no file path", uri)
|
||||
}
|
||||
uriNew := "sqlite3://" + filepath.Join(root, path)
|
||||
return "sqlite3", sqliteDSN(filepath.Join(root, path), query), uriNew, nil
|
||||
case "postgres", "postgresql":
|
||||
// lib/pq parses the URL form natively; pass it through unchanged.
|
||||
return "postgres", uri, uri, nil
|
||||
default:
|
||||
return "", "", "", fmt.Errorf("unsupported database URI scheme %q (want sqlite:// or postgres://)", scheme)
|
||||
}
|
||||
}
|
||||
|
||||
// sqliteDSN builds a modernc DSN: "file:<path>?<params>". Default pragmas (WAL,
|
||||
// busy_timeout, foreign_keys) and _txlock=immediate are applied only when the
|
||||
// caller supplied no pragmas of their own; an explicit pragma set is respected
|
||||
// verbatim.
|
||||
func sqliteDSN(path, query string) string {
|
||||
v, _ := url.ParseQuery(query)
|
||||
if len(v["_pragma"]) == 0 {
|
||||
v.Add("_pragma", "journal_mode(WAL)")
|
||||
v.Add("_pragma", "busy_timeout(5000)")
|
||||
v.Add("_pragma", "foreign_keys(ON)")
|
||||
}
|
||||
v.Add("_txlock", "immediate")
|
||||
return "file:" + path + "?" + v.Encode()
|
||||
}
|
||||
|
||||
// sqlitePath extracts the file path from a modernc DSN ("file:<path>?...").
|
||||
func sqlitePath(dsn string) string {
|
||||
s := strings.TrimPrefix(dsn, "file:")
|
||||
if i := strings.Index(s, "?"); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// splitScheme splits "scheme:rest" at the first colon. ok is false when there
|
||||
// is no colon.
|
||||
func splitScheme(uri string) (scheme, rest string, ok bool) {
|
||||
i := strings.Index(uri, ":")
|
||||
if i < 0 {
|
||||
return "", uri, false
|
||||
}
|
||||
return uri[:i], uri[i+1:], true
|
||||
}
|
||||
Reference in New Issue
Block a user