@@ -13,6 +13,7 @@ import (
13
13
"os/user"
14
14
"path"
15
15
"path/filepath"
16
+ "slices"
16
17
"strconv"
17
18
"strings"
18
19
"sync"
@@ -350,6 +351,7 @@ type GetOptions struct {
350
351
ChmodDirs * os.FileMode // set permissions on directories. no effect on archives being extracted
351
352
ChownFiles * idtools.IDPair // set ownership of files. no effect on archives being extracted
352
353
ChmodFiles * os.FileMode // set permissions on files. no effect on archives being extracted
354
+ Parents bool // maintain the sources parent directory in the destination
353
355
StripSetuidBit bool // strip the setuid bit off of items being copied. no effect on archives being extracted
354
356
StripSetgidBit bool // strip the setgid bit off of items being copied. no effect on archives being extracted
355
357
StripStickyBit bool // strip the sticky bit off of items being copied. no effect on archives being extracted
@@ -1182,6 +1184,49 @@ func errorIsPermission(err error) bool {
1182
1184
return errors .Is (err , os .ErrPermission ) || strings .Contains (err .Error (), "permission denied" )
1183
1185
}
1184
1186
1187
+ func getParents (path string , stopPath string ) []string {
1188
+ out := []string {}
1189
+ for path != "/" && path != "." && path != stopPath {
1190
+ path = filepath .Dir (path )
1191
+ if path == stopPath {
1192
+ continue
1193
+ }
1194
+ out = append (out , path )
1195
+ }
1196
+ slices .Reverse (out )
1197
+ return out
1198
+ }
1199
+
1200
+ func checkLinks (item string , req request , info os.FileInfo ) (string , os.FileInfo , error ) {
1201
+ // chase links. if we hit a dead end, we should just fail
1202
+ oldItem := item
1203
+ followedLinks := 0
1204
+ const maxFollowedLinks = 16
1205
+ for ! req .GetOptions .NoDerefSymlinks && info .Mode ()& os .ModeType == os .ModeSymlink && followedLinks < maxFollowedLinks {
1206
+ path , err := os .Readlink (item )
1207
+ if err != nil {
1208
+ continue
1209
+ }
1210
+ if filepath .IsAbs (path ) || looksLikeAbs (path ) {
1211
+ path = filepath .Join (req .Root , path )
1212
+ } else {
1213
+ path = filepath .Join (filepath .Dir (item ), path )
1214
+ }
1215
+ item = path
1216
+ if _ , err = convertToRelSubdirectory (req .Root , item ); err != nil {
1217
+ return "" , nil , fmt .Errorf ("copier: get: computing path of %q(%q) relative to %q: %w" , oldItem , item , req .Root , err )
1218
+ }
1219
+ if info , err = os .Lstat (item ); err != nil {
1220
+ return "" , nil , fmt .Errorf ("copier: get: lstat %q(%q): %w" , oldItem , item , err )
1221
+ }
1222
+ followedLinks ++
1223
+ }
1224
+ if followedLinks >= maxFollowedLinks {
1225
+ return "" , nil , fmt .Errorf ("copier: get: resolving symlink %q(%q): %w" , oldItem , item , syscall .ELOOP )
1226
+ }
1227
+ return item , info , nil
1228
+ }
1229
+
1185
1230
func copierHandlerGet (bulkWriter io.Writer , req request , pm * fileutils.PatternMatcher , idMappings * idtools.IDMappings ) (* response , func () error , error ) {
1186
1231
statRequest := req
1187
1232
statRequest .Request = requestStat
@@ -1196,15 +1241,25 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
1196
1241
return errorResponse ("copier: get: expected at least one glob pattern, got 0" )
1197
1242
}
1198
1243
// build a queue of items by globbing
1199
- var queue []string
1244
+ type queueItem struct {
1245
+ glob string
1246
+ parents []string
1247
+ }
1248
+ var queue []queueItem
1200
1249
globMatchedCount := 0
1201
1250
for _ , glob := range req .Globs {
1202
1251
globMatched , err := extendedGlob (glob )
1203
1252
if err != nil {
1204
1253
return errorResponse ("copier: get: glob %q: %v" , glob , err )
1205
1254
}
1206
- globMatchedCount += len (globMatched )
1207
- queue = append (queue , globMatched ... )
1255
+ for _ , path := range globMatched {
1256
+ var parents []string
1257
+ if req .GetOptions .Parents {
1258
+ parents = getParents (path , req .Directory )
1259
+ }
1260
+ globMatchedCount ++
1261
+ queue = append (queue , queueItem {glob : path , parents : parents })
1262
+ }
1208
1263
}
1209
1264
// no matches -> error
1210
1265
if len (queue ) == 0 {
@@ -1219,7 +1274,9 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
1219
1274
defer tw .Close ()
1220
1275
hardlinkChecker := new (hardlinkChecker )
1221
1276
itemsCopied := 0
1222
- for i , item := range queue {
1277
+ addedParents := map [string ]struct {}{}
1278
+ for i , qItem := range queue {
1279
+ item := qItem .glob
1223
1280
// if we're not discarding the names of individual directories, keep track of this one
1224
1281
relNamePrefix := ""
1225
1282
if req .GetOptions .KeepDirectoryNames {
@@ -1230,31 +1287,53 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
1230
1287
if err != nil {
1231
1288
return fmt .Errorf ("copier: get: lstat %q: %w" , item , err )
1232
1289
}
1233
- // chase links. if we hit a dead end, we should just fail
1234
- followedLinks := 0
1235
- const maxFollowedLinks = 16
1236
- for ! req .GetOptions .NoDerefSymlinks && info .Mode ()& os .ModeType == os .ModeSymlink && followedLinks < maxFollowedLinks {
1237
- path , err := os .Readlink (item )
1290
+ if req .GetOptions .Parents && info .Mode ().IsDir () {
1291
+ if ! slices .Contains (qItem .parents , item ) {
1292
+ qItem .parents = append (qItem .parents , item )
1293
+ }
1294
+ }
1295
+ // Copy parents in to tarball first if exists
1296
+ for _ , parent := range qItem .parents {
1297
+ oldParent := parent
1298
+ parentInfo , err := os .Lstat (parent )
1238
1299
if err != nil {
1239
- continue
1300
+ return fmt . Errorf ( "copier: get: lstat %q: %w" , parent , err )
1240
1301
}
1241
- if filepath .IsAbs (path ) || looksLikeAbs (path ) {
1242
- path = filepath .Join (req .Root , path )
1243
- } else {
1244
- path = filepath .Join (filepath .Dir (item ), path )
1302
+ parent , parentInfo , err = checkLinks (parent , req , parentInfo )
1303
+ if err != nil {
1304
+ return err
1305
+ }
1306
+ parentName , err := convertToRelSubdirectory (req .Directory , oldParent )
1307
+ if err != nil {
1308
+ return fmt .Errorf ("copier: get: error computing path of %q relative to %q: %w" , parent , req .Directory , err )
1245
1309
}
1246
- item = path
1247
- if _ , err = convertToRelSubdirectory ( req . Root , item ); err != nil {
1248
- return fmt . Errorf ( "copier: get: computing path of %q(%q) relative to %q: %w" , queue [ i ], item , req . Root , err )
1310
+ if parentName == "" || parentName == "." {
1311
+ // skip the "." entry
1312
+ continue
1249
1313
}
1250
- if info , err = os .Lstat (item ); err != nil {
1251
- return fmt .Errorf ("copier: get: lstat %q(%q): %w" , queue [i ], item , err )
1314
+
1315
+ if _ , ok := addedParents [parentName ]; ok {
1316
+ continue
1252
1317
}
1253
- followedLinks ++
1318
+ addedParents [parentName ] = struct {}{}
1319
+
1320
+ if err := copierHandlerGetOne (parentInfo , "" , parentName , parent , req .GetOptions , tw , hardlinkChecker , idMappings ); err != nil {
1321
+ if req .GetOptions .IgnoreUnreadable && errorIsPermission (err ) {
1322
+ continue
1323
+ } else if errors .Is (err , os .ErrNotExist ) {
1324
+ logrus .Warningf ("copier: file disappeared while reading: %q" , parent )
1325
+ return nil
1326
+ }
1327
+ return fmt .Errorf ("copier: get: %q: %w" , queue [i ].glob , err )
1328
+ }
1329
+ itemsCopied ++
1254
1330
}
1255
- if followedLinks >= maxFollowedLinks {
1256
- return fmt .Errorf ("copier: get: resolving symlink %q(%q): %w" , queue [i ], item , syscall .ELOOP )
1331
+
1332
+ item , info , err = checkLinks (item , req , info )
1333
+ if err != nil {
1334
+ return err
1257
1335
}
1336
+
1258
1337
// evaluate excludes relative to the root directory
1259
1338
if info .Mode ().IsDir () {
1260
1339
// we don't expand any of the contents that are archives
@@ -1354,6 +1433,12 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
1354
1433
ok = filepath .SkipDir
1355
1434
}
1356
1435
}
1436
+ if req .GetOptions .Parents {
1437
+ rel , err = convertToRelSubdirectory (req .Directory , path )
1438
+ if err != nil {
1439
+ return fmt .Errorf ("copier: get: error computing path of %q relative to %q: %w" , path , req .Root , err )
1440
+ }
1441
+ }
1357
1442
// add the item to the outgoing tar stream
1358
1443
if err := copierHandlerGetOne (info , symlinkTarget , rel , path , options , tw , hardlinkChecker , idMappings ); err != nil {
1359
1444
if req .GetOptions .IgnoreUnreadable && errorIsPermission (err ) {
@@ -1368,7 +1453,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
1368
1453
}
1369
1454
// walk the directory tree, checking/adding items individually
1370
1455
if err := filepath .WalkDir (item , walkfn ); err != nil {
1371
- return fmt .Errorf ("copier: get: %q(%q): %w" , queue [i ], item , err )
1456
+ return fmt .Errorf ("copier: get: %q(%q): %w" , queue [i ]. glob , item , err )
1372
1457
}
1373
1458
itemsCopied ++
1374
1459
} else {
@@ -1379,15 +1464,24 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa
1379
1464
if skip {
1380
1465
continue
1381
1466
}
1382
- // add the item to the outgoing tar stream. in
1383
- // cases where this was a symlink that we
1384
- // dereferenced, be sure to use the name of the
1385
- // link.
1386
- if err := copierHandlerGetOne (info , "" , filepath .Base (queue [i ]), item , req .GetOptions , tw , hardlinkChecker , idMappings ); err != nil {
1467
+
1468
+ name := filepath .Base (queue [i ].glob )
1469
+ if req .GetOptions .Parents {
1470
+ name , err = convertToRelSubdirectory (req .Directory , queue [i ].glob )
1471
+ if err != nil {
1472
+ return fmt .Errorf ("copier: get: error computing path of %q relative to %q: %w" , item , req .Root , err )
1473
+ }
1474
+ if name == "" || name == "." {
1475
+ // skip the "." entry
1476
+ continue
1477
+ }
1478
+ }
1479
+
1480
+ if err := copierHandlerGetOne (info , "" , name , item , req .GetOptions , tw , hardlinkChecker , idMappings ); err != nil {
1387
1481
if req .GetOptions .IgnoreUnreadable && errorIsPermission (err ) {
1388
1482
continue
1389
1483
}
1390
- return fmt .Errorf ("copier: get: %q: %w" , queue [i ], err )
1484
+ return fmt .Errorf ("copier: get: %q: %w" , queue [i ]. glob , err )
1391
1485
}
1392
1486
itemsCopied ++
1393
1487
}
0 commit comments