Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions hotline/file_transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"math"
"os"
"path"
"path/filepath"
"slices"
"strings"
"sync"
Expand Down Expand Up @@ -404,6 +405,15 @@ func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *Fil
// This notifies the server to send the next item header

basePathLen := len(fullPath)
walkRoot := fullPath + "/"
if target, err := fileStore.ReadLink(fullPath); err == nil {
if !filepath.IsAbs(target) {
target = filepath.Join(filepath.Dir(fullPath), target)
}
if targetInfo, err := fileStore.Stat(target); err == nil && targetInfo.IsDir() {
walkRoot = target
}
}

rLogger.Info("Start folder download", "path", fullPath)

Expand All @@ -413,25 +423,36 @@ func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *Fil
}

i := 0
err := fileStore.Walk(fullPath+"/", func(path string, info os.FileInfo, err error) error {
err := fileStore.Walk(walkRoot, func(walkPath string, info os.FileInfo, err error) error {
//s.Stats.DownloadCounter += 1
i += 1

if err != nil {
return err
}
displayPath := walkPath
if filepath.Clean(walkRoot) != filepath.Clean(fullPath+"/") {
relativePath, err := filepath.Rel(walkRoot, walkPath)
if err != nil {
return fmt.Errorf("map symlink download path: %w", err)
}
displayPath = fullPath + string(filepath.Separator)
if relativePath != "." {
displayPath = filepath.Join(fullPath, relativePath)
}
}

// skip dot files
if strings.HasPrefix(info.Name(), ".") {
return nil
}

hlFile, err := NewFile(fileStore, path, 0)
hlFile, err := NewFile(fileStore, walkPath, 0)
if err != nil {
return err
}

subPath := path[basePathLen+1:]
subPath := displayPath[basePathLen+1:]

if i == 1 {
return nil
Expand Down Expand Up @@ -496,14 +517,18 @@ func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *Fil
return fmt.Errorf("error sending flat file object: %w", err)
}

file, err := fileStore.Open(path)
file, err := fileStore.Open(walkPath)
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}

if _, err = io.Copy(rwc, io.TeeReader(file, fileTransfer.bytesSentCounter)); err != nil {
_ = file.Close()
return fmt.Errorf("error sending file: %w", err)
}
if err := file.Close(); err != nil {
return fmt.Errorf("error closing file: %w", err)
}

if nextAction[1] != 2 && hlFile.Ffo.FlatFileHeader.ForkCount[1] == 3 {
err = binary.Write(rwc, binary.BigEndian, hlFile.rsrcForkHeader())
Expand All @@ -517,8 +542,12 @@ func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *Fil
}

if _, err = io.Copy(rwc, io.TeeReader(rFile, fileTransfer.bytesSentCounter)); err != nil {
_ = rFile.Close()
return fmt.Errorf("error sending resource fork: %w", err)
}
if err := rFile.Close(); err != nil {
return fmt.Errorf("error closing resource fork: %w", err)
}
}

// Read the client's Next Action request. This is always 3, I think?
Expand Down
42 changes: 42 additions & 0 deletions hotline/file_transfer_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,19 @@ func runFolderDownloadClient(t *testing.T, conn net.Conn, nExpected int) []downl
return got
}

type symlinkRootFileStore struct {
*OSFileStore
linkName string
target string
}

func (s *symlinkRootFileStore) ReadLink(name string) (string, error) {
if filepath.Clean(name) == filepath.Clean(s.linkName) {
return s.target, nil
}
return s.OSFileStore.ReadLink(name)
}

func TestDownloadFolderHandler(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))

Expand Down Expand Up @@ -333,6 +346,35 @@ func TestDownloadFolderHandler(t *testing.T) {
assert.Equal(t, []byte("bbbbb"), byName[filepath.Join("sub", "b.txt")].data)
})

t.Run("streams files from a symlinked root", func(t *testing.T) {
dir := t.TempDir()
fileStore := &symlinkRootFileStore{OSFileStore: &OSFileStore{}}

root := filepath.Join(dir, "share")
fileStore.linkName = root
target := filepath.Join(dir, "actual-share")
fileStore.target = filepath.Base(target) // Symlink targets are often relative to their parent.
require.NoError(t, os.Mkdir(root, 0755))
require.NoError(t, os.Mkdir(target, 0755))
require.NoError(t, os.WriteFile(filepath.Join(target, "a.txt"), []byte("from target"), 0644))

serverConn, clientConn := net.Pipe()
require.NoError(t, clientConn.SetDeadline(time.Now().Add(10*time.Second)))

ft := &FileTransfer{bytesSentCounter: &WriteCounter{}}
errCh := make(chan error, 1)
go func() {
errCh <- DownloadFolderHandler(serverConn, root, ft, fileStore, logger, true)
_ = serverConn.Close()
}()

got := runFolderDownloadClient(t, clientConn, 1)
require.NoError(t, <-errCh)
require.Len(t, got, 1)
assert.Equal(t, "a.txt", got[0].name)
assert.Equal(t, []byte("from target"), got[0].data)
})

t.Run("honors a resume request for a file", func(t *testing.T) {
dir := t.TempDir()
fileStore := &OSFileStore{}
Expand Down
Loading