@@ -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,10 +225,22 @@ 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 }
214245 }
215246
@@ -223,6 +254,7 @@ func applySingleSourceCopy(ctx context.Context,
223254 srcRoot * os.Root ,
224255 srcPath string ,
225256 stagingRoot * os.Root ,
257+ stagingDir string ,
226258 destPath string ,
227259 destEndsWithSlash bool ) error {
228260 // Clean the source path to handle trailing slashes
@@ -238,10 +270,14 @@ func applySingleSourceCopy(ctx context.Context,
238270 }
239271
240272 if srcInfo .IsDir () {
273+ // Extract strategy is not supported for directories
274+ if op .Strategy == swapi .ExtractStrategy {
275+ return fmt .Errorf ("extract strategy is not supported for directories, got '%s'" , srcPath )
276+ }
241277 return applySingleDirectoryCopy (ctx , op , srcRoot , srcPath , stagingRoot , destPath )
242- } else {
243- return applySingleFileCopy (ctx , op , srcRoot , srcPath , stagingRoot , destPath , destEndsWithSlash )
244278 }
279+
280+ return applySingleFileCopy (ctx , op , srcRoot , srcPath , stagingRoot , stagingDir , destPath , destEndsWithSlash )
245281}
246282
247283// applySingleFileCopy handles copying a single file using cp-like semantics:
@@ -252,12 +288,22 @@ func applySingleFileCopy(ctx context.Context,
252288 srcRoot * os.Root ,
253289 srcPath string ,
254290 stagingRoot * os.Root ,
291+ stagingDir string ,
255292 destPath string ,
256293 destEndsWithSlash bool ) error {
257294 // Check if the file should be excluded
258295 if shouldExclude (srcPath , op .Exclude ) {
259296 return nil // Skip excluded file
260297 }
298+
299+ // Handle Extract strategy for tarballs
300+ if op .Strategy == swapi .ExtractStrategy {
301+ if ! isTarball (srcPath ) {
302+ return fmt .Errorf ("extract strategy requires tarball file (.tar.gz or .tgz), got '%s'" , srcPath )
303+ }
304+ return extractTarball (ctx , srcRoot , srcPath , stagingDir , destPath )
305+ }
306+
261307 var finalDestPath string
262308
263309 if destEndsWithSlash {
@@ -303,6 +349,7 @@ func containsGlobChars(path string) bool {
303349// - dir/** patterns strip the directory prefix (like cp -r dir/** dest/)
304350// - other patterns preserve the full match path
305351func calculateGlobDestination (pattern , match , destPath string ) string {
352+
306353 // Check if pattern ends with /** (recursive contents pattern)
307354 if strings .HasSuffix (pattern , "/**" ) {
308355 // Extract the directory prefix from pattern (everything before /**)
@@ -545,12 +592,21 @@ func shouldExclude(filePath string, excludePatterns []string) bool {
545592 return false
546593 }
547594
595+ fileName := filepath .Base (filePath )
596+
548597 for _ , pattern := range excludePatterns {
549598 // We validate the patterns when parsing the copy operation,
550599 // so it's safe to use MatchUnvalidated here.
551600 if doublestar .MatchUnvalidated (pattern , filePath ) {
552601 return true
553602 }
603+ // For simple patterns without path separators (e.g., "*.md"),
604+ // also match against just the filename. This provides a more
605+ // intuitive user experience where "*.md" excludes all markdown
606+ // files regardless of their directory depth.
607+ if ! strings .Contains (pattern , "/" ) && doublestar .MatchUnvalidated (pattern , fileName ) {
608+ return true
609+ }
554610 }
555611
556612 return false
0 commit comments