Skip to content

Commit d92df9d

Browse files
authored
feat: option to have File store preserve permissions when extracting (#891)
This PR takes care of #886, or rather part of it. It adds an option to the File store that's similar to tar's `--preserve-permissions`. closes #886 Signed-off-by: Sammy Abed <[email protected]>
1 parent cb6d75b commit d92df9d

File tree

4 files changed

+80
-9
lines changed

4 files changed

+80
-9
lines changed

content/file/file.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ type Store struct {
108108
// value overrides the [AnnotationUnpack].
109109
// Default value: false.
110110
SkipUnpack bool
111+
// PreservePermissions controls whether to preserve file permissions when unpacking,
112+
// disregarding the active umask, similar to tar's `--preserve-permissions`
113+
PreservePermissions bool
111114

112115
workingDir string // the working directory of the file store
113116
closed int32 // if the store is closed - 0: false, 1: true.
@@ -499,7 +502,7 @@ func (s *Store) pushDir(name, target string, expected ocispec.Descriptor, conten
499502
checksum := expected.Annotations[AnnotationDigest]
500503
buf := bufPool.Get().(*[]byte)
501504
defer bufPool.Put(buf)
502-
if err := extractTarGzip(target, name, gzPath, checksum, *buf); err != nil {
505+
if err := extractTarGzip(target, name, gzPath, checksum, *buf, s.PreservePermissions); err != nil {
503506
return fmt.Errorf("failed to extract tar to %s: %w", target, err)
504507
}
505508
return nil

content/file/utils.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func tarDirectory(ctx context.Context, root, prefix string, w io.Writer, removeT
113113

114114
// extractTarGzip decompresses the gzip
115115
// and extracts tar file to a directory specified by the `dir` parameter.
116-
func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte) (err error) {
116+
func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte, preservePermissions bool) (err error) {
117117
fp, err := os.Open(gzPath)
118118
if err != nil {
119119
return err
@@ -144,7 +144,7 @@ func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte) (err
144144
r = io.TeeReader(r, verifier)
145145
}
146146
}
147-
if err := extractTarDirectory(dirPath, dirName, r, buf); err != nil {
147+
if err := extractTarDirectory(dirPath, dirName, r, buf, preservePermissions); err != nil {
148148
return err
149149
}
150150
if verifier != nil && !verifier.Verified() {
@@ -156,7 +156,7 @@ func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte) (err
156156
// extractTarDirectory extracts tar file to a directory specified by the `dir`
157157
// parameter. The file name prefix is ensured to be the string specified by the
158158
// `prefix` parameter and is trimmed.
159-
func extractTarDirectory(dirPath, dirName string, r io.Reader, buf []byte) error {
159+
func extractTarDirectory(dirPath, dirName string, r io.Reader, buf []byte, preservePermissions bool) error {
160160
tr := tar.NewReader(r)
161161
for {
162162
header, err := tr.Next()
@@ -214,7 +214,14 @@ func extractTarDirectory(dirPath, dirName string, r io.Reader, buf []byte) error
214214
}
215215

216216
// Change access time and modification time if possible (error ignored)
217-
os.Chtimes(filePath, header.AccessTime, header.ModTime)
217+
_ = os.Chtimes(filePath, header.AccessTime, header.ModTime)
218+
219+
// Restore full mode bits
220+
if preservePermissions && (header.Typeflag == tar.TypeReg || header.Typeflag == tar.TypeDir) {
221+
if err := os.Chmod(filePath, os.FileMode(header.Mode)); err != nil {
222+
return err
223+
}
224+
}
218225
}
219226
}
220227

content/file/utils_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ func Test_ensureLinkPath(t *testing.T) {
238238

239239
func Test_extractTarGzip_Error(t *testing.T) {
240240
t.Run("Non-existing file", func(t *testing.T) {
241-
err := extractTarGzip("", "", "non-existing-file", "", nil)
241+
err := extractTarGzip("", "", "non-existing-file", "", nil, false)
242242
if err == nil {
243243
t.Fatal("expected error, got nil")
244244
}
@@ -300,7 +300,7 @@ func Test_extractTarDirectory(t *testing.T) {
300300
dirPath := filepath.Join(tempDir, dirName)
301301
buf := make([]byte, 1024)
302302

303-
if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tt.tarData), buf); (err != nil) != tt.wantErr {
303+
if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tt.tarData), buf, false); (err != nil) != tt.wantErr {
304304
t.Fatalf("extractTarDirectory() error = %v, wantErr %v", err, tt.wantErr)
305305
}
306306
if !tt.wantErr {
@@ -348,7 +348,7 @@ func Test_extractTarDirectory_HardLink(t *testing.T) {
348348
{name: "base/test_hardlink", linkname: linkPath, mode: 0666, isHardLink: true},
349349
})
350350

351-
if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf); err != nil {
351+
if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf, false); err != nil {
352352
t.Fatalf("extractTarDirectory() error = %v", err)
353353
}
354354

@@ -372,7 +372,7 @@ func Test_extractTarDirectory_HardLink(t *testing.T) {
372372
{name: "base/test_hardlink", linkname: "whatever", mode: 0666, isHardLink: true},
373373
})
374374

375-
if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf); err == nil {
375+
if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf, false); err == nil {
376376
t.Error("extractTarDirectory() error = nil, wantErr = true")
377377
}
378378
})

content/file/utils_unix_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//go:build !windows
2+
3+
/*
4+
Copyright The ORAS Authors.
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package file
19+
20+
import (
21+
"bytes"
22+
"os"
23+
"path/filepath"
24+
"testing"
25+
)
26+
27+
func Test_extractTarDirectory_PreservePermissions(t *testing.T) {
28+
fileContent := "hello world"
29+
fileMode := os.FileMode(0771)
30+
tarData := createTar(t, []tarEntry{
31+
{name: "base/", mode: os.ModeDir | 0777},
32+
{name: "base/test.txt", content: fileContent, mode: fileMode},
33+
})
34+
35+
tempDir := t.TempDir()
36+
dirName := "base"
37+
dirPath := filepath.Join(tempDir, dirName)
38+
buf := make([]byte, 1024)
39+
40+
if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf, true); err != nil {
41+
t.Fatalf("extractTarDirectory() error = %v", err)
42+
}
43+
44+
filePath := filepath.Join(dirPath, "test.txt")
45+
fi, err := os.Lstat(filePath)
46+
if err != nil {
47+
t.Fatalf("failed to stat file %s: %v", filePath, err)
48+
}
49+
50+
gotContent, err := os.ReadFile(filePath)
51+
if err != nil {
52+
t.Fatalf("failed to read file %s: %v", filePath, err)
53+
}
54+
if string(gotContent) != fileContent {
55+
t.Errorf("file content = %s, want %s", gotContent, fileContent)
56+
}
57+
58+
if fi.Mode() != fileMode {
59+
t.Errorf("file %q mode = %s, want %s", fi.Name(), fi.Mode(), fileMode)
60+
}
61+
}

0 commit comments

Comments
 (0)