package main import ( "context" "fmt" "log" "net/http" "path/filepath" "strings" "time" s3 "github.com/minio/minio-go/v7" ) // parseDocsRequestPath parses paths like: // /docs////... (rest may be empty) func parseDocsRequestPath(p string) (namespace, name, version, rest string, err error) { p = strings.TrimPrefix(p, "/") p = strings.TrimPrefix(p, "docs/") parts := strings.SplitN(p, "/", 4) if len(parts) < 3 { err = fmt.Errorf("invalid docs path: %s", p) return } namespace = parts[0] name = parts[1] version = parts[2] if len(parts) == 3 { rest = "" } else { rest = parts[3] } return } // docsObjectKey builds the S3 key for a docs object given parsed parts. func docsObjectKey(namespace, name, version, pathPart string) string { clean := strings.TrimPrefix(pathPart, "/") if clean == "" { return fmt.Sprintf("docs/%s/%s/%s/index.html", namespace, name, version) } return fmt.Sprintf("docs/%s/%s/%s/%s", namespace, name, version, clean) } // tryCandidateKeys returns a list of keys to attempt for a given request path. // Order matters: exact path first, then /index.html, then top-level index. func tryCandidateKeys(namespace, name, version, rest string) []string { keys := []string{} if rest == "" { keys = append(keys, docsObjectKey(namespace, name, version, "index.html")) return keys } // exact keys = append(keys, docsObjectKey(namespace, name, version, rest)) // if it looks like a directory or has no extension, try index under it if strings.HasSuffix(rest, "/") || filepath.Ext(rest) == "" { keys = append(keys, docsObjectKey(namespace, name, version, strings.TrimSuffix(rest, "/")+"/index.html")) } // finally, try root index keys = append(keys, docsObjectKey(namespace, name, version, "index.html")) return keys } // docsHandler serves static documentation files from S3 (public-facing via Ingress). func docsHandler(w http.ResponseWriter, r *http.Request) { ns, name, ver, rest, err := parseDocsRequestPath(r.URL.Path) if err != nil { http.Error(w, "Bad docs path", http.StatusBadRequest) return } ctx := context.Background() candidates := tryCandidateKeys(ns, name, ver, rest) log.Printf("Docs candidates (host=%s): %v", hostname, candidates) var lastErr error for _, key := range candidates { _, err := s3Client.StatObject(ctx, bucketName, key, s3.GetObjectOptions{}) if err != nil { lastErr = err continue } presigned, err := s3Client.PresignedGetObject(ctx, bucketName, key, 15*time.Minute, nil) if err != nil { lastErr = err continue } http.Redirect(w, r, presigned.String(), http.StatusFound) return } log.Printf("Docs not found in candidates: %v, lastErr: %v", candidates, lastErr) http.Error(w, "Documentation not found", http.StatusNotFound) }