791cd4d6af
Build / build (push) Successful in 12m20s
Rebrand the project as Tinyforge to reflect its evolution from a Docker container watcher into a self-hosted mini CI/deployment platform. Rename covers: Go module path, Docker labels, DB/config filenames, JWT issuer, Dockerfile binary, docker-compose, CI workflows, frontend i18n, README with static sites docs, and all code comments.
109 lines
3.3 KiB
Go
109 lines
3.3 KiB
Go
package volume
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/alexei/tinyforge/internal/store"
|
|
)
|
|
|
|
// ResolveParams holds the parameters needed to resolve a volume's host path.
|
|
type ResolveParams struct {
|
|
BasePath string
|
|
ProjectName string
|
|
StageName string // required for instance and stage scopes
|
|
ImageTag string // required for instance scope
|
|
AllowedVolumePaths string // JSON array of allowed absolute paths (from settings)
|
|
}
|
|
|
|
// ResolvePath returns the absolute host path for a volume based on its scope.
|
|
// Returns an error for ephemeral volumes (no host path) or missing parameters.
|
|
func ResolvePath(vol store.Volume, params ResolveParams) (string, error) {
|
|
scope := vol.Scope
|
|
if scope == "" {
|
|
switch vol.Mode {
|
|
case "isolated":
|
|
scope = "instance"
|
|
default:
|
|
scope = "project"
|
|
}
|
|
}
|
|
|
|
if scope == "ephemeral" {
|
|
return "", fmt.Errorf("ephemeral volumes have no host path")
|
|
}
|
|
|
|
if scope == "absolute" {
|
|
return resolveAbsolute(vol.Source, params.AllowedVolumePaths)
|
|
}
|
|
|
|
switch scope {
|
|
case "instance":
|
|
if params.StageName == "" || params.ImageTag == "" {
|
|
return "", fmt.Errorf("instance scope requires stage and tag parameters")
|
|
}
|
|
return filepath.Join(params.BasePath, params.ProjectName, fmt.Sprintf("%s-%s", params.StageName, params.ImageTag), vol.Source), nil
|
|
case "stage":
|
|
if params.StageName == "" {
|
|
return "", fmt.Errorf("stage scope requires stage parameter")
|
|
}
|
|
return filepath.Join(params.BasePath, params.ProjectName, params.StageName, vol.Source), nil
|
|
case "project":
|
|
return filepath.Join(params.BasePath, params.ProjectName, vol.Source), nil
|
|
case "project_named":
|
|
return filepath.Join(params.BasePath, params.ProjectName, "_named", vol.Name, vol.Source), nil
|
|
case "named":
|
|
return filepath.Join(params.BasePath, "_named", vol.Name, vol.Source), nil
|
|
default:
|
|
return filepath.Join(params.BasePath, params.ProjectName, vol.Source), nil
|
|
}
|
|
}
|
|
|
|
// resolveAbsolute validates that the source path is under one of the allowed prefixes.
|
|
func resolveAbsolute(source, allowedPathsJSON string) (string, error) {
|
|
if source == "" {
|
|
return "", fmt.Errorf("absolute scope requires a source path")
|
|
}
|
|
|
|
cleaned := filepath.Clean(source)
|
|
if !filepath.IsAbs(cleaned) {
|
|
return "", fmt.Errorf("absolute scope requires an absolute source path (starting with /)")
|
|
}
|
|
|
|
allowed, err := parseAllowedPaths(allowedPathsJSON)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse allowed volume paths: %w", err)
|
|
}
|
|
if len(allowed) == 0 {
|
|
return "", fmt.Errorf("absolute volume paths are disabled (no allowed paths configured in settings)")
|
|
}
|
|
|
|
for _, prefix := range allowed {
|
|
prefixClean := filepath.Clean(prefix)
|
|
if strings.HasPrefix(cleaned, prefixClean+string(filepath.Separator)) || cleaned == prefixClean {
|
|
return cleaned, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("path %q is not under any allowed volume path", source)
|
|
}
|
|
|
|
// parseAllowedPaths parses a JSON array of path strings.
|
|
func parseAllowedPaths(jsonStr string) ([]string, error) {
|
|
if jsonStr == "" || jsonStr == "[]" {
|
|
return nil, nil
|
|
}
|
|
var paths []string
|
|
if err := json.Unmarshal([]byte(jsonStr), &paths); err != nil {
|
|
return nil, err
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
// ParseAllowedPaths is the exported version for use in API validation.
|
|
func ParseAllowedPaths(jsonStr string) ([]string, error) {
|
|
return parseAllowedPaths(jsonStr)
|
|
}
|