Skip to content

Commit 9bbe70a

Browse files
feat: [CI-18487]: Glob pattern support for PLUGIN_STRIP_PREFIX
1 parent d50ece1 commit 9bbe70a

File tree

3 files changed

+448
-3
lines changed

3 files changed

+448
-3
lines changed

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func main() {
8686
},
8787
cli.StringFlag{
8888
Name: "strip-prefix",
89-
Usage: "used to add or remove a prefix from the source/target path",
89+
Usage: "prefix to strip from source path (supports wildcards: *, **, ?)",
9090
EnvVar: "PLUGIN_STRIP_PREFIX",
9191
},
9292
cli.StringSliceFlag{

plugin.go

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"fmt"
45
"io"
56
"mime"
67
"os"
@@ -84,7 +85,7 @@ type Plugin struct {
8485
Source string
8586
Target string
8687

87-
// Strip the prefix from the target path
88+
// Strip the prefix from the target path (supports wildcards)
8889
StripPrefix string
8990

9091
// Exclude files matching this pattern.
@@ -138,6 +139,17 @@ func (p *Plugin) Exec() error {
138139
return err
139140
}
140141

142+
// Validate strip prefix pattern if it contains wildcards
143+
if p.StripPrefix != "" {
144+
if err := validateStripPrefix(p.StripPrefix); err != nil {
145+
log.WithFields(log.Fields{
146+
"error": err,
147+
"pattern": p.StripPrefix,
148+
}).Error("Invalid strip_prefix pattern")
149+
return err
150+
}
151+
}
152+
141153
for _, match := range matches {
142154
// skip directories
143155
if isDir(match, matches) {
@@ -305,7 +317,19 @@ func assumeRole(roleArn, roleSessionName, externalID string) *credentials.Creden
305317
// resolveKey is a helper function that returns s3 object key where file present at srcPath is uploaded to.
306318
// srcPath is assumed to be in forward slash format
307319
func resolveKey(target, srcPath, stripPrefix string) string {
308-
key := filepath.Join(target, strings.TrimPrefix(srcPath, filepath.ToSlash(stripPrefix)))
320+
// Use wildcard-aware prefix stripping
321+
stripped, err := stripWildcardPrefix(srcPath, stripPrefix)
322+
if err != nil {
323+
// Log error but continue with original path
324+
log.WithFields(log.Fields{
325+
"error": err,
326+
"path": srcPath,
327+
"pattern": stripPrefix,
328+
}).Warning("Failed to strip prefix, using original path")
329+
stripped = srcPath
330+
}
331+
332+
key := filepath.Join(target, stripped)
309333
key = filepath.ToSlash(key)
310334
if !strings.HasPrefix(key, "/") {
311335
key = "/" + key
@@ -508,3 +532,99 @@ func assumeRoleWithWebIdentity(sess *session.Session, roleArn, roleSessionName,
508532
}
509533
return credentials.NewStaticCredentials(*result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken), nil
510534
}
535+
536+
// validateStripPrefix validates a strip prefix pattern with wildcards
537+
func validateStripPrefix(pattern string) error {
538+
// Pattern must start with /
539+
if !strings.HasPrefix(pattern, "/") {
540+
return fmt.Errorf("strip_prefix must start with '/'")
541+
}
542+
543+
// Check length limit
544+
if len(pattern) > 256 {
545+
return fmt.Errorf("strip_prefix pattern too long (max 256 characters)")
546+
}
547+
548+
// Count wildcards
549+
wildcardCount := strings.Count(pattern, "*") + strings.Count(pattern, "?")
550+
if wildcardCount > 20 {
551+
return fmt.Errorf("strip_prefix pattern contains too many wildcards (max 20)")
552+
}
553+
554+
// Check for empty segments
555+
if strings.Contains(pattern, "//") {
556+
return fmt.Errorf("strip_prefix pattern contains empty segment '//'")
557+
}
558+
559+
// Check for invalid ** usage (must be standalone segment)
560+
parts := strings.Split(pattern, "/")
561+
for _, part := range parts {
562+
if strings.Contains(part, "**") && part != "**" {
563+
return fmt.Errorf("'**' must be a standalone directory segment")
564+
}
565+
}
566+
567+
return nil
568+
}
569+
570+
// patternToRegex converts shell-style wildcards to regex
571+
func patternToRegex(pattern string) (*regexp.Regexp, error) {
572+
// Escape special regex characters except our wildcards
573+
escaped := regexp.QuoteMeta(pattern)
574+
575+
// Replace escaped wildcards with regex equivalents
576+
// Order matters: ** must be replaced before *
577+
escaped = strings.ReplaceAll(escaped, `\*\*`, "(.+)") // ** -> (.+) any depth
578+
escaped = strings.ReplaceAll(escaped, `\*`, "([^/]+)") // * -> ([^/]+) one segment
579+
escaped = strings.ReplaceAll(escaped, `\?`, "([^/])") // ? -> ([^/]) one character
580+
581+
// Anchor at start
582+
escaped = "^" + escaped
583+
584+
return regexp.Compile(escaped)
585+
}
586+
587+
// stripWildcardPrefix strips prefix using wildcard pattern matching
588+
func stripWildcardPrefix(path, pattern string) (string, error) {
589+
if pattern == "" {
590+
return path, nil
591+
}
592+
593+
// Normalize paths
594+
path = filepath.ToSlash(path)
595+
pattern = filepath.ToSlash(pattern)
596+
597+
// Handle literal prefix (no wildcards)
598+
if !strings.ContainsAny(pattern, "*?") {
599+
stripped := strings.TrimPrefix(path, pattern)
600+
// Validate result for literal prefix
601+
if stripped == "" || stripped == "/" || strings.TrimPrefix(stripped, "/") == "" {
602+
return path, fmt.Errorf("strip_prefix removes entire path for '%s'", filepath.Base(path))
603+
}
604+
return stripped, nil
605+
}
606+
607+
// Convert pattern to regex
608+
re, err := patternToRegex(pattern)
609+
if err != nil {
610+
return path, fmt.Errorf("invalid pattern: %v", err)
611+
}
612+
613+
// Find matches
614+
matches := re.FindStringSubmatch(path)
615+
if len(matches) == 0 {
616+
// No match, return path unchanged
617+
return path, nil
618+
}
619+
620+
// Calculate what to strip (the full match)
621+
fullMatch := matches[0]
622+
stripped := strings.TrimPrefix(path, fullMatch)
623+
624+
// Validate result
625+
if stripped == "" || stripped == "/" || strings.TrimPrefix(stripped, "/") == "" {
626+
return path, fmt.Errorf("strip_prefix removes entire path for '%s'", filepath.Base(path))
627+
}
628+
629+
return stripped, nil
630+
}

0 commit comments

Comments
 (0)