Add destination link metadata
This commit is contained in:
@@ -78,6 +78,9 @@ func Execute(ctx context.Context, req Request, plan Plan) error {
|
||||
Source: state.SourceState{Manifest: req.SourceBundle.Manifest},
|
||||
Outputs: stateOutputs(plan.Outputs),
|
||||
}
|
||||
if plan.PrimaryURL != "" {
|
||||
destinationState.Links = &state.LinkState{PrimaryURL: plan.PrimaryURL}
|
||||
}
|
||||
if err := state.Validate(destinationState); err != nil {
|
||||
cleanup()
|
||||
return err
|
||||
|
||||
116
internal/publish/links.go
Normal file
116
internal/publish/links.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package publish
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/config"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/state"
|
||||
)
|
||||
|
||||
func PlanLinks(req Request, outputs []Output) ([]Output, string, error) {
|
||||
if req.Links == nil {
|
||||
return outputs, "", nil
|
||||
}
|
||||
linked := make([]Output, 0, len(outputs))
|
||||
for _, output := range outputs {
|
||||
outputURL, err := OutputURL(req.Links.BaseURL, req.DestinationBundlePath, output.DestinationPath)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
output.URL = outputURL
|
||||
linked = append(linked, output)
|
||||
}
|
||||
return linked, primaryURL(linked, req.Links.Primary), nil
|
||||
}
|
||||
|
||||
func OutputURL(baseURL, destinationBundlePath, outputPath string) (string, error) {
|
||||
parsed, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("link base URL: %w", err)
|
||||
}
|
||||
segments := pathSegments(parsed.Path)
|
||||
segments = append(segments, pathSegments(destinationBundlePath)...)
|
||||
outputSegments := pathSegments(outputPath)
|
||||
trailingSlash := false
|
||||
if len(outputSegments) > 0 && outputSegments[len(outputSegments)-1] == "index.html" {
|
||||
outputSegments = outputSegments[:len(outputSegments)-1]
|
||||
trailingSlash = true
|
||||
}
|
||||
segments = append(segments, outputSegments...)
|
||||
parsed.Path = urlPath(segments, trailingSlash)
|
||||
parsed.RawPath = ""
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
func pathSegments(value string) []string {
|
||||
trimmed := strings.Trim(value, "/")
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(trimmed, "/")
|
||||
}
|
||||
|
||||
func urlPath(segments []string, trailingSlash bool) string {
|
||||
if len(segments) == 0 {
|
||||
return "/"
|
||||
}
|
||||
path := "/" + strings.Join(segments, "/")
|
||||
if trailingSlash && !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func primaryURL(outputs []Output, policy string) string {
|
||||
switch policy {
|
||||
case config.LinkPrimaryHTML:
|
||||
return firstGeneratedHTMLURL(outputs)
|
||||
case config.LinkPrimarySource:
|
||||
return firstSourceURL(outputs)
|
||||
default:
|
||||
if url := firstIndexURL(outputs); url != "" {
|
||||
return url
|
||||
}
|
||||
if url := firstGeneratedHTMLURL(outputs); url != "" {
|
||||
return url
|
||||
}
|
||||
return firstSourceURL(outputs)
|
||||
}
|
||||
}
|
||||
|
||||
func firstIndexURL(outputs []Output) string {
|
||||
for _, output := range outputs {
|
||||
if isIndexOutput(output.DestinationPath) {
|
||||
return output.URL
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstGeneratedHTMLURL(outputs []Output) string {
|
||||
for _, output := range outputs {
|
||||
if output.Kind == state.OutputKindGenerated && isHTMLOutput(output.DestinationPath) {
|
||||
return output.URL
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstSourceURL(outputs []Output) string {
|
||||
for _, output := range outputs {
|
||||
if output.Kind == state.OutputKindSource {
|
||||
return output.URL
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isIndexOutput(path string) bool {
|
||||
return path == "index.html" || strings.HasSuffix(path, "/index.html")
|
||||
}
|
||||
|
||||
func isHTMLOutput(path string) bool {
|
||||
return strings.HasSuffix(path, ".html")
|
||||
}
|
||||
149
internal/publish/links_test.go
Normal file
149
internal/publish/links_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package publish
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/config"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/state"
|
||||
)
|
||||
|
||||
func TestOutputURLUsesURLPathSemantics(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseURL string
|
||||
destinationBundlePath string
|
||||
outputPath string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nested non index",
|
||||
baseURL: "https://reports.example.com/archive",
|
||||
destinationBundlePath: "daily/brentwood",
|
||||
outputPath: "report.html",
|
||||
want: "https://reports.example.com/archive/daily/brentwood/report.html",
|
||||
},
|
||||
{
|
||||
name: "nested index",
|
||||
baseURL: "https://reports.example.com/archive",
|
||||
destinationBundlePath: "daily/brentwood",
|
||||
outputPath: "index.html",
|
||||
want: "https://reports.example.com/archive/daily/brentwood/",
|
||||
},
|
||||
{
|
||||
name: "fixed index",
|
||||
baseURL: "https://reports.example.com/latest",
|
||||
destinationBundlePath: "",
|
||||
outputPath: "index.html",
|
||||
want: "https://reports.example.com/latest/",
|
||||
},
|
||||
{
|
||||
name: "escaped segments",
|
||||
baseURL: "https://reports.example.com/archive root",
|
||||
destinationBundlePath: "daily reports",
|
||||
outputPath: "morning report.html",
|
||||
want: "https://reports.example.com/archive%20root/daily%20reports/morning%20report.html",
|
||||
},
|
||||
{
|
||||
name: "nested output index",
|
||||
baseURL: "https://reports.example.com",
|
||||
destinationBundlePath: "daily",
|
||||
outputPath: "site/index.html",
|
||||
want: "https://reports.example.com/daily/site/",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := OutputURL(tt.baseURL, tt.destinationBundlePath, tt.outputPath)
|
||||
if err != nil {
|
||||
t.Fatalf("OutputURL() error = %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("OutputURL() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanLinksSelectsPrimaryURL(t *testing.T) {
|
||||
outputs := []Output{
|
||||
{
|
||||
DestinationPath: "report.md",
|
||||
Kind: state.OutputKindSource,
|
||||
},
|
||||
{
|
||||
DestinationPath: "report.html",
|
||||
Kind: state.OutputKindGenerated,
|
||||
},
|
||||
{
|
||||
DestinationPath: "index.html",
|
||||
Kind: state.OutputKindGenerated,
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
primary string
|
||||
want string
|
||||
}{
|
||||
{name: "auto prefers index", primary: config.LinkPrimaryAuto, want: "https://reports.example.com/daily/"},
|
||||
{name: "html uses first generated html", primary: config.LinkPrimaryHTML, want: "https://reports.example.com/daily/report.html"},
|
||||
{name: "source uses first source", primary: config.LinkPrimarySource, want: "https://reports.example.com/daily/report.md"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
linked, primaryURL, err := PlanLinks(Request{
|
||||
DestinationBundlePath: "daily",
|
||||
Links: &config.Links{
|
||||
BaseURL: "https://reports.example.com",
|
||||
Primary: tt.primary,
|
||||
},
|
||||
}, outputs)
|
||||
if err != nil {
|
||||
t.Fatalf("PlanLinks() error = %v", err)
|
||||
}
|
||||
if primaryURL != tt.want {
|
||||
t.Fatalf("primary URL = %q, want %q", primaryURL, tt.want)
|
||||
}
|
||||
for index, output := range linked {
|
||||
if output.URL == "" {
|
||||
t.Fatalf("linked output %d has empty URL", index)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanLinksReturnsNoPrimaryWhenPolicyHasNoMatch(t *testing.T) {
|
||||
linked, primaryURL, err := PlanLinks(Request{
|
||||
DestinationBundlePath: "daily",
|
||||
Links: &config.Links{
|
||||
BaseURL: "https://reports.example.com",
|
||||
Primary: config.LinkPrimaryHTML,
|
||||
},
|
||||
}, []Output{{
|
||||
DestinationPath: "report.md",
|
||||
Kind: state.OutputKindSource,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("PlanLinks() error = %v", err)
|
||||
}
|
||||
if primaryURL != "" {
|
||||
t.Fatalf("primary URL = %q, want empty", primaryURL)
|
||||
}
|
||||
if linked[0].URL != "https://reports.example.com/daily/report.md" {
|
||||
t.Fatalf("linked URL = %q", linked[0].URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanLinksLeavesOutputsUnchangedWithoutConfig(t *testing.T) {
|
||||
outputs := []Output{{DestinationPath: "report.md", Kind: state.OutputKindSource}}
|
||||
linked, primaryURL, err := PlanLinks(Request{}, outputs)
|
||||
if err != nil {
|
||||
t.Fatalf("PlanLinks() error = %v", err)
|
||||
}
|
||||
if primaryURL != "" {
|
||||
t.Fatalf("primary URL = %q, want empty", primaryURL)
|
||||
}
|
||||
if linked[0].URL != "" {
|
||||
t.Fatalf("output URL = %q, want empty", linked[0].URL)
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,7 @@ func stateOutputs(outputs []Output) []state.OutputFile {
|
||||
Kind: output.Kind,
|
||||
SourcePath: output.SourcePath,
|
||||
Transform: output.Transform,
|
||||
URL: output.URL,
|
||||
SHA256: output.SHA256,
|
||||
Size: output.Size,
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@ type Request struct {
|
||||
PathMapping string
|
||||
Publish config.PublishPolicy
|
||||
Transform config.Transform
|
||||
Links *config.Links
|
||||
Transformers TransformerResolver
|
||||
Transfer config.TransferPolicy
|
||||
DistributorVersion string
|
||||
@@ -53,6 +54,7 @@ type Plan struct {
|
||||
Action Action
|
||||
Reason string
|
||||
Force bool
|
||||
PrimaryURL string
|
||||
Outputs []Output
|
||||
ExistingState *state.DistributorState
|
||||
}
|
||||
@@ -62,6 +64,7 @@ type Output struct {
|
||||
DestinationPath string
|
||||
Kind string
|
||||
Transform string
|
||||
URL string
|
||||
Data []byte
|
||||
SHA256 string
|
||||
Size int64
|
||||
@@ -75,6 +78,10 @@ func Build(ctx context.Context, req Request) (Plan, error) {
|
||||
if err != nil {
|
||||
return Plan{}, err
|
||||
}
|
||||
outputs, primaryURL, err := PlanLinks(req, outputs)
|
||||
if err != nil {
|
||||
return Plan{}, err
|
||||
}
|
||||
status, err := inspectDestination(ctx, req.DestinationBackend, req.DestinationBundlePath)
|
||||
if err != nil {
|
||||
return Plan{}, err
|
||||
@@ -91,6 +98,7 @@ func Build(ctx context.Context, req Request) (Plan, error) {
|
||||
Action: action,
|
||||
Reason: reason,
|
||||
Force: action == ActionForceReplace,
|
||||
PrimaryURL: primaryURL,
|
||||
Outputs: outputs,
|
||||
ExistingState: status.State,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user