|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "fmt" |
4 | 5 | "io" |
5 | 6 | "mime" |
6 | 7 | "os" |
@@ -84,7 +85,7 @@ type Plugin struct { |
84 | 85 | Source string |
85 | 86 | Target string |
86 | 87 |
|
87 | | - // Strip the prefix from the target path |
| 88 | + // Strip the prefix from the target path (supports wildcards) |
88 | 89 | StripPrefix string |
89 | 90 |
|
90 | 91 | // Exclude files matching this pattern. |
@@ -138,6 +139,17 @@ func (p *Plugin) Exec() error { |
138 | 139 | return err |
139 | 140 | } |
140 | 141 |
|
| 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 | + |
141 | 153 | for _, match := range matches { |
142 | 154 | // skip directories |
143 | 155 | if isDir(match, matches) { |
@@ -305,7 +317,19 @@ func assumeRole(roleArn, roleSessionName, externalID string) *credentials.Creden |
305 | 317 | // resolveKey is a helper function that returns s3 object key where file present at srcPath is uploaded to. |
306 | 318 | // srcPath is assumed to be in forward slash format |
307 | 319 | 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) |
309 | 333 | key = filepath.ToSlash(key) |
310 | 334 | if !strings.HasPrefix(key, "/") { |
311 | 335 | key = "/" + key |
@@ -508,3 +532,99 @@ func assumeRoleWithWebIdentity(sess *session.Session, roleArn, roleSessionName, |
508 | 532 | } |
509 | 533 | return credentials.NewStaticCredentials(*result.Credentials.AccessKeyId, *result.Credentials.SecretAccessKey, *result.Credentials.SessionToken), nil |
510 | 534 | } |
| 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