From 5cc6ed27177304f743ebefc79fa3a17481bf8c98 Mon Sep 17 00:00:00 2001 From: Jeff Halter <868228+jhalter@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:33:04 -0700 Subject: Ensure temporary upload files are closed before rename This seems to be important on Windows! See #161 --- hotline/file_transfer.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) (limited to 'hotline/file_transfer.go') diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index 626cfff..f09e995 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -318,15 +318,16 @@ func UploadHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTransfe if err == nil { return fmt.Errorf("existing file found: %s", fullPath) } - if errors.Is(err, fs.ErrNotExist) { - // If not found, open or create a new .incomplete file - file, err = os.OpenFile(fullPath+IncompleteFileSuffix, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - if err != nil { - return err - } + + if !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("check file existence: %w", err) } - defer file.Close() + // If not found, open or create a new .incomplete file + file, err = os.OpenFile(fullPath+IncompleteFileSuffix, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("open temp file for uploade: %w", err) + } f, err := NewFileWrapper(fileStore, fullPath, 0) if err != nil { @@ -350,9 +351,16 @@ func UploadHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTransfe } if err := receiveFile(rwc, file, rForkWriter, iForkWriter, fileTransfer.bytesSentCounter); err != nil { + _ = file.Close() // Close on error return fmt.Errorf("receive file: %v", err) } + // Close the file before attempting to rename it. + if err := file.Close(); err != nil { + return fmt.Errorf("close file: %v", err) + } + + // Rename the temporary upload file to the final file name. if err := fileStore.Rename(fullPath+".incomplete", fullPath); err != nil { return fmt.Errorf("rename incomplete file: %v", err) } @@ -665,6 +673,12 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT return err } + // Close the file before attempting to rename it. + if err := incWriter.Close(); err != nil { + return fmt.Errorf("close file: %v", err) + } + + // Rename the temporary upload file to the final file name. if err := os.Rename(filePath+".incomplete", filePath); err != nil { return err } -- cgit From 55b8e77c409761639e95168c77dc22c13e858b6b Mon Sep 17 00:00:00 2001 From: Jeff Halter <868228+jhalter@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:01:22 -0700 Subject: Replace filepath.Join with path.Join for Windows compatibility Replace all instances of filepath.Join with path.Join across the codebase to improve Windows compatibility following the guidance from https://github.com/golang/go/issues/44305. Key changes: - Replaced filepath.Join with path.Join in 14 files - Updated import statements appropriately - Resolved variable shadowing issues where function parameters named 'path' were conflicting with the path package - Maintained filepath imports where needed for OS-specific functions like filepath.Walk, filepath.IsAbs, filepath.Dir, and filepath.Base All tests pass, confirming the changes maintain functionality while improving cross-platform compatibility. --- cmd/mobius-hotline-server/main.go | 7 +++---- cmd/mobius-hotline-server/main_test.go | 16 ++++++++-------- hotline/file_path.go | 8 ++++---- hotline/file_transfer.go | 13 +++++++------ internal/mobius/account_manager.go | 12 ++++++------ internal/mobius/ban.go | 4 ++-- internal/mobius/ban_test.go | 8 ++++---- internal/mobius/threaded_news_test.go | 4 ++-- internal/mobius/transaction_handlers.go | 3 +-- 9 files changed, 37 insertions(+), 38 deletions(-) (limited to 'hotline/file_transfer.go') diff --git a/cmd/mobius-hotline-server/main.go b/cmd/mobius-hotline-server/main.go index e230771..afad4d1 100644 --- a/cmd/mobius-hotline-server/main.go +++ b/cmd/mobius-hotline-server/main.go @@ -10,7 +10,6 @@ import ( "os" "os/signal" "path" - "path/filepath" "syscall" "github.com/jhalter/mobius/hotline" @@ -108,7 +107,7 @@ func main() { os.Exit(1) } - srv.AccountManager, err = mobius.NewYAMLAccountManager(filepath.Join(*configDir, "Users/")) + srv.AccountManager, err = mobius.NewYAMLAccountManager(path.Join(*configDir, "Users/")) if err != nil { slogger.Error(fmt.Sprintf("Error loading accounts: %v", err)) os.Exit(1) @@ -120,7 +119,7 @@ func main() { os.Exit(1) } - bannerPath := filepath.Join(*configDir, config.BannerFile) + bannerPath := path.Join(*configDir, config.BannerFile) srv.Banner, err = os.ReadFile(bannerPath) if err != nil { slogger.Error(fmt.Sprintf("Error loading accounts: %v", err)) @@ -146,7 +145,7 @@ func main() { } // Let's try to reload the banner - bannerPath := filepath.Join(*configDir, config.BannerFile) + bannerPath := path.Join(*configDir, config.BannerFile) srv.Banner, err = os.ReadFile(bannerPath) if err != nil { slogger.Error(fmt.Sprintf("Error reloading banner: %v", err)) diff --git a/cmd/mobius-hotline-server/main_test.go b/cmd/mobius-hotline-server/main_test.go index 17a2cb4..ed63065 100644 --- a/cmd/mobius-hotline-server/main_test.go +++ b/cmd/mobius-hotline-server/main_test.go @@ -2,7 +2,7 @@ package main import ( "os" - "path/filepath" + "path" "testing" "github.com/jhalter/mobius/internal/mobius" @@ -30,7 +30,7 @@ func TestCopyDir(t *testing.T) { } for _, expectedFile := range expectedFiles { - fullPath := filepath.Join(dstDir, expectedFile) + fullPath := path.Join(dstDir, expectedFile) assert.FileExists(t, fullPath, "Expected file %s to exist", expectedFile) // Verify file is not empty @@ -46,7 +46,7 @@ func TestCopyDir(t *testing.T) { } for _, expectedDir := range expectedDirs { - fullPath := filepath.Join(dstDir, expectedDir) + fullPath := path.Join(dstDir, expectedDir) info, err := os.Stat(fullPath) require.NoError(t, err) assert.True(t, info.IsDir(), "Expected %s to be a directory", expectedDir) @@ -69,11 +69,11 @@ func TestCopyDirRecursive(t *testing.T) { require.NoError(t, err) // Verify nested structure is copied correctly - nestedPath := filepath.Join(dstDir, "Users", "admin.yaml") + nestedPath := path.Join(dstDir, "Users", "admin.yaml") assert.FileExists(t, nestedPath) // Verify nested Files directory - filesDir := filepath.Join(dstDir, "Files") + filesDir := path.Join(dstDir, "Files") info, err := os.Stat(filesDir) require.NoError(t, err) assert.True(t, info.IsDir()) @@ -81,7 +81,7 @@ func TestCopyDirRecursive(t *testing.T) { func TestCopyFile(t *testing.T) { dstDir := t.TempDir() - dstFile := filepath.Join(dstDir, "copied.yaml") + dstFile := path.Join(dstDir, "copied.yaml") // Copy a single file from embedded config err := copyFile("mobius/config/config.yaml", dstFile) @@ -99,7 +99,7 @@ func TestCopyFile(t *testing.T) { func TestCopyFileErrors(t *testing.T) { t.Run("source file does not exist", func(t *testing.T) { dstDir := t.TempDir() - err := copyFile("nonexistent.txt", filepath.Join(dstDir, "dest.txt")) + err := copyFile("nonexistent.txt", path.Join(dstDir, "dest.txt")) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to open source file") }) @@ -118,7 +118,7 @@ func TestCopyDirPermissions(t *testing.T) { require.NoError(t, err) // Check directory permissions - info, err := os.Stat(filepath.Join(dstDir, "Users")) + info, err := os.Stat(path.Join(dstDir, "Users")) require.NoError(t, err) assert.True(t, info.IsDir()) diff --git a/hotline/file_path.go b/hotline/file_path.go index f4a27cc..a3a13f1 100644 --- a/hotline/file_path.go +++ b/hotline/file_path.go @@ -7,7 +7,7 @@ import ( "errors" "fmt" "io" - "path/filepath" + "path" "strings" ) @@ -112,13 +112,13 @@ func ReadPath(fileRoot string, filePath, fileName []byte) (fullPath string, err var subPath string for _, pathItem := range fp.Items { - subPath = filepath.Join("/", subPath, string(pathItem.Name)) + subPath = path.Join("/", subPath, string(pathItem.Name)) } - fullPath = filepath.Join( + fullPath = path.Join( fileRoot, subPath, - filepath.Join("/", string(fileName)), + path.Join("/", string(fileName)), ) fullPath, err = txtDecoder.String(fullPath) if err != nil { diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index f09e995..0ddac0b 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -11,6 +11,7 @@ import ( "log/slog" "math" "os" + "path" "path/filepath" "slices" "strings" @@ -200,7 +201,7 @@ func (fu *folderUpload) FormattedPath() string { pathData = pathData[3+segLen:] } - return filepath.Join(pathSegments...) + return path.Join(pathSegments...) } type FileHeader struct { @@ -566,8 +567,8 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT } if fu.IsFolder == [2]byte{0, 1} { - if _, err := os.Stat(filepath.Join(fullPath, fu.FormattedPath())); os.IsNotExist(err) { - if err := os.Mkdir(filepath.Join(fullPath, fu.FormattedPath()), 0777); err != nil { + if _, err := os.Stat(path.Join(fullPath, fu.FormattedPath())); os.IsNotExist(err) { + if err := os.Mkdir(path.Join(fullPath, fu.FormattedPath()), 0777); err != nil { return err } } @@ -580,7 +581,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT nextAction := DlFldrActionSendFile // Check if we have the full file already. If so, send dlFldrAction_NextFile to client to skip. - _, err := os.Stat(filepath.Join(fullPath, fu.FormattedPath())) + _, err := os.Stat(path.Join(fullPath, fu.FormattedPath())) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } @@ -589,7 +590,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT } // Check if we have a partial file already. If so, send dlFldrAction_ResumeFile to client to resume upload. - incompleteFile, err := os.Stat(filepath.Join(fullPath, fu.FormattedPath()+IncompleteFileSuffix)) + incompleteFile, err := os.Stat(path.Join(fullPath, fu.FormattedPath()+IncompleteFileSuffix)) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } @@ -642,7 +643,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT return err } - filePath := filepath.Join(fullPath, fu.FormattedPath()) + filePath := path.Join(fullPath, fu.FormattedPath()) hlFile, err := NewFileWrapper(fileStore, filePath, 0) if err != nil { diff --git a/internal/mobius/account_manager.go b/internal/mobius/account_manager.go index 72984b7..2558d3a 100644 --- a/internal/mobius/account_manager.go +++ b/internal/mobius/account_manager.go @@ -37,7 +37,7 @@ func NewYAMLAccountManager(accountDir string) (*YAMLAccountManager, error) { accounts: make(map[string]hotline.Account), } - matches, err := filepath.Glob(filepath.Join(accountDir, "*.yaml")) + matches, err := filepath.Glob(path.Join(accountDir, "*.yaml")) if err != nil { return nil, fmt.Errorf("list account files: %w", err) } @@ -77,7 +77,7 @@ func (am *YAMLAccountManager) Create(account hotline.Account) error { // Create account file, returning an error if one already exists. file, err := os.OpenFile( - filepath.Join(am.accountDir, path.Join("/", account.Login+".yaml")), + path.Join(am.accountDir, path.Join("/", account.Login+".yaml")), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644, ) if err != nil { @@ -107,8 +107,8 @@ func (am *YAMLAccountManager) Update(account hotline.Account, newLogin string) e // If the login has changed, rename the account file. if account.Login != newLogin { err := os.Rename( - filepath.Join(am.accountDir, path.Join("/", account.Login)+".yaml"), - filepath.Join(am.accountDir, path.Join("/", newLogin)+".yaml"), + path.Join(am.accountDir, path.Join("/", account.Login)+".yaml"), + path.Join(am.accountDir, path.Join("/", newLogin)+".yaml"), ) if err != nil { return fmt.Errorf("error renaming account file: %w", err) @@ -124,7 +124,7 @@ func (am *YAMLAccountManager) Update(account hotline.Account, newLogin string) e return err } - if err := os.WriteFile(filepath.Join(am.accountDir, newLogin+".yaml"), out, 0644); err != nil { + if err := os.WriteFile(path.Join(am.accountDir, newLogin+".yaml"), out, 0644); err != nil { return fmt.Errorf("error writing account file: %w", err) } @@ -161,7 +161,7 @@ func (am *YAMLAccountManager) Delete(login string) error { am.mu.Lock() defer am.mu.Unlock() - err := os.Remove(filepath.Join(am.accountDir, path.Join("/", login+".yaml"))) + err := os.Remove(path.Join(am.accountDir, path.Join("/", login+".yaml"))) if err != nil { return fmt.Errorf("delete account file: %v", err) } diff --git a/internal/mobius/ban.go b/internal/mobius/ban.go index e73b14a..781052b 100644 --- a/internal/mobius/ban.go +++ b/internal/mobius/ban.go @@ -4,7 +4,7 @@ import ( "fmt" "gopkg.in/yaml.v3" "os" - "path/filepath" + "path" "sync" "time" ) @@ -64,7 +64,7 @@ func (bf *BanFile) Add(ip string, until *time.Time) error { return fmt.Errorf("marshal yaml: %v", err) } - err = os.WriteFile(filepath.Join(bf.filePath), out, 0644) + err = os.WriteFile(path.Join(bf.filePath), out, 0644) if err != nil { return fmt.Errorf("write file: %v", err) } diff --git a/internal/mobius/ban_test.go b/internal/mobius/ban_test.go index f03f214..1bf68a4 100644 --- a/internal/mobius/ban_test.go +++ b/internal/mobius/ban_test.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/stretchr/testify/assert" "os" - "path/filepath" + "path" "sync" "testing" "time" @@ -26,9 +26,9 @@ func TestNewBanFile(t *testing.T) { }{ { name: "Valid path with valid content", - args: args{path: filepath.Join(cwd, "test", "config", "Banlist.yaml")}, + args: args{path: path.Join(cwd, "test", "config", "Banlist.yaml")}, want: &BanFile{ - filePath: filepath.Join(cwd, "test", "config", "Banlist.yaml"), + filePath: path.Join(cwd, "test", "config", "Banlist.yaml"), banList: map[string]*time.Time{"192.168.86.29": &testTime}, }, wantErr: assert.NoError, @@ -55,7 +55,7 @@ func TestAdd(t *testing.T) { defer os.RemoveAll(tmpDir) // Clean up the temporary directory. // Path to the temporary ban file. - tmpFilePath := filepath.Join(tmpDir, "banfile.yaml") + tmpFilePath := path.Join(tmpDir, "banfile.yaml") // Initialize BanFile. bf := &BanFile{ diff --git a/internal/mobius/threaded_news_test.go b/internal/mobius/threaded_news_test.go index dd06a28..7f5cdaa 100644 --- a/internal/mobius/threaded_news_test.go +++ b/internal/mobius/threaded_news_test.go @@ -5,7 +5,7 @@ import ( "github.com/jhalter/mobius/hotline" "github.com/stretchr/testify/assert" "os" - "path/filepath" + "path" "sync" "testing" ) @@ -164,7 +164,7 @@ func TestThreadedNewsYAML_CreateGrouping(t *testing.T) { defer os.RemoveAll(tmpDir) // Clean up the temporary directory. // Path to the temporary ban file. - tmpFilePath := filepath.Join(tmpDir, "ThreadedNews.yaml") + tmpFilePath := path.Join(tmpDir, "ThreadedNews.yaml") type fields struct { ThreadedNews hotline.ThreadedNews diff --git a/internal/mobius/transaction_handlers.go b/internal/mobius/transaction_handlers.go index c03704a..6553887 100644 --- a/internal/mobius/transaction_handlers.go +++ b/internal/mobius/transaction_handlers.go @@ -10,7 +10,6 @@ import ( "math/big" "os" "path" - "path/filepath" "strings" "time" @@ -453,7 +452,7 @@ func HandleNewFolder(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotl } for _, pathItem := range newFp.Items { - subPath = filepath.Join("/", subPath, string(pathItem.Name)) + subPath = path.Join("/", subPath, string(pathItem.Name)) } } newFolderPath := path.Join(cc.FileRoot(), subPath, folderName) -- cgit From 83430dba76359f3b84a50051dd3fffcbbef90c18 Mon Sep 17 00:00:00 2001 From: Jeff Halter <868228+jhalter@users.noreply.github.com> Date: Thu, 26 Jun 2025 17:31:07 -0700 Subject: Refactor FormattedPath to use scanner interface and add comprehensive tests - Replace manual byte slicing with bufio.Scanner for safer parsing - Add pathSegmentScanner implementing bufio.SplitFunc pattern - Add comprehensive table tests covering edge cases and special characters - Improve code safety with proper bounds checking - Follow established codebase patterns for binary data parsing --- hotline/file_transfer.go | 39 +++++++++++++++---- hotline/file_transfer_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 7 deletions(-) (limited to 'hotline/file_transfer.go') diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index 0ddac0b..701259a 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -2,6 +2,7 @@ package hotline import ( "bufio" + "bytes" "crypto/rand" "encoding/binary" "errors" @@ -188,17 +189,41 @@ type folderUpload struct { // return n + 6, nil //} +// pathSegmentScanner implements bufio.SplitFunc for parsing path segments +func pathSegmentScanner(data []byte, _ bool) (advance int, token []byte, err error) { + if len(data) < 3 { + return 0, nil, nil + } + + segLen := int(data[2]) + totalLen := 3 + segLen + + if len(data) < totalLen { + return 0, nil, nil + } + + return totalLen, data[0:totalLen], nil +} + func (fu *folderUpload) FormattedPath() string { pathItemLen := binary.BigEndian.Uint16(fu.PathItemCount[:]) - var pathSegments []string - pathData := fu.FileNamePath + if pathItemLen == 0 { + return "" + } - // TODO: implement scanner interface instead? - for i := uint16(0); i < pathItemLen; i++ { - segLen := pathData[2] - pathSegments = append(pathSegments, string(pathData[3:3+segLen])) - pathData = pathData[3+segLen:] + var pathSegments []string + scanner := bufio.NewScanner(bytes.NewReader(fu.FileNamePath)) + scanner.Split(pathSegmentScanner) + + for scanner.Scan() && len(pathSegments) < int(pathItemLen) { + segmentData := scanner.Bytes() + if len(segmentData) >= 3 { + segLen := int(segmentData[2]) + if len(segmentData) >= 3+segLen { + pathSegments = append(pathSegments, string(segmentData[3:3+segLen])) + } + } } return path.Join(pathSegments...) diff --git a/hotline/file_transfer_test.go b/hotline/file_transfer_test.go index 0b549f8..ba29910 100644 --- a/hotline/file_transfer_test.go +++ b/hotline/file_transfer_test.go @@ -175,3 +175,94 @@ func TestFileHeader_Payload(t *testing.T) { }) } } + +func Test_folderUpload_FormattedPath(t *testing.T) { + tests := []struct { + name string + pathItemCount [2]byte + fileNamePath []byte + want string + }{ + { + name: "empty path", + pathItemCount: [2]byte{0x00, 0x00}, + fileNamePath: []byte{}, + want: "", + }, + { + name: "single path segment", + pathItemCount: [2]byte{0x00, 0x01}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x03, // segment length + 0x66, 0x6f, 0x6f, // "foo" + }, + want: "foo", + }, + { + name: "multiple path segments", + pathItemCount: [2]byte{0x00, 0x03}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x04, // segment length + 0x68, 0x6f, 0x6d, 0x65, // "home" + 0x00, 0x00, // path separator + 0x04, // segment length + 0x75, 0x73, 0x65, 0x72, // "user" + 0x00, 0x00, // path separator + 0x09, // segment length + 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, // "documents" + }, + want: "home/user/documents", + }, + { + name: "path with spaces", + pathItemCount: [2]byte{0x00, 0x02}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x07, // segment length + 0x4d, 0x79, 0x20, 0x46, 0x69, 0x6c, 0x65, // "My File" + 0x00, 0x00, // path separator + 0x0d, // segment length (13 bytes) + 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6e, 0x74, 0x2e, 0x74, 0x78, 0x74, // "Important.txt" + }, + want: "My File/Important.txt", + }, + { + name: "single character segments", + pathItemCount: [2]byte{0x00, 0x03}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x01, // segment length + 0x61, // "a" + 0x00, 0x00, // path separator + 0x01, // segment length + 0x62, // "b" + 0x00, 0x00, // path separator + 0x01, // segment length + 0x63, // "c" + }, + want: "a/b/c", + }, + { + name: "path with special characters", + pathItemCount: [2]byte{0x00, 0x01}, + fileNamePath: []byte{ + 0x00, 0x00, // path separator + 0x08, // segment length + 0x74, 0x65, 0x73, 0x74, 0x40, 0x24, 0x25, 0x26, // "test@$%&" + }, + want: "test@$%&", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fu := &folderUpload{ + PathItemCount: tt.pathItemCount, + FileNamePath: tt.fileNamePath, + } + got := fu.FormattedPath() + assert.Equal(t, tt.want, got) + }) + } +} -- cgit From e61b3160bc8936b7ffb0af5c89b13742a7081826 Mon Sep 17 00:00:00 2001 From: Jeff Halter <868228+jhalter@users.noreply.github.com> Date: Sun, 6 Jul 2025 16:02:53 -0700 Subject: Add ForkType type alias for improved type safety - Replace [4]byte with ForkType in ForkInfoList struct - Update constants ForkTypeDATA and ForkTypeMACR to use ForkType - Add String() method to ForkType for better debugging - Improve consistency with existing type patterns (FieldType, TranType) - Clean up unused code and improve documentation --- hotline/file_resume_data.go | 57 +++++++++++++++++++-------------------------- hotline/file_transfer.go | 27 +-------------------- hotline/file_wrapper.go | 2 +- 3 files changed, 26 insertions(+), 60 deletions(-) (limited to 'hotline/file_transfer.go') diff --git a/hotline/file_resume_data.go b/hotline/file_resume_data.go index 5dfd2d8..80f5fe5 100644 --- a/hotline/file_resume_data.go +++ b/hotline/file_resume_data.go @@ -3,38 +3,54 @@ package hotline import ( "bytes" "encoding/binary" + "fmt" ) // FileResumeData is sent when a client or server would like to resume a transfer from an offset type FileResumeData struct { Format [4]byte // "RFLT" Version [2]byte // Always 1 - RSVD [34]byte // Unused + RSVD [34]byte // Present in the Hotline protocol docs, but unused. Left here for documentation purposes. ForkCount [2]byte // Length of ForkInfoList. Either 2 or 3 depending on whether file has a resource fork ForkInfoList []ForkInfoList +} + +// ForkType represents a 4-byte fork type identifier +type ForkType [4]byte - //readOffset int // TODO +// String returns a string representation of the fork type +func (ft ForkType) String() string { + return fmt.Sprintf("%c%c%c%c", ft[0], ft[1], ft[2], ft[3]) } type ForkInfoList struct { - Fork [4]byte // "DATA" or "MACR" - DataSize [4]byte // offset from which to resume the transfer of data - RSVDA [4]byte // Unused - RSVDB [4]byte // Unused + Fork ForkType // "DATA" or "MACR" + DataSize [4]byte // offset from which to resume the transfer of data + RSVDA [4]byte // Present in the Hotline protocol docs, but unused. Left here for documentation purposes. + RSVDB [4]byte // Present in the Hotline protocol docs, but unused. Left here for documentation purposes. } +var ( + ForkTypeDATA = ForkType{0x44, 0x41, 0x54, 0x41} // DATA: Data fork + ForkTypeMACR = ForkType{0x4d, 0x41, 0x43, 0x52} // MACR: Mac resource fork +) + func NewForkInfoList(b []byte) *ForkInfoList { return &ForkInfoList{ - Fork: [4]byte{0x44, 0x41, 0x54, 0x41}, + Fork: ForkTypeDATA, DataSize: [4]byte{b[0], b[1], b[2], b[3]}, RSVDA: [4]byte{}, RSVDB: [4]byte{}, } } +var ( + FormatRFLT = [4]byte{0x52, 0x46, 0x4C, 0x54} // File resume format: "RFLT" (?) +) + func NewFileResumeData(list []ForkInfoList) *FileResumeData { return &FileResumeData{ - Format: [4]byte{0x52, 0x46, 0x4C, 0x54}, // RFLT + Format: FormatRFLT, Version: [2]byte{0, 1}, RSVD: [34]byte{}, ForkCount: [2]byte{0, uint8(len(list))}, @@ -42,31 +58,6 @@ func NewFileResumeData(list []ForkInfoList) *FileResumeData { } } -// -//func (frd *FileResumeData) Read(p []byte) (int, error) { -// buf := slices.Concat( -// frd.Format[:], -// frd.Version[:], -// frd.RSVD[:], -// frd.ForkCount[:], -// ) -// for _, fil := range frd.ForkInfoList { -// buf = append(buf, fil...) -// _ = binary.Write(&buf, binary.LittleEndian, fil) -// } -// -// var buf bytes.Buffer -// _ = binary.Write(&buf, binary.LittleEndian, frd.Format) -// _ = binary.Write(&buf, binary.LittleEndian, frd.Version) -// _ = binary.Write(&buf, binary.LittleEndian, frd.RSVD) -// _ = binary.Write(&buf, binary.LittleEndian, frd.ForkCount) -// for _, fil := range frd.ForkInfoList { -// _ = binary.Write(&buf, binary.LittleEndian, fil) -// } -// -// return buf.Bytes(), nil -//} - func (frd *FileResumeData) BinaryMarshal() ([]byte, error) { var buf bytes.Buffer _ = binary.Write(&buf, binary.LittleEndian, frd.Format) diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index 701259a..521b963 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -172,23 +172,6 @@ type folderUpload struct { FileNamePath []byte } -//func (fu *folderUpload) Write(p []byte) (int, error) { -// if len(p) < 7 { -// return 0, errors.New("buflen too short") -// } -// copy(fu.DataSize[:], p[0:2]) -// copy(fu.IsFolder[:], p[2:4]) -// copy(fu.PathItemCount[:], p[4:6]) -// -// fu.FileNamePath = make([]byte, binary.BigEndian.Uint16(fu.DataSize[:])-4) // -4 to subtract the path separator bytes TODO: wat -// n, err := io.ReadFull(rwc, fu.FileNamePath) -// if err != nil { -// return 0, err -// } -// -// return n + 6, nil -//} - // pathSegmentScanner implements bufio.SplitFunc for parsing path segments func pathSegmentScanner(data []byte, _ bool) (advance int, token []byte, err error) { if len(data) < 3 { @@ -269,12 +252,6 @@ func (fh *FileHeader) Read(p []byte) (int, error) { } func DownloadHandler(w io.Writer, fullPath string, fileTransfer *FileTransfer, fs FileStore, rLogger *slog.Logger, preserveForks bool) error { - //s.Stats.DownloadCounter += 1 - //s.Stats.DownloadsInProgress += 1 - //defer func() { - // s.Stats.DownloadsInProgress -= 1 - //}() - var dataOffset int64 if fileTransfer.FileResumeData != nil { dataOffset = int64(binary.BigEndian.Uint32(fileTransfer.FileResumeData.ForkInfoList[0].DataSize[:])) @@ -520,7 +497,6 @@ func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *Fil return fmt.Errorf("error opening file: %w", err) } - // wr := bufio.NewWriterSize(rwc, 1460) if _, err = io.Copy(rwc, io.TeeReader(file, fileTransfer.bytesSentCounter)); err != nil { return fmt.Errorf("error sending file: %w", err) } @@ -576,7 +552,6 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT //s.Stats.UploadCounter += 1 var fu folderUpload - // TODO: implement io.Writer on folderUpload and replace this if _, err := io.ReadFull(rwc, fu.DataSize[:]); err != nil { return err } @@ -586,7 +561,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT if _, err := io.ReadFull(rwc, fu.PathItemCount[:]); err != nil { return err } - fu.FileNamePath = make([]byte, binary.BigEndian.Uint16(fu.DataSize[:])-4) // -4 to subtract the path separator bytes TODO: wat + fu.FileNamePath = make([]byte, binary.BigEndian.Uint16(fu.DataSize[:])-4) // -4 to subtract the length of the DataSize and IsFolder fields if _, err := io.ReadFull(rwc, fu.FileNamePath); err != nil { return err } diff --git a/hotline/file_wrapper.go b/hotline/file_wrapper.go index b13e0dc..b6aff94 100644 --- a/hotline/file_wrapper.go +++ b/hotline/file_wrapper.go @@ -85,7 +85,7 @@ func (f *fileWrapper) rsrcForkSize() (s [4]byte) { func (f *fileWrapper) rsrcForkHeader() FlatFileForkHeader { return FlatFileForkHeader{ - ForkType: [4]byte{0x4D, 0x41, 0x43, 0x52}, // "MACR" + ForkType: ForkTypeMACR, DataSize: f.rsrcForkSize(), } } -- cgit From 1b2df8a630bfc83304ef90610bdf7ebe91d4e555 Mon Sep 17 00:00:00 2001 From: Jeff Halter <868228+jhalter@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:36:06 -0800 Subject: Fix error when downloading incomplete files --- hotline/file_transfer.go | 16 ++++++++-------- hotline/file_wrapper.go | 8 ++++++-- hotline/files.go | 4 ++-- internal/mobius/transaction_handlers.go | 15 +++++++-------- 4 files changed, 23 insertions(+), 20 deletions(-) (limited to 'hotline/file_transfer.go') diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index 521b963..e8acbb3 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -257,7 +257,7 @@ func DownloadHandler(w io.Writer, fullPath string, fileTransfer *FileTransfer, f dataOffset = int64(binary.BigEndian.Uint32(fileTransfer.FileResumeData.ForkInfoList[0].DataSize[:])) } - fw, err := NewFileWrapper(fs, fullPath, 0) + hlFile, err := NewFile(fs, fullPath, 0) if err != nil { return fmt.Errorf("reading file header: %v", err) } @@ -267,12 +267,12 @@ func DownloadHandler(w io.Writer, fullPath string, fileTransfer *FileTransfer, f // If file transfer options are included, that means this is a "quick preview" request. In this case skip sending // the flat file info and proceed directly to sending the file data. if fileTransfer.Options == nil { - if _, err = io.Copy(w, fw.Ffo); err != nil { + if _, err = io.Copy(w, hlFile.Ffo); err != nil { return fmt.Errorf("send flat file object: %v", err) } } - file, err := fw.dataForkReader() + file, err := hlFile.dataForkReader() if err != nil { return fmt.Errorf("open data fork reader: %v", err) } @@ -288,13 +288,13 @@ func DownloadHandler(w io.Writer, fullPath string, fileTransfer *FileTransfer, f // If the client requested to resume transfer, do not send the resource fork header. if fileTransfer.FileResumeData == nil { - err = binary.Write(w, binary.BigEndian, fw.rsrcForkHeader()) + err = binary.Write(w, binary.BigEndian, hlFile.rsrcForkHeader()) if err != nil { return fmt.Errorf("send resource fork header: %v", err) } } - rFile, _ := fw.rsrcForkFile() + rFile, _ := hlFile.rsrcForkFile() //if err != nil { // // return fmt.Errorf("open resource fork file: %v", err) //} @@ -332,7 +332,7 @@ func UploadHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTransfe return fmt.Errorf("open temp file for uploade: %w", err) } - f, err := NewFileWrapper(fileStore, fullPath, 0) + f, err := NewFile(fileStore, fullPath, 0) if err != nil { return err } @@ -424,7 +424,7 @@ func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *Fil return nil } - hlFile, err := NewFileWrapper(fileStore, path, 0) + hlFile, err := NewFile(fileStore, path, 0) if err != nil { return err } @@ -645,7 +645,7 @@ func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileT filePath := path.Join(fullPath, fu.FormattedPath()) - hlFile, err := NewFileWrapper(fileStore, filePath, 0) + hlFile, err := NewFile(fileStore, filePath, 0) if err != nil { return err } diff --git a/hotline/file_wrapper.go b/hotline/file_wrapper.go index 41b3338..b80bc4b 100644 --- a/hotline/file_wrapper.go +++ b/hotline/file_wrapper.go @@ -30,7 +30,7 @@ type File struct { Ffo *flattenedFileObject } -func NewFileWrapper(fs FileStore, path string, dataOffset int64) (*File, error) { +func NewFile(fs FileStore, path string, dataOffset int64) (*File, error) { dir := filepath.Dir(path) fName := filepath.Base(path) f := File{ @@ -130,7 +130,11 @@ func (f *File) incFileWriter() (io.WriteCloser, error) { } func (f *File) dataForkReader() (io.Reader, error) { - return f.fs.Open(f.dataPath) + if _, err := f.fs.Stat(f.dataPath); err == nil { + return f.fs.Open(f.dataPath) + } + + return f.fs.Open(f.incompletePath) } func (f *File) rsrcForkFile() (*os.File, error) { diff --git a/hotline/files.go b/hotline/files.go index e9f7b4f..581b11c 100644 --- a/hotline/files.go +++ b/hotline/files.go @@ -112,9 +112,9 @@ func GetFileNameList(path string, ignoreList []string) (fields []Field, err erro continue } - hlFile, err := NewFileWrapper(&OSFileStore{}, path+"/"+file.Name(), 0) + hlFile, err := NewFile(&OSFileStore{}, path+"/"+file.Name(), 0) if err != nil { - return nil, fmt.Errorf("NewFileWrapper: %w", err) + return nil, fmt.Errorf("NewFile: %w", err) } copy(fnwi.FileSize[:], hlFile.TotalSize()) diff --git a/internal/mobius/transaction_handlers.go b/internal/mobius/transaction_handlers.go index 52ea29a..20a315d 100644 --- a/internal/mobius/transaction_handlers.go +++ b/internal/mobius/transaction_handlers.go @@ -86,11 +86,10 @@ const ( // General error messages ErrMsgAccountNotFound = "Account not found." - ErrMsgUserNotFound = "User not found." - ErrMsgCreateAlias = "Error creating alias" + ErrMsgUserNotFound = "User not found." + ErrMsgCreateAlias = "Error creating alias" ) - // Converts bytes from Mac Roman encoding to UTF-8 var txtDecoder = charmap.Macintosh.NewDecoder() @@ -307,7 +306,7 @@ func HandleGetFileInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []ho return res } - fw, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0) + fw, err := hotline.NewFile(cc.Server.FS, fullFilePath, 0) if err != nil { return res } @@ -361,7 +360,7 @@ func HandleSetFileInfo(cc *hotline.ClientConn, t *hotline.Transaction) (res []ho return res } - hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0) + hlFile, err := hotline.NewFile(cc.Server.FS, fullFilePath, 0) if err != nil { return res } @@ -449,7 +448,7 @@ func HandleDeleteFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hot return res } - hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, 0) + hlFile, err := hotline.NewFile(cc.Server.FS, fullFilePath, 0) if err != nil { return res } @@ -494,7 +493,7 @@ func HandleMoveFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []hotli cc.Logger.Info("Move file", "src", filePath+"/"+fileName, "dst", fileNewPath+"/"+fileName) - hlFile, err := hotline.NewFileWrapper(cc.Server.FS, filePath, 0) + hlFile, err := hotline.NewFile(cc.Server.FS, filePath, 0) if err != nil { return res } @@ -1553,7 +1552,7 @@ func HandleDownloadFile(cc *hotline.ClientConn, t *hotline.Transaction) (res []h return res } - hlFile, err := hotline.NewFileWrapper(cc.Server.FS, fullFilePath, dataOffset) + hlFile, err := hotline.NewFile(cc.Server.FS, fullFilePath, dataOffset) if err != nil { return res } -- cgit