@@ -127,6 +127,18 @@ func applyCopyOperations(ctx context.Context,
127127 return nil
128128}
129129
130+ // If the copy operation uses the Extract strategy, it uses doublestar.Glob as we do not need to walk the whole tree
131+ // otherwise we us std fs.Glob
132+ func getGlobMatchingEntries (op swapi.CopyOperation , srcRoot * os.Root , srcPattern string ) ([]string , error ) {
133+ if op .Strategy == swapi .ExtractStrategy {
134+ // Use doublestar.Glob for recursive and advanced glob patterns (e.g., **/*.tar.gz)
135+ return doublestar .Glob (srcRoot .FS (), srcPattern )
136+ } else {
137+ // Use fs.Glob for simple, non-recursive glob patterns
138+ return fs .Glob (srcRoot .FS (), srcPattern )
139+ }
140+ }
141+
130142// applyCopyOperation applies a single copy operation from the sources to the staging directory.
131143// This function implements cp-like semantics by first analyzing the source pattern to determine
132144// if it's a glob, direct file/directory reference, or wildcard pattern, then making copy decisions
@@ -175,11 +187,11 @@ func applyCopyOperation(ctx context.Context,
175187
176188 if ! isGlobPattern {
177189 // Direct path reference - check what it actually is first (cp-like behavior)
178- return applySingleSourceCopy (ctx , op , srcRoot , srcPattern , stagingRoot , destRelPath , destEndsWithSlash )
190+ return applySingleSourceCopy (ctx , op , srcRoot , srcPattern , stagingRoot , stagingDir , destRelPath , destEndsWithSlash )
179191 }
180192
181- // Glob pattern - find all matches and copy each
182- matches , err := fs . Glob ( srcRoot . FS (), srcPattern )
193+ matches , err := getGlobMatchingEntries ( op , srcRoot , srcPattern )
194+
183195 if err != nil {
184196 return fmt .Errorf ("invalid glob pattern '%s': %w" , srcPattern , err )
185197 }
@@ -188,12 +200,19 @@ func applyCopyOperation(ctx context.Context,
188200 return fmt .Errorf ("no files match pattern '%s' in source '%s'" , srcPattern , srcAlias )
189201 }
190202
191- // Filter out excluded files
203+ // Filter out excluded files and special directory entries
192204 filteredMatches := make ([]string , 0 , len (matches ))
193205 for _ , match := range matches {
194- if ! shouldExclude (match , op .Exclude ) {
195- filteredMatches = append (filteredMatches , match )
206+ // Skip current directory and parent directory references
207+ // doublestar.Glob returns "." for patterns like "**" which would
208+ // cause the entire source to be copied, bypassing per-file strategies
209+ if match == "." || match == ".." {
210+ continue
211+ }
212+ if shouldExclude (match , op .Exclude ) {
213+ continue
196214 }
215+ filteredMatches = append (filteredMatches , match )
197216 }
198217
199218 if len (filteredMatches ) == 0 {
@@ -206,11 +225,24 @@ func applyCopyOperation(ctx context.Context,
206225 return err
207226 }
208227
209- // Calculate destination path based on glob pattern type
210- destFile := calculateGlobDestination (srcPattern , match , destRelPath )
211- if err := copyFileWithRoots (ctx , op , srcRoot , match , stagingRoot , destFile ); err != nil {
212- return fmt .Errorf ("failed to copy file '%s' to '%s': %w" , match , destFile , err )
228+ // Handle Extract strategy for tarballs
229+ if op .Strategy == swapi .ExtractStrategy {
230+ if ! isTarball (match ) {
231+ // Ignore files that are not tarball archives and directories
232+ continue
233+ }
234+ if err := extractTarball (ctx , srcRoot , match , stagingDir , destRelPath ); err != nil {
235+ return fmt .Errorf ("failed to extract tarball '%s' to '%s': %w" , match , destRelPath , err )
236+ }
237+ } else {
238+ // Calculate destination path based on glob pattern type
239+ destFile := calculateGlobDestination (srcPattern , match , destRelPath )
240+
241+ if err := copyFileWithRoots (ctx , op , srcRoot , match , stagingRoot , destFile ); err != nil {
242+ return fmt .Errorf ("failed to copy file '%s' to '%s': %w" , match , destFile , err )
243+ }
213244 }
245+
214246 }
215247
216248 return nil
@@ -223,6 +255,7 @@ func applySingleSourceCopy(ctx context.Context,
223255 srcRoot * os.Root ,
224256 srcPath string ,
225257 stagingRoot * os.Root ,
258+ stagingDir string ,
226259 destPath string ,
227260 destEndsWithSlash bool ) error {
228261 // Clean the source path to handle trailing slashes
@@ -238,10 +271,14 @@ func applySingleSourceCopy(ctx context.Context,
238271 }
239272
240273 if srcInfo .IsDir () {
274+ // Extract strategy is not supported for directories
275+ if op .Strategy == swapi .ExtractStrategy {
276+ return fmt .Errorf ("extract strategy is not supported for directories, got '%s'" , srcPath )
277+ }
241278 return applySingleDirectoryCopy (ctx , op , srcRoot , srcPath , stagingRoot , destPath )
242- } else {
243- return applySingleFileCopy (ctx , op , srcRoot , srcPath , stagingRoot , destPath , destEndsWithSlash )
244279 }
280+
281+ return applySingleFileCopy (ctx , op , srcRoot , srcPath , stagingRoot , stagingDir , destPath , destEndsWithSlash )
245282}
246283
247284// applySingleFileCopy handles copying a single file using cp-like semantics:
@@ -252,12 +289,22 @@ func applySingleFileCopy(ctx context.Context,
252289 srcRoot * os.Root ,
253290 srcPath string ,
254291 stagingRoot * os.Root ,
292+ stagingDir string ,
255293 destPath string ,
256294 destEndsWithSlash bool ) error {
257295 // Check if the file should be excluded
258296 if shouldExclude (srcPath , op .Exclude ) {
259297 return nil // Skip excluded file
260298 }
299+
300+ // Handle Extract strategy for tarballs
301+ if op .Strategy == swapi .ExtractStrategy {
302+ if ! isTarball (srcPath ) {
303+ return fmt .Errorf ("extract strategy requires tarball file (.tar.gz or .tgz), got '%s'" , srcPath )
304+ }
305+ return extractTarball (ctx , srcRoot , srcPath , stagingDir , destPath )
306+ }
307+
261308 var finalDestPath string
262309
263310 if destEndsWithSlash {
@@ -303,6 +350,7 @@ func containsGlobChars(path string) bool {
303350// - dir/** patterns strip the directory prefix (like cp -r dir/** dest/)
304351// - other patterns preserve the full match path
305352func calculateGlobDestination (pattern , match , destPath string ) string {
353+
306354 // Check if pattern ends with /** (recursive contents pattern)
307355 if strings .HasSuffix (pattern , "/**" ) {
308356 // Extract the directory prefix from pattern (everything before /**)
@@ -545,12 +593,21 @@ func shouldExclude(filePath string, excludePatterns []string) bool {
545593 return false
546594 }
547595
596+ fileName := filepath .Base (filePath )
597+
548598 for _ , pattern := range excludePatterns {
549599 // We validate the patterns when parsing the copy operation,
550600 // so it's safe to use MatchUnvalidated here.
551601 if doublestar .MatchUnvalidated (pattern , filePath ) {
552602 return true
553603 }
604+ // For simple patterns without path separators (e.g., "*.md"),
605+ // also match against just the filename. This provides a more
606+ // intuitive user experience where "*.md" excludes all markdown
607+ // files regardless of their directory depth.
608+ if ! strings .Contains (pattern , "/" ) && doublestar .MatchUnvalidated (pattern , fileName ) {
609+ return true
610+ }
554611 }
555612
556613 return false
0 commit comments