From: Jeff Halter Date: Sat, 4 Jun 2022 00:11:06 +0000 (-0700) Subject: Initial implementation of file transfer resume X-Git-Url: https://git.r.bdr.sh/rbdr/mobius/commitdiff_plain/16a4ad707a05df25c9d12b8cc89fb3a9e3be0dba Initial implementation of file transfer resume --- diff --git a/hotline/field.go b/hotline/field.go index ed009f4..5f2e873 100644 --- a/hotline/field.go +++ b/hotline/field.go @@ -29,6 +29,8 @@ const fieldServerName = 162 const fieldFileNameWithInfo = 200 const fieldFileName = 201 const fieldFilePath = 202 +const fieldFileResumeData = 203 +const fieldFileTransferOptions = 204 const fieldFileTypeString = 205 const fieldFileCreatorString = 206 const fieldFileSize = 207 diff --git a/hotline/file_resume_data.go b/hotline/file_resume_data.go new file mode 100644 index 0000000..f5b82de --- /dev/null +++ b/hotline/file_resume_data.go @@ -0,0 +1,75 @@ +package hotline + +import ( + "bytes" + "encoding/binary" +) + +// 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 + ForkCount [2]byte // Length of ForkInfoList. Either 2 or 3 depending on whether file has a resource fork + ForkInfoList []ForkInfoList +} + +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 +} + +func NewForkInfoList(b []byte) *ForkInfoList { + return &ForkInfoList{ + Fork: [4]byte{0x44, 0x41, 0x54, 0x41}, + DataSize: [4]byte{b[0], b[1], b[2], b[3]}, + RSVDA: [4]byte{}, + RSVDB: [4]byte{}, + } +} + +func NewFileResumeData(list []ForkInfoList) *FileResumeData { + return &FileResumeData{ + Format: [4]byte{0x52, 0x46, 0x4C, 0x54}, // RFLT + Version: [2]byte{0, 1}, + RSVD: [34]byte{}, + ForkCount: [2]byte{0, uint8(len(list))}, + ForkInfoList: list, + } +} + +func (frd *FileResumeData) BinaryMarshal() ([]byte, error) { + 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) UnmarshalBinary(b []byte) error { + frd.Format = [4]byte{b[0], b[1], b[2], b[3]} + frd.Version = [2]byte{b[4], b[5]} + frd.ForkCount = [2]byte{b[40], b[41]} + + for i := 0; i < int(frd.ForkCount[1]); i++ { + var fil ForkInfoList + start := 42 + i*16 + end := start + 16 + + r := bytes.NewReader(b[start:end]) + if err := binary.Read(r, binary.BigEndian, &fil); err != nil { + return err + } + + frd.ForkInfoList = append(frd.ForkInfoList, fil) + } + + return nil +} diff --git a/hotline/file_transfer.go b/hotline/file_transfer.go index c0c2f87..65ef0b0 100644 --- a/hotline/file_transfer.go +++ b/hotline/file_transfer.go @@ -23,6 +23,7 @@ type FileTransfer struct { FolderItemCount []byte BytesSent int clientID uint16 + fileResumeData *FileResumeData } func (ft *FileTransfer) String() string { @@ -32,6 +33,10 @@ func (ft *FileTransfer) String() string { return out } +func (ft *FileTransfer) ItemCount() int { + return int(binary.BigEndian.Uint16(ft.FolderItemCount)) +} + // 00 28 // DataSize // 00 00 // IsFolder // 00 02 // PathItemCount diff --git a/hotline/file_types.go b/hotline/file_types.go index 0a2fa93..ae4bd36 100644 --- a/hotline/file_types.go +++ b/hotline/file_types.go @@ -1,8 +1,10 @@ package hotline type fileType struct { - TypeCode string - CreatorCode string + TypeCode string // 4 byte type code used in file transfers + CreatorCode string // 4 byte creator code used in file transfers + CreatorString string // variable length string used in file get info + FileTypeString string // variable length string used in file get info } var defaultFileType = fileType{ @@ -55,4 +57,8 @@ var fileTypes = map[string]fileType{ TypeCode: "MooV", CreatorCode: "TVOD", }, + "incomplete": { // Partial file upload + TypeCode: "HTft", + CreatorCode: "HTLC", + }, } diff --git a/hotline/files.go b/hotline/files.go index 19375e9..478041b 100644 --- a/hotline/files.go +++ b/hotline/files.go @@ -2,6 +2,8 @@ package hotline import ( "encoding/binary" + "errors" + "io/fs" "io/ioutil" "os" "path/filepath" @@ -54,11 +56,13 @@ func getFileNameList(filePath string) (fields []Field, err error) { copy(fnwi.Creator[:], fileCreator[:]) } + strippedName := strings.Replace(file.Name(), ".incomplete", "", -1) + nameSize := make([]byte, 2) - binary.BigEndian.PutUint16(nameSize, uint16(len(file.Name()))) + binary.BigEndian.PutUint16(nameSize, uint16(len(strippedName))) copy(fnwi.NameSize[:], nameSize[:]) - fnwi.name = []byte(file.Name()) + fnwi.name = []byte(strippedName) b, err := fnwi.MarshalBinary() if err != nil { @@ -133,3 +137,21 @@ func EncodeFilePath(filePath string) []byte { return bytes } + +const incompleteFileSuffix = ".incomplete" + +// effectiveFile wraps os.Open to check for the presence of a partial file transfer as a fallback +func effectiveFile(filePath string) (*os.File, error) { + file, err := os.Open(filePath) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + + if errors.Is(err, fs.ErrNotExist) { + file, err = os.OpenFile(filePath+incompleteFileSuffix, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return nil, err + } + } + return file, nil +} diff --git a/hotline/flattened_file_object.go b/hotline/flattened_file_object.go index ba6b063..6f38e46 100644 --- a/hotline/flattened_file_object.go +++ b/hotline/flattened_file_object.go @@ -175,15 +175,16 @@ func (f *flattenedFileObject) BinaryMarshal() []byte { return out } -func NewFlattenedFileObject(fileRoot string, filePath, fileName []byte) (*flattenedFileObject, error) { +func NewFlattenedFileObject(fileRoot string, filePath, fileName []byte, dataOffset int64) (*flattenedFileObject, error) { fullFilePath, err := readPath(fileRoot, filePath, fileName) if err != nil { return nil, err } - file, err := os.Open(fullFilePath) + file, err := effectiveFile(fullFilePath) if err != nil { return nil, err } + defer func(file *os.File) { _ = file.Close() }(file) fileInfo, err := file.Stat() @@ -192,7 +193,7 @@ func NewFlattenedFileObject(fileRoot string, filePath, fileName []byte) (*flatte } dataSize := make([]byte, 4) - binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size())) + binary.BigEndian.PutUint32(dataSize, uint32(fileInfo.Size()-dataOffset)) mTime := toHotlineTime(fileInfo.ModTime()) diff --git a/hotline/flattened_file_object_test.go b/hotline/flattened_file_object_test.go index 300eb34..9a57e7e 100644 --- a/hotline/flattened_file_object_test.go +++ b/hotline/flattened_file_object_test.go @@ -52,7 +52,7 @@ func TestNewFlattenedFileObject(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NewFlattenedFileObject(tt.args.fileRoot, tt.args.filePath, tt.args.fileName) + got, err := NewFlattenedFileObject(tt.args.fileRoot, tt.args.filePath, tt.args.fileName, 0) if tt.wantErr(t, err, fmt.Sprintf("NewFlattenedFileObject(%v, %v, %v)", tt.args.fileRoot, tt.args.filePath, tt.args.fileName)) { return } diff --git a/hotline/server.go b/hotline/server.go index b0ddf2d..0828385 100644 --- a/hotline/server.go +++ b/hotline/server.go @@ -7,6 +7,7 @@ import ( "fmt" "go.uber.org/zap" "io" + "io/fs" "io/ioutil" "math/big" "math/rand" @@ -633,6 +634,9 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { transferRefNum := binary.BigEndian.Uint32(t.ReferenceNumber[:]) fileTransfer := s.FileTransfers[transferRefNum] + // delete single use transferRefNum + delete(s.FileTransfers, transferRefNum) + switch fileTransfer.Type { case FileDownload: fullFilePath, err := readPath(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName) @@ -640,11 +644,12 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { return err } - ffo, err := NewFlattenedFileObject( - s.Config.FileRoot, - fileTransfer.FilePath, - fileTransfer.FileName, - ) + var dataOffset int64 + if fileTransfer.fileResumeData != nil { + dataOffset = int64(binary.BigEndian.Uint32(fileTransfer.fileResumeData.ForkInfoList[0].DataSize[:])) + } + + ffo, err := NewFlattenedFileObject(s.Config.FileRoot, fileTransfer.FilePath, fileTransfer.FileName, dataOffset) if err != nil { return err } @@ -662,32 +667,49 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { } sendBuffer := make([]byte, 1048576) + var totalSent int64 for { var bytesRead int - if bytesRead, err = file.Read(sendBuffer); err == io.EOF { + if bytesRead, err = file.ReadAt(sendBuffer, dataOffset+totalSent); err == io.EOF { + if _, err := conn.Write(sendBuffer[:bytesRead]); err != nil { + return err + } break } + if err != nil { + return err + } + totalSent += int64(bytesRead) fileTransfer.BytesSent += bytesRead - delete(s.FileTransfers, transferRefNum) - if _, err := conn.Write(sendBuffer[:bytesRead]); err != nil { return err } } case FileUpload: destinationFile := s.Config.FileRoot + ReadFilePath(fileTransfer.FilePath) + "/" + string(fileTransfer.FileName) - newFile, err := FS.Create(destinationFile) - if err != nil { - return err + tmpFile := destinationFile + ".incomplete" + + file, err := effectiveFile(destinationFile) + if errors.Is(err, fs.ErrNotExist) { + file, err = FS.Create(tmpFile) + if err != nil { + return err + } } - defer func() { _ = newFile.Close() }() + + defer func() { _ = file.Close() }() s.Logger.Infow("File upload started", "transactionRef", fileTransfer.ReferenceNumber, "dstFile", destinationFile) - if err := receiveFile(conn, newFile, nil); err != nil { - s.Logger.Errorw("file upload error", "error", err) + // TODO: replace io.Discard with a real file when ready to implement storing of resource fork data + if err := receiveFile(conn, file, io.Discard); err != nil { + return err + } + + if err := os.Rename(destinationFile+".incomplete", destinationFile); err != nil { + return err } s.Logger.Infow("File upload complete", "transactionRef", fileTransfer.ReferenceNumber, "dstFile", destinationFile) @@ -708,7 +730,7 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { // [2]byte // Resume data size // []byte file resume data (see myField_FileResumeData) // - // 3. Otherwise download of the file is requested and client sends []byte{0x00, 0x01} + // 3. Otherwise, download of the file is requested and client sends []byte{0x00, 0x01} // // When download is requested (case 2 or 3), server replies with: // [4]byte - file size @@ -758,23 +780,42 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { if _, err := conn.Read(nextAction); err != nil { return err } - if nextAction[1] == 3 { - return nil - } s.Logger.Infow("Client folder download action", "action", fmt.Sprintf("%X", nextAction[0:2])) + var dataOffset int64 + + switch nextAction[1] { + case dlFldrActionResumeFile: + // client asked to resume this file + var frd FileResumeData + // get size of resumeData + if _, err := conn.Read(nextAction); err != nil { + return err + } + + resumeDataLen := binary.BigEndian.Uint16(nextAction) + resumeDataBytes := make([]byte, resumeDataLen) + if _, err := conn.Read(resumeDataBytes); err != nil { + return err + } + + if err := frd.UnmarshalBinary(resumeDataBytes); err != nil { + return err + } + dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:])) + case dlFldrActionNextFile: + // client asked to skip this file + return nil + } + if info.IsDir() { return nil } splitPath := strings.Split(path, "/") - ffo, err := NewFlattenedFileObject( - strings.Join(splitPath[:len(splitPath)-1], "/"), - nil, - []byte(info.Name()), - ) + ffo, err := NewFlattenedFileObject(strings.Join(splitPath[:len(splitPath)-1], "/"), nil, []byte(info.Name()), dataOffset) if err != nil { return err } @@ -801,22 +842,42 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { return err } - // Copy N bytes from file to connection - _, err = io.CopyN(conn, file, int64(binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize[:]))) - if err != nil { - return err + // // Copy N bytes from file to connection + // _, err = io.CopyN(conn, file, int64(binary.BigEndian.Uint32(ffo.FlatFileDataForkHeader.DataSize[:]))) + // if err != nil { + // return err + // } + // file.Close() + sendBuffer := make([]byte, 1048576) + var totalSent int64 + for { + var bytesRead int + if bytesRead, err = file.ReadAt(sendBuffer, dataOffset+totalSent); err == io.EOF { + if _, err := conn.Write(sendBuffer[:bytesRead]); err != nil { + return err + } + break + } + if err != nil { + panic(err) + } + totalSent += int64(bytesRead) + + fileTransfer.BytesSent += bytesRead + + if _, err := conn.Write(sendBuffer[:bytesRead]); err != nil { + return err + } } - file.Close() // TODO: optionally send resource fork header and resource fork data - // Read the client's Next Action request + // Read the client's Next Action request. This is always 3, I think? if _, err := conn.Read(nextAction); err != nil { return err } - // TODO: switch behavior based on possible next action - return err + return nil }) case FolderUpload: @@ -834,9 +895,8 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { // Check if the target folder exists. If not, create it. if _, err := FS.Stat(dstPath); os.IsNotExist(err) { - s.Logger.Infow("Creating target path", "dstPath", dstPath) if err := FS.Mkdir(dstPath, 0777); err != nil { - s.Logger.Error(err) + return err } } @@ -846,10 +906,10 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { } fileSize := make([]byte, 4) - itemCount := binary.BigEndian.Uint16(fileTransfer.FolderItemCount) - readBuffer := make([]byte, 1024) - for i := uint16(0); i < itemCount; i++ { + + for i := 0; i < fileTransfer.ItemCount(); i++ { + _, err := conn.Read(readBuffer) if err != nil { return err @@ -866,48 +926,106 @@ func (s *Server) handleFileTransfer(conn io.ReadWriteCloser) error { if fu.IsFolder == [2]byte{0, 1} { if _, err := os.Stat(dstPath + "/" + fu.FormattedPath()); os.IsNotExist(err) { - s.Logger.Infow("Target path does not exist; Creating...", "dstPath", dstPath) if err := os.Mkdir(dstPath+"/"+fu.FormattedPath(), 0777); err != nil { - s.Logger.Error(err) + return err } } // Tell client to send next file if _, err := conn.Write([]byte{0, dlFldrActionNextFile}); err != nil { - s.Logger.Error(err) return err } } else { - // TODO: Check if we have the full file already. If so, send dlFldrAction_NextFile to client to skip. - // TODO: Check if we have a partial file already. If so, send dlFldrAction_ResumeFile to client to resume upload. - // Send dlFldrAction_SendFile to client to begin transfer - if _, err := conn.Write([]byte{0, dlFldrActionSendFile}); err != nil { + nextAction := dlFldrActionSendFile + + // Check if we have the full file already. If so, send dlFldrAction_NextFile to client to skip. + _, err := os.Stat(dstPath + "/" + fu.FormattedPath()) + if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } + if err == nil { + nextAction = dlFldrActionNextFile + } - if _, err := conn.Read(fileSize); err != nil { + // Check if we have a partial file already. If so, send dlFldrAction_ResumeFile to client to resume upload. + inccompleteFile, err := os.Stat(dstPath + "/" + fu.FormattedPath() + incompleteFileSuffix) + if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } + if err == nil { + nextAction = dlFldrActionResumeFile + } - filePath := dstPath + "/" + fu.FormattedPath() - s.Logger.Infow("Starting file transfer", "path", filePath, "fileNum", i+1, "totalFiles", itemCount, "fileSize", binary.BigEndian.Uint32(fileSize)) + fmt.Printf("Next Action: %v\n", nextAction) - newFile, err := FS.Create(filePath) - if err != nil { + if _, err := conn.Write([]byte{0, uint8(nextAction)}); err != nil { return err } - if err := receiveFile(conn, newFile, ioutil.Discard); err != nil { - s.Logger.Error(err) + switch nextAction { + case dlFldrActionNextFile: + continue + case dlFldrActionResumeFile: + offset := make([]byte, 4) + binary.BigEndian.PutUint32(offset, uint32(inccompleteFile.Size())) + + file, err := os.OpenFile(dstPath+"/"+fu.FormattedPath()+incompleteFileSuffix, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + + fileResumeData := NewFileResumeData([]ForkInfoList{ + *NewForkInfoList(offset), + }) + + b, _ := fileResumeData.BinaryMarshal() + + bs := make([]byte, 2) + binary.BigEndian.PutUint16(bs, uint16(len(b))) + + if _, err := conn.Write(append(bs, b...)); err != nil { + return err + } + + if _, err := conn.Read(fileSize); err != nil { + return err + } + + if err := receiveFile(conn, file, ioutil.Discard); err != nil { + s.Logger.Error(err) + } + + err = os.Rename(dstPath+"/"+fu.FormattedPath()+".incomplete", dstPath+"/"+fu.FormattedPath()) + if err != nil { + return err + } + + case dlFldrActionSendFile: + if _, err := conn.Read(fileSize); err != nil { + return err + } + + filePath := dstPath + "/" + fu.FormattedPath() + s.Logger.Infow("Starting file transfer", "path", filePath, "fileNum", i+1, "totalFiles", "zz", "fileSize", binary.BigEndian.Uint32(fileSize)) + + newFile, err := FS.Create(filePath + ".incomplete") + if err != nil { + return err + } + + if err := receiveFile(conn, newFile, ioutil.Discard); err != nil { + s.Logger.Error(err) + } + _ = newFile.Close() + if err := os.Rename(filePath+".incomplete", filePath); err != nil { + return err + } } - _ = newFile.Close() // Tell client to send next file if _, err := conn.Write([]byte{0, dlFldrActionNextFile}); err != nil { - s.Logger.Error(err) return err } - } } s.Logger.Infof("Folder upload complete") diff --git a/hotline/transaction_handlers.go b/hotline/transaction_handlers.go index cce303f..ec7910a 100644 --- a/hotline/transaction_handlers.go +++ b/hotline/transaction_handlers.go @@ -407,7 +407,7 @@ func HandleGetFileInfo(cc *ClientConn, t *Transaction) (res []Transaction, err e fileName := t.GetField(fieldFileName).Data filePath := t.GetField(fieldFilePath).Data - ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName) + ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName, 0) if err != nil { return res, err } @@ -1222,13 +1222,26 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err fileName := t.GetField(fieldFileName).Data filePath := t.GetField(fieldFilePath).Data + // 2 bytes + // transferOptions := t.GetField(fieldFileTransferOptions).Data + resumeData := t.GetField(fieldFileResumeData).Data + + var dataOffset int64 + var frd FileResumeData + if resumeData != nil { + if err := frd.UnmarshalBinary(t.GetField(fieldFileResumeData).Data); err != nil { + return res, err + } + dataOffset = int64(binary.BigEndian.Uint32(frd.ForkInfoList[0].DataSize[:])) + } + var fp FilePath err = fp.UnmarshalBinary(filePath) if err != nil { return res, err } - ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName) + ffo, err := NewFlattenedFileObject(cc.Server.Config.FileRoot, filePath, fileName, dataOffset) if err != nil { return res, err } @@ -1243,6 +1256,12 @@ func HandleDownloadFile(cc *ClientConn, t *Transaction) (res []Transaction, err Type: FileDownload, } + if resumeData != nil { + var frd FileResumeData + frd.UnmarshalBinary(t.GetField(fieldFileResumeData).Data) + ft.fileResumeData = &frd + } + cc.Server.FileTransfers[data] = ft cc.Transfers[FileDownload] = append(cc.Transfers[FileDownload], ft) @@ -1362,8 +1381,12 @@ func HandleUploadFolder(cc *ClientConn, t *Transaction) (res []Transaction, err } // HandleUploadFile -// Special cases: -// * If the target directory contains "uploads" (case insensitive) +// Fields used in the request: +// 201 File name +// 202 File path +// 204 File transfer options "Optional +// Used only to resume download, currently has value 2" +// 108 File transfer size "Optional used if download is not resumed" func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err error) { if !authorize(cc.Account.Access, accessUploadFile) { res = append(res, cc.NewErrReply(t, "You are not allowed to upload files.")) @@ -1373,6 +1396,11 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er fileName := t.GetField(fieldFileName).Data filePath := t.GetField(fieldFilePath).Data + transferOptions := t.GetField(fieldFileTransferOptions).Data + + // TODO: is this field useful for anything? + // transferSize := t.GetField(fieldTransferSize).Data + var fp FilePath if filePath != nil { if err = fp.UnmarshalBinary(filePath); err != nil { @@ -1398,7 +1426,33 @@ func HandleUploadFile(cc *ClientConn, t *Transaction) (res []Transaction, err er Type: FileUpload, } - res = append(res, cc.NewReply(t, NewField(fieldRefNum, transactionRef))) + replyT := cc.NewReply(t, NewField(fieldRefNum, transactionRef)) + + // client has requested to resume a partially transfered file + if transferOptions != nil { + fullFilePath, err := readPath(cc.Server.Config.FileRoot, filePath, fileName) + if err != nil { + return res, err + } + + fileInfo, err := FS.Stat(fullFilePath + incompleteFileSuffix) + if err != nil { + return res, err + } + + offset := make([]byte, 4) + binary.BigEndian.PutUint32(offset, uint32(fileInfo.Size())) + + fileResumeData := NewFileResumeData([]ForkInfoList{ + *NewForkInfoList(offset), + }) + + b, _ := fileResumeData.BinaryMarshal() + + replyT.Fields = append(replyT.Fields, NewField(fieldFileResumeData, b)) + } + + res = append(res, replyT) return res, err } diff --git a/hotline/transfer.go b/hotline/transfer.go index 706dea9..78bd888 100644 --- a/hotline/transfer.go +++ b/hotline/transfer.go @@ -89,8 +89,8 @@ func receiveFile(conn io.Reader, targetFile io.Writer, resForkFile io.Writer) er if ffh.ForkCount == [2]byte{0, 3} { var resForkHeader FlatFileDataForkHeader resForkBuf := make([]byte, 16) - - if _, err := conn.Read(resForkBuf); err != nil { + resForkBufWrter := bufio.NewWriterSize(resForkFile, 16) + if _, err := io.CopyN(resForkBufWrter, conn, 16); err != nil { return err } err = binary.Read(bytes.NewReader(resForkBuf), binary.BigEndian, &resForkHeader)