Skip to content

Commit

Permalink
feat: option to have File store preserve permissions when extracting (#…
Browse files Browse the repository at this point in the history
…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 <sammy.abed@digitalasset.com>
  • Loading branch information
sammy-da authored Feb 26, 2025
1 parent cb6d75b commit d92df9d
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 9 deletions.
5 changes: 4 additions & 1 deletion content/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ type Store struct {
// value overrides the [AnnotationUnpack].
// Default value: false.
SkipUnpack bool
// PreservePermissions controls whether to preserve file permissions when unpacking,
// disregarding the active umask, similar to tar's `--preserve-permissions`
PreservePermissions bool

workingDir string // the working directory of the file store
closed int32 // if the store is closed - 0: false, 1: true.
Expand Down Expand Up @@ -499,7 +502,7 @@ func (s *Store) pushDir(name, target string, expected ocispec.Descriptor, conten
checksum := expected.Annotations[AnnotationDigest]
buf := bufPool.Get().(*[]byte)
defer bufPool.Put(buf)
if err := extractTarGzip(target, name, gzPath, checksum, *buf); err != nil {
if err := extractTarGzip(target, name, gzPath, checksum, *buf, s.PreservePermissions); err != nil {
return fmt.Errorf("failed to extract tar to %s: %w", target, err)
}
return nil
Expand Down
15 changes: 11 additions & 4 deletions content/file/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func tarDirectory(ctx context.Context, root, prefix string, w io.Writer, removeT

// extractTarGzip decompresses the gzip
// and extracts tar file to a directory specified by the `dir` parameter.
func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte) (err error) {
func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte, preservePermissions bool) (err error) {
fp, err := os.Open(gzPath)
if err != nil {
return err
Expand Down Expand Up @@ -144,7 +144,7 @@ func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte) (err
r = io.TeeReader(r, verifier)
}
}
if err := extractTarDirectory(dirPath, dirName, r, buf); err != nil {
if err := extractTarDirectory(dirPath, dirName, r, buf, preservePermissions); err != nil {
return err
}
if verifier != nil && !verifier.Verified() {
Expand All @@ -156,7 +156,7 @@ func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte) (err
// extractTarDirectory extracts tar file to a directory specified by the `dir`
// parameter. The file name prefix is ensured to be the string specified by the
// `prefix` parameter and is trimmed.
func extractTarDirectory(dirPath, dirName string, r io.Reader, buf []byte) error {
func extractTarDirectory(dirPath, dirName string, r io.Reader, buf []byte, preservePermissions bool) error {
tr := tar.NewReader(r)
for {
header, err := tr.Next()
Expand Down Expand Up @@ -214,7 +214,14 @@ func extractTarDirectory(dirPath, dirName string, r io.Reader, buf []byte) error
}

// Change access time and modification time if possible (error ignored)
os.Chtimes(filePath, header.AccessTime, header.ModTime)
_ = os.Chtimes(filePath, header.AccessTime, header.ModTime)

// Restore full mode bits
if preservePermissions && (header.Typeflag == tar.TypeReg || header.Typeflag == tar.TypeDir) {
if err := os.Chmod(filePath, os.FileMode(header.Mode)); err != nil {
return err
}
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions content/file/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func Test_ensureLinkPath(t *testing.T) {

func Test_extractTarGzip_Error(t *testing.T) {
t.Run("Non-existing file", func(t *testing.T) {
err := extractTarGzip("", "", "non-existing-file", "", nil)
err := extractTarGzip("", "", "non-existing-file", "", nil, false)
if err == nil {
t.Fatal("expected error, got nil")
}
Expand Down Expand Up @@ -300,7 +300,7 @@ func Test_extractTarDirectory(t *testing.T) {
dirPath := filepath.Join(tempDir, dirName)
buf := make([]byte, 1024)

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

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

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

if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf); err == nil {
if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf, false); err == nil {
t.Error("extractTarDirectory() error = nil, wantErr = true")
}
})
Expand Down
61 changes: 61 additions & 0 deletions content/file/utils_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build !windows

/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package file

import (
"bytes"
"os"
"path/filepath"
"testing"
)

func Test_extractTarDirectory_PreservePermissions(t *testing.T) {
fileContent := "hello world"
fileMode := os.FileMode(0771)
tarData := createTar(t, []tarEntry{
{name: "base/", mode: os.ModeDir | 0777},
{name: "base/test.txt", content: fileContent, mode: fileMode},
})

tempDir := t.TempDir()
dirName := "base"
dirPath := filepath.Join(tempDir, dirName)
buf := make([]byte, 1024)

if err := extractTarDirectory(dirPath, dirName, bytes.NewReader(tarData), buf, true); err != nil {
t.Fatalf("extractTarDirectory() error = %v", err)
}

filePath := filepath.Join(dirPath, "test.txt")
fi, err := os.Lstat(filePath)
if err != nil {
t.Fatalf("failed to stat file %s: %v", filePath, err)
}

gotContent, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("failed to read file %s: %v", filePath, err)
}
if string(gotContent) != fileContent {
t.Errorf("file content = %s, want %s", gotContent, fileContent)
}

if fi.Mode() != fileMode {
t.Errorf("file %q mode = %s, want %s", fi.Name(), fi.Mode(), fileMode)
}
}

0 comments on commit d92df9d

Please sign in to comment.