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