Record the real uid and gid of backed-up files #241
@@ -22,6 +22,14 @@ the tag exists and is exercised; what is left is merging `next` to
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-06: Made a backup record the real uid and gid of files,
|
||||||
|
directories and symlinks
|
||||||
|
([issue #216](https://git.eeqj.de/sneak/vaultik/issues/216)). The
|
||||||
|
scanner asked the stat result for `Uid()` and `Gid()` methods, which
|
||||||
|
`*syscall.Stat_t` does not have, so every entry was stored as `0:0`
|
||||||
|
and a restore as root gave every file to root. It now reads the
|
||||||
|
`Uid` and `Gid` fields of `*syscall.Stat_t`.
|
||||||
|
|
||||||
- 2026-10-06: Made a `file://` destination whose directory is missing
|
- 2026-10-06: Made a `file://` destination whose directory is missing
|
||||||
count as one that cannot be listed
|
count as one that cannot be listed
|
||||||
([issue #220](https://git.eeqj.de/sneak/vaultik/issues/220)). The file
|
([issue #220](https://git.eeqj.de/sneak/vaultik/issues/220)). The file
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
@@ -1142,12 +1143,9 @@ func (s *Scanner) buildSymlinkEntry(path string, info os.FileInfo) *database.Fil
|
|||||||
}
|
}
|
||||||
|
|
||||||
var uid, gid uint32
|
var uid, gid uint32
|
||||||
if stat, ok := info.Sys().(interface {
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||||
Uid() uint32
|
uid = stat.Uid
|
||||||
Gid() uint32
|
gid = stat.Gid
|
||||||
}); ok {
|
|
||||||
uid = stat.Uid()
|
|
||||||
gid = stat.Gid()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &database.File{
|
return &database.File{
|
||||||
@@ -1166,12 +1164,9 @@ func (s *Scanner) buildSymlinkEntry(path string, info os.FileInfo) *database.Fil
|
|||||||
// buildDirectoryEntry creates a File record for a directory.
|
// buildDirectoryEntry creates a File record for a directory.
|
||||||
func (s *Scanner) buildDirectoryEntry(path string, info os.FileInfo) *database.File {
|
func (s *Scanner) buildDirectoryEntry(path string, info os.FileInfo) *database.File {
|
||||||
var uid, gid uint32
|
var uid, gid uint32
|
||||||
if stat, ok := info.Sys().(interface {
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||||
Uid() uint32
|
uid = stat.Uid
|
||||||
Gid() uint32
|
gid = stat.Gid
|
||||||
}); ok {
|
|
||||||
uid = stat.Uid()
|
|
||||||
gid = stat.Gid()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &database.File{
|
return &database.File{
|
||||||
@@ -1204,16 +1199,10 @@ func (s *Scanner) recordNonRegularFile(ctx context.Context, ftp *FileToProcess)
|
|||||||
func (s *Scanner) checkFileInMemory(
|
func (s *Scanner) checkFileInMemory(
|
||||||
path string, info os.FileInfo, knownFiles map[string]*database.File,
|
path string, info os.FileInfo, knownFiles map[string]*database.File,
|
||||||
) (*database.File, bool) {
|
) (*database.File, bool) {
|
||||||
// Get file stats
|
|
||||||
stat, ok := info.Sys().(interface {
|
|
||||||
Uid() uint32
|
|
||||||
Gid() uint32
|
|
||||||
})
|
|
||||||
|
|
||||||
var uid, gid uint32
|
var uid, gid uint32
|
||||||
if ok {
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||||
uid = stat.Uid()
|
uid = stat.Uid
|
||||||
gid = stat.Gid()
|
gid = stat.Gid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check against in-memory map first to get existing ID if available
|
// Check against in-memory map first to get existing ID if available
|
||||||
|
|||||||
@@ -307,3 +307,80 @@ func TestScannerLargeFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestScannerRecordsOwnership backs up real files on disk and checks that
|
||||||
|
// the uid and gid of a file, a directory and a symlink are recorded.
|
||||||
|
// When the tests run as root both sides are 0, so only a run as another
|
||||||
|
// user can catch ownership recorded as 0.
|
||||||
|
func TestScannerRecordsOwnership(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
sourceDir := t.TempDir()
|
||||||
|
filePath := filepath.Join(sourceDir, "file.txt")
|
||||||
|
dirPath := filepath.Join(sourceDir, "subdir")
|
||||||
|
linkPath := filepath.Join(sourceDir, "link")
|
||||||
|
|
||||||
|
err := os.WriteFile(filePath, []byte("owned"), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Mkdir(dirPath, 0o700)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Symlink("file.txt", linkPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := database.NewTestDB()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create test database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := db.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to close database: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
repos := database.NewRepositories(db)
|
||||||
|
|
||||||
|
scanner := snapshot.NewScanner(snapshot.ScannerConfig{
|
||||||
|
FS: afero.NewOsFs(),
|
||||||
|
ChunkSize: int64(1024 * 16),
|
||||||
|
Repositories: repos,
|
||||||
|
MaxBlobSize: int64(1024 * 1024),
|
||||||
|
CompressionLevel: 3,
|
||||||
|
AgeRecipients: []string{testAgePublicKey},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
snapshotID := "test-snapshot-ownership"
|
||||||
|
|
||||||
|
createTestSnapshotRecord(ctx, t, repos, snapshotID)
|
||||||
|
|
||||||
|
_, err = scanner.Scan(ctx, sourceDir, snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("scan failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range []string{filePath, dirPath, linkPath} {
|
||||||
|
file, err := repos.Files.GetByPath(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get %s: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file == nil {
|
||||||
|
t.Fatalf("%s was not recorded", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(file.UID) != os.Getuid() || int(file.GID) != os.Getgid() {
|
||||||
|
t.Errorf("%s recorded as uid %d gid %d, want uid %d gid %d",
|
||||||
|
path, file.UID, file.GID, os.Getuid(), os.Getgid())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user