@@ -139,7 +139,8 @@ func (p *Plugin) Exec() error {
139139 return err
140140 }
141141
142- // Validate strip prefix pattern if it contains wildcards
142+ // Validate strip prefix pattern and precompile regex once
143+ normalizedStrip := strings .ReplaceAll (p .StripPrefix , "\\ " , "/" )
143144 if p .StripPrefix != "" {
144145 if err := validateStripPrefix (p .StripPrefix ); err != nil {
145146 log .WithFields (log.Fields {
@@ -150,13 +151,52 @@ func (p *Plugin) Exec() error {
150151 }
151152 }
152153
154+ var compiled * regexp.Regexp
155+ if normalizedStrip != "" && strings .ContainsAny (normalizedStrip , "*?" ) {
156+ var err error
157+ compiled , err = patternToRegex (normalizedStrip )
158+ if err != nil {
159+ log .WithFields (log.Fields {
160+ "error" : err ,
161+ "pattern" : p .StripPrefix ,
162+ }).Error ("Failed to compile strip_prefix pattern" )
163+ return err
164+ }
165+ }
166+
167+ anyMatched := false
168+
153169 for _ , match := range matches {
154170 // skip directories
155171 if isDir (match , matches ) {
156172 continue
157173 }
158174
159- target := resolveKey (p .Target , match , p .StripPrefix )
175+ // Preview stripping (using precompiled regex when available)
176+ stripped := match
177+ matched := false
178+ if normalizedStrip != "" {
179+ var err error
180+ stripped , matched , err = stripWildcardPrefixWithRegex (match , normalizedStrip , compiled )
181+ if err != nil {
182+ log .WithFields (log.Fields {
183+ "error" : err ,
184+ "path" : match ,
185+ "pattern" : p .StripPrefix ,
186+ }).Warn ("Failed to strip prefix, using original path" )
187+ stripped = match
188+ }
189+ }
190+ if matched {
191+ anyMatched = true
192+ }
193+
194+ // Build final key (ensure relative component for join)
195+ rel := strings .TrimPrefix (filepath .ToSlash (stripped ), "/" )
196+ target := filepath .ToSlash (filepath .Join (p .Target , rel ))
197+ if ! strings .HasPrefix (target , "/" ) {
198+ target = "/" + target
199+ }
160200
161201 contentType := matchExtension (match , p .ContentType )
162202 contentEncoding := matchExtension (match , p .ContentEncoding )
@@ -177,9 +217,22 @@ func (p *Plugin) Exec() error {
177217 "target" : target ,
178218 }).Info ("Uploading file" )
179219
180- // when executing a dry-run we exit because we don't actually want to
181- // upload the file to S3.
220+ // when executing a dry-run print what would be stripped and skip upload.
182221 if p .DryRun {
222+ removed := ""
223+ if matched {
224+ // removed prefix = original - stripped suffix
225+ orig := filepath .ToSlash (match )
226+ rem := strings .TrimSuffix (orig , filepath .ToSlash (stripped ))
227+ removed = rem
228+ }
229+ log .WithFields (log.Fields {
230+ "name" : match ,
231+ "bucket" : p .Bucket ,
232+ "target" : target ,
233+ "strip_pattern" : p .StripPrefix ,
234+ "removed_prefix" : removed ,
235+ }).Info ("Dry-run: would upload" )
183236 continue
184237 }
185238
@@ -238,6 +291,12 @@ func (p *Plugin) Exec() error {
238291 f .Close ()
239292 }
240293
294+ if normalizedStrip != "" && ! anyMatched {
295+ log .WithFields (log.Fields {
296+ "pattern" : p .StripPrefix ,
297+ }).Warn ("strip_prefix did not match any paths; keys will include original path" )
298+ }
299+
241300 return nil
242301}
243302
@@ -325,10 +384,12 @@ func resolveKey(target, srcPath, stripPrefix string) string {
325384 "error" : err ,
326385 "path" : srcPath ,
327386 "pattern" : stripPrefix ,
328- }).Warning ("Failed to strip prefix, using original path" )
387+ }).Warn ("Failed to strip prefix, using original path" )
329388 stripped = srcPath
330389 }
331-
390+ // Ensure we never drop the target when the stripped path is absolute.
391+ // Always join with a relative path component.
392+ stripped = strings .TrimPrefix (stripped , "/" )
332393 key := filepath .Join (target , stripped )
333394 key = filepath .ToSlash (key )
334395 if ! strings .HasPrefix (key , "/" ) {
@@ -341,42 +402,42 @@ func resolveSource(sourceDir, source, stripPrefix string) string {
341402 // Remove the leading sourceDir from the source path
342403 path := strings .TrimPrefix (strings .TrimPrefix (source , sourceDir ), "/" )
343404
344- // Add the specified stripPrefix to the resulting path
345- return stripPrefix + path
405+ // Add the specified stripPrefix to the resulting path
406+ return stripPrefix + path
346407}
347408
348409// checks if the source path is a dir
349410func isDir (source string , matches []string ) bool {
350- stat , err := os .Stat (source )
351- if err != nil {
352- return true // should never happen
353- }
354- if stat .IsDir () {
355- count := 0
356- for _ , match := range matches {
357- if strings .HasPrefix (match , source ) {
358- count ++
359- }
360- }
361- if count <= 1 {
362- log .Warnf ("Skipping '%s' since it is a directory. Please use correct glob expression if this is unexpected." , source )
363- }
364- return true
365- }
366- return false
411+ stat , err := os .Stat (source )
412+ if err != nil {
413+ return true // should never happen
414+ }
415+ if stat .IsDir () {
416+ count := 0
417+ for _ , match := range matches {
418+ if strings .HasPrefix (match , source ) {
419+ count ++
420+ }
421+ }
422+ if count <= 1 {
423+ log .Warnf ("Skipping '%s' since it is a directory. Please use correct glob expression if this is unexpected." , source )
424+ }
425+ return true
426+ }
427+ return false
367428}
368429
369430// normalizePath converts the path to a forward slash format and trims the prefix.
370431func normalizePath (path string ) string {
371- return strings .TrimPrefix (filepath .ToSlash (path ), "/" )
432+ return strings .TrimPrefix (filepath .ToSlash (path ), "/" )
372433}
373434
374435// downloadS3Object downloads a single object from S3
375436func (p * Plugin ) downloadS3Object (client * s3.S3 , sourceDir , key , target string ) error {
376- log .WithFields (log.Fields {
377- "bucket" : p .Bucket ,
378- "key" : key ,
379- }).Info ("Getting S3 object" )
437+ log .WithFields (log.Fields {
438+ "bucket" : p .Bucket ,
439+ "key" : key ,
440+ }).Info ("Getting S3 object" )
380441
381442 obj , err := client .GetObject (& s3.GetObjectInput {
382443 Bucket : & p .Bucket ,
@@ -394,7 +455,6 @@ func (p *Plugin) downloadS3Object(client *s3.S3, sourceDir, key, target string)
394455
395456 // Create the destination file path
396457 destination := filepath .Join (p .Target , target )
397- log .Println ("Destination: " , destination )
398458
399459 // Extract the directory from the destination path
400460 dir := filepath .Dir (destination )
@@ -534,10 +594,18 @@ func assumeRoleWithWebIdentity(sess *session.Session, roleArn, roleSessionName,
534594
535595// validateStripPrefix validates a strip prefix pattern with wildcards
536596func validateStripPrefix (pattern string ) error {
537- // Pattern must start with /
538- if ! strings .HasPrefix (pattern , "/" ) {
539- return fmt .Errorf ("strip_prefix must start with '/'" )
540- }
597+ // Normalize Windows backslashes to forward slashes for validation (OS-independent)
598+ pattern = strings .ReplaceAll (pattern , "\\ " , "/" )
599+
600+ // Pattern must start with /
601+ if ! strings .HasPrefix (pattern , "/" ) {
602+ return fmt .Errorf ("strip_prefix must start with '/'" )
603+ }
604+
605+ // Reject Windows drive-letter prefixes like C:/...
606+ if len (pattern ) >= 2 && pattern [1 ] == ':' {
607+ return fmt .Errorf ("strip_prefix must be an absolute POSIX-style path (e.g. '/root/...'), drive letters are not supported" )
608+ }
541609
542610 // Check length limit
543611 if len (pattern ) > 256 {
@@ -563,7 +631,7 @@ func validateStripPrefix(pattern string) error {
563631 }
564632 }
565633
566- return nil
634+ return nil
567635}
568636
569637// patternToRegex converts shell-style wildcards to regex
@@ -580,50 +648,56 @@ func patternToRegex(pattern string) (*regexp.Regexp, error) {
580648 // Anchor at start
581649 escaped = "^" + escaped
582650
583- return regexp .Compile (escaped )
651+ return regexp .Compile (escaped )
652+ }
653+
654+ // stripWildcardPrefixWithRegex strips prefix using wildcard pattern matching, reusing
655+ // a precompiled regex when provided. It returns the possibly stripped path, whether
656+ // the pattern matched, and any error if stripping would remove the entire key.
657+ func stripWildcardPrefixWithRegex (path , pattern string , re * regexp.Regexp ) (string , bool , error ) {
658+ if pattern == "" {
659+ return path , false , nil
660+ }
661+
662+ // Normalize paths to forward slashes (OS-independent)
663+ path = strings .ReplaceAll (path , "\\ " , "/" )
664+ pattern = strings .ReplaceAll (pattern , "\\ " , "/" )
665+
666+ // Literal prefix (no wildcards)
667+ if ! strings .ContainsAny (pattern , "*?" ) {
668+ if ! strings .HasPrefix (path , pattern ) {
669+ return path , false , nil
670+ }
671+ stripped := strings .TrimPrefix (path , pattern )
672+ if stripped == "" || stripped == "/" || strings .TrimPrefix (stripped , "/" ) == "" {
673+ return path , true , fmt .Errorf ("strip_prefix removes entire path for '%s'" , filepath .Base (path ))
674+ }
675+ return stripped , true , nil
676+ }
677+
678+ // Wildcard pattern
679+ var err error
680+ if re == nil {
681+ re , err = patternToRegex (pattern )
682+ if err != nil {
683+ return path , false , fmt .Errorf ("invalid pattern: %v" , err )
684+ }
685+ }
686+
687+ m := re .FindStringSubmatch (path )
688+ if len (m ) == 0 {
689+ return path , false , nil
690+ }
691+ full := m [0 ]
692+ stripped := strings .TrimPrefix (path , full )
693+ if stripped == "" || stripped == "/" || strings .TrimPrefix (stripped , "/" ) == "" {
694+ return path , true , fmt .Errorf ("strip_prefix removes entire path for '%s'" , filepath .Base (path ))
695+ }
696+ return stripped , true , nil
584697}
585698
586699// stripWildcardPrefix strips prefix using wildcard pattern matching
587700func stripWildcardPrefix (path , pattern string ) (string , error ) {
588- if pattern == "" {
589- return path , nil
590- }
591-
592- // Normalize paths
593- path = filepath .ToSlash (path )
594- pattern = filepath .ToSlash (pattern )
595-
596- // Handle literal prefix (no wildcards)
597- if ! strings .ContainsAny (pattern , "*?" ) {
598- stripped := strings .TrimPrefix (path , pattern )
599- // Validate result for literal prefix
600- if stripped == "" || stripped == "/" || strings .TrimPrefix (stripped , "/" ) == "" {
601- return path , fmt .Errorf ("strip_prefix removes entire path for '%s'" , filepath .Base (path ))
602- }
603- return stripped , nil
604- }
605-
606- // Convert pattern to regex
607- re , err := patternToRegex (pattern )
608- if err != nil {
609- return path , fmt .Errorf ("invalid pattern: %v" , err )
610- }
611-
612- // Find matches
613- matches := re .FindStringSubmatch (path )
614- if len (matches ) == 0 {
615- // No match, return path unchanged
616- return path , nil
617- }
618-
619- // Calculate what to strip (the full match)
620- fullMatch := matches [0 ]
621- stripped := strings .TrimPrefix (path , fullMatch )
622-
623- // Validate result
624- if stripped == "" || stripped == "/" || strings .TrimPrefix (stripped , "/" ) == "" {
625- return path , fmt .Errorf ("strip_prefix removes entire path for '%s'" , filepath .Base (path ))
626- }
627-
628- return stripped , nil
701+ stripped , _ , err := stripWildcardPrefixWithRegex (path , pattern , nil )
702+ return stripped , err
629703}
0 commit comments