From 61f0cfe40df9e94530ebc290087a42e01c047ed7 Mon Sep 17 00:00:00 2001 From: EpicPlayerA10 Date: Fri, 5 Dec 2025 13:38:32 +0100 Subject: [PATCH 1/2] Handle symlinks when compressing/decompressing archive --- internal/ufs/fs_unix.go | 24 ++++++++++++++++++++++++ server/filesystem/archive.go | 2 +- server/filesystem/compress.go | 9 +++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/internal/ufs/fs_unix.go b/internal/ufs/fs_unix.go index dff36c9b8..15364da8e 100644 --- a/internal/ufs/fs_unix.go +++ b/internal/ufs/fs_unix.go @@ -272,6 +272,30 @@ func (fs *UnixFS) OpenFileat(dirfd int, name string, flag int, mode FileMode) (F return os.NewFile(uintptr(fd), name), nil } +// Readlinkat reads the destination of the named symbolic link. +// +// If the file is not a symbolic link, it will return an error. +// This is like Readlink but allows passing an existing directory file +// descriptor rather than needing to resolve one. +// +// If there is an error, it will be of type *PathError. +func (fs *UnixFS) Readlinkat(dirfd int, name string) (string, error) { + // Automatically guess the buffer size + for size := 128; ; size *= 2 { + buf := make([]byte, size) + n, err := unix.Readlinkat(dirfd, name, buf) + if err != nil { + return "", err + } + + if n < size { + return string(buf[:n]), nil + } + + // Continue if buffer size is too small + } +} + // ReadDir reads the named directory, // // returning all its directory entries sorted by filename. diff --git a/server/filesystem/archive.go b/server/filesystem/archive.go index 16ae7f9ed..dac5028e8 100644 --- a/server/filesystem/archive.go +++ b/server/filesystem/archive.go @@ -282,7 +282,7 @@ func (a *Archive) addToArchive(dirfd int, name, relative string, entry ufs.DirEn // the logs, but we're not going to stop the backup. There are far too many cases of // symlinks causing all sorts of unnecessary pain in this process. Sucks to suck if // it doesn't work. - target, err = os.Readlink(s.Name()) + target, err = a.Filesystem.unixFS.Readlinkat(dirfd, name) if err != nil { // Ignore the not exist errors specifically, since there is nothing important about that. if !os.IsNotExist(err) { diff --git a/server/filesystem/compress.go b/server/filesystem/compress.go index f2775cb31..c47da8fbb 100644 --- a/server/filesystem/compress.go +++ b/server/filesystem/compress.go @@ -266,6 +266,15 @@ func (fs *Filesystem) extractStream(ctx context.Context, opts extractStreamOptio if err := fs.IsIgnored(p); err != nil { return nil } + + // Handle symlinks + if f.Mode()&iofs.ModeSymlink != 0 { + if err := fs.Symlink(f.LinkTarget, p); err != nil { + return wrapError(err, opts.FileName) + } + return nil + } + r, err := f.Open() if err != nil { return err From 0df74add96ac9a7040ef053aacd55fafdc11fff6 Mon Sep 17 00:00:00 2001 From: EpicPlayerA10 Date: Fri, 5 Dec 2025 14:00:45 +0100 Subject: [PATCH 2/2] also handle symlinks in restoring backups --- server/backup.go | 12 +++++++++++- server/backup/backup.go | 3 +-- server/backup/backup_local.go | 2 +- server/backup/backup_s3.go | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/server/backup.go b/server/backup.go index 1568290d5..d3bff128a 100644 --- a/server/backup.go +++ b/server/backup.go @@ -9,6 +9,7 @@ import ( "emperror.dev/errors" "github.com/apex/log" "github.com/docker/docker/client" + "github.com/mholt/archives" "github.com/pterodactyl/wings/environment" "github.com/pterodactyl/wings/remote" @@ -151,9 +152,18 @@ func (s *Server) RestoreBackup(b backup.BackupInterface, reader io.ReadCloser) ( // Attempt to restore the backup to the server by running through each entry // in the file one at a time and writing them to the disk. s.Log().Debug("starting file writing process for backup restoration") - err = b.Restore(s.Context(), reader, func(file string, info fs.FileInfo, r io.ReadCloser) error { + err = b.Restore(s.Context(), reader, func(file string, info archives.FileInfo, r io.ReadCloser) error { defer r.Close() s.Events().Publish(DaemonMessageEvent, "(restoring): "+file) + + // Handle symlinks + if info.Mode()&fs.ModeSymlink != 0 { + if err := s.Filesystem().Symlink(info.LinkTarget, file); err != nil { + return err + } + return nil + } + // TODO: since this will be called a lot, it may be worth adding an optimized // Write with Chtimes method to the UnixFS that is able to re-use the // same dirfd and file name. diff --git a/server/backup/backup.go b/server/backup/backup.go index 01e73d0dd..70617f4ee 100644 --- a/server/backup/backup.go +++ b/server/backup/backup.go @@ -5,7 +5,6 @@ import ( "crypto/sha1" "encoding/hex" "io" - "io/fs" "os" "path" @@ -34,7 +33,7 @@ const ( // RestoreCallback is a generic restoration callback that exists for both local // and remote backups allowing the files to be restored. -type RestoreCallback func(file string, info fs.FileInfo, r io.ReadCloser) error +type RestoreCallback func(file string, info archives.FileInfo, r io.ReadCloser) error // noinspection GoNameStartsWithPackageName type BackupInterface interface { diff --git a/server/backup/backup_local.go b/server/backup/backup_local.go index 2351416f8..9495e8978 100644 --- a/server/backup/backup_local.go +++ b/server/backup/backup_local.go @@ -100,7 +100,7 @@ func (b *LocalBackup) Restore(ctx context.Context, _ io.Reader, callback Restore } defer r.Close() - return callback(f.NameInArchive, f.FileInfo, r) + return callback(f.NameInArchive, f, r) }); err != nil { return err } diff --git a/server/backup/backup_s3.go b/server/backup/backup_s3.go index e281ca70a..b2d76b33e 100644 --- a/server/backup/backup_s3.go +++ b/server/backup/backup_s3.go @@ -100,7 +100,7 @@ func (s *S3Backup) Restore(ctx context.Context, r io.Reader, callback RestoreCal } defer r.Close() - return callback(f.NameInArchive, f.FileInfo, r) + return callback(f.NameInArchive, f, r) }); err != nil { return err }