| Current Path : /home/hotlineuser/mobius/hotline/ |
| Current File : //home/hotlineuser/mobius/hotline/file_transfer.go |
package hotline
import (
"bufio"
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"math"
"os"
"path"
"path/filepath"
"slices"
"strings"
"sync"
)
// Folder download actions. Send by the client to indicate the next action the server should take
// for a folder download.
const (
DlFldrActionSendFile = 1
DlFldrActionResumeFile = 2
DlFldrActionNextFile = 3
)
// File transfer types
type FileTransferType uint8
const (
FileDownload = FileTransferType(0)
FileUpload = FileTransferType(1)
FolderDownload = FileTransferType(2)
FolderUpload = FileTransferType(3)
BannerDownload = FileTransferType(4)
)
type FileTransferID [4]byte
type FileTransferMgr interface {
Add(ft *FileTransfer)
Get(id FileTransferID) *FileTransfer
Delete(id FileTransferID)
}
type MemFileTransferMgr struct {
fileTransfers map[FileTransferID]*FileTransfer
mu sync.Mutex
}
func NewMemFileTransferMgr() *MemFileTransferMgr {
return &MemFileTransferMgr{
fileTransfers: make(map[FileTransferID]*FileTransfer),
}
}
func (ftm *MemFileTransferMgr) Add(ft *FileTransfer) {
ftm.mu.Lock()
defer ftm.mu.Unlock()
_, _ = rand.Read(ft.RefNum[:])
ftm.fileTransfers[ft.RefNum] = ft
ft.ClientConn.ClientFileTransferMgr.Add(ft.Type, ft)
}
func (ftm *MemFileTransferMgr) Get(id FileTransferID) *FileTransfer {
ftm.mu.Lock()
defer ftm.mu.Unlock()
return ftm.fileTransfers[id]
}
func (ftm *MemFileTransferMgr) Delete(id FileTransferID) {
ftm.mu.Lock()
defer ftm.mu.Unlock()
ft := ftm.fileTransfers[id]
ft.ClientConn.ClientFileTransferMgr.Delete(ft.Type, id)
delete(ftm.fileTransfers, id)
}
type FileTransfer struct {
FileRoot string
FileName []byte
FilePath []byte
RefNum [4]byte
Type FileTransferType
TransferSize []byte
FolderItemCount []byte
FileResumeData *FileResumeData
Options []byte
bytesSentCounter *WriteCounter
ClientConn *ClientConn
}
// WriteCounter counts the number of bytes written to it.
type WriteCounter struct {
mux sync.Mutex
Total int64 // Total # of bytes written
}
// Write implements the io.Writer interface.
//
// Always completes and never returns an error.
func (wc *WriteCounter) Write(p []byte) (int, error) {
wc.mux.Lock()
defer wc.mux.Unlock()
n := len(p)
wc.Total += int64(n)
return n, nil
}
func (cc *ClientConn) NewFileTransfer(transferType FileTransferType, fileroot string, fileName, filePath, size []byte) *FileTransfer {
ft := &FileTransfer{
FileName: fileName,
FileRoot: fileroot,
FilePath: filePath,
Type: transferType,
TransferSize: size,
ClientConn: cc,
bytesSentCounter: &WriteCounter{},
}
cc.Server.FileTransferMgr.Add(ft)
return ft
}
// String returns a string representation of a file transfer and its progress for display in the GetInfo window
// Example:
// MasterOfOrionII1.4.0. 0% 197.9M
func (ft *FileTransfer) String() string {
trunc := fmt.Sprintf("%.21s", ft.FileName)
return fmt.Sprintf("%-21s %.3s%% %6s\n", trunc, ft.percentComplete(), ft.formattedTransferSize())
}
func (ft *FileTransfer) percentComplete() string {
ft.bytesSentCounter.mux.Lock()
defer ft.bytesSentCounter.mux.Unlock()
return fmt.Sprintf(
"%v",
math.RoundToEven(float64(ft.bytesSentCounter.Total)/float64(binary.BigEndian.Uint32(ft.TransferSize))*100),
)
}
func (ft *FileTransfer) formattedTransferSize() string {
sizeInKB := float32(binary.BigEndian.Uint32(ft.TransferSize)) / 1024
if sizeInKB >= 1024 {
return fmt.Sprintf("%.1fM", sizeInKB/1024)
} else {
return fmt.Sprintf("%.0fK", sizeInKB)
}
}
func (ft *FileTransfer) ItemCount() int {
return int(binary.BigEndian.Uint16(ft.FolderItemCount))
}
type folderUpload struct {
DataSize [2]byte
IsFolder [2]byte
PathItemCount [2]byte
FileNamePath []byte
}
// 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[:])
if pathItemLen == 0 {
return ""
}
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...)
}
type FileHeader struct {
Size [2]byte // Total size of FileHeader payload
Type [2]byte // 0 for file, 1 for dir
FilePath []byte // encoded file path
readOffset int // Internal offset to track read progress
}
func NewFileHeader(fileName string, isDir bool) FileHeader {
fh := FileHeader{
FilePath: EncodeFilePath(fileName),
}
if isDir {
fh.Type = [2]byte{0x00, 0x01}
}
encodedPathLen := uint16(len(fh.FilePath) + len(fh.Type))
binary.BigEndian.PutUint16(fh.Size[:], encodedPathLen)
return fh
}
func (fh *FileHeader) Read(p []byte) (int, error) {
buf := slices.Concat(
fh.Size[:],
fh.Type[:],
fh.FilePath,
)
return readFrom(p, &fh.readOffset, buf)
}
func DownloadHandler(w io.Writer, fullPath string, fileTransfer *FileTransfer, fs FileStore, rLogger *slog.Logger, preserveForks bool) error {
var dataOffset int64
if fileTransfer.FileResumeData != nil {
dataOffset = int64(binary.BigEndian.Uint32(fileTransfer.FileResumeData.ForkInfoList[0].DataSize[:]))
}
hlFile, err := NewFile(fs, fullPath, 0)
if err != nil {
return fmt.Errorf("reading file header: %v", err)
}
rLogger.Info("Download file", "filePath", fullPath)
// 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, hlFile.Ffo); err != nil {
return fmt.Errorf("send flat file object: %v", err)
}
}
file, err := hlFile.dataForkReader()
if err != nil {
return fmt.Errorf("open data fork reader: %v", err)
}
br := bufio.NewReader(file)
if _, err := br.Discard(int(dataOffset)); err != nil {
return fmt.Errorf("seek to resume offsent: %v", err)
}
if _, err = io.Copy(w, io.TeeReader(br, fileTransfer.bytesSentCounter)); err != nil {
return fmt.Errorf("send data fork: %v", err)
}
// If the client requested to resume transfer, do not send the resource fork header.
if fileTransfer.FileResumeData == nil {
err = binary.Write(w, binary.BigEndian, hlFile.rsrcForkHeader())
if err != nil {
return fmt.Errorf("send resource fork header: %v", err)
}
}
rFile, _ := hlFile.rsrcForkFile()
//if err != nil {
// // return fmt.Errorf("open resource fork file: %v", err)
//}
_, _ = io.Copy(w, io.TeeReader(rFile, fileTransfer.bytesSentCounter))
//if err != nil {
// // return fmt.Errorf("send resource fork data: %v", err)
//}
return nil
}
func UploadHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTransfer, fileStore FileStore, rLogger *slog.Logger, preserveForks bool) error {
var file *os.File
// A file upload has two possible cases:
// 1) Upload a new file
// 2) Resume a partially transferred file
// We have to infer which case applies by inspecting what is already on the filesystem
// Check for existing file. If found, do not proceed. This is an invalid scenario, as the file upload transaction
// handler should have returned an error to the client indicating there was an existing file present.
_, err := os.Stat(fullPath)
if err == nil {
return fmt.Errorf("existing file found: %s", fullPath)
}
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("check file existence: %w", err)
}
// 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 := NewFile(fileStore, fullPath, 0)
if err != nil {
return err
}
rLogger.Debug("File upload started", "dstFile", fullPath)
rForkWriter := io.Discard
iForkWriter := io.Discard
if preserveForks {
rForkWriter, err = f.rsrcForkWriter()
if err != nil {
return err
}
iForkWriter, err = f.InfoForkWriter()
if err != nil {
return err
}
}
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)
}
rLogger.Info("File upload complete", "dstFile", fullPath)
return nil
}
func DownloadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTransfer, fileStore FileStore, rLogger *slog.Logger, preserveForks bool) error {
// Folder Download flow:
// 1. Get filePath from the transfer
// 2. Iterate over files
// 3. For each file:
// Send file header to client
// The client can reply in 3 ways:
//
// 1. If type is an odd number (unknown type?), or file download for the current file is completed:
// client sends []byte{0x00, 0x03} to tell the server to continue to the next file
//
// 2. If download of a file is to be resumed:
// client sends:
// []byte{0x00, 0x02} // download folder action
// [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}
//
// When download is requested (case 2 or 3), server replies with:
// [4]byte - file size
// []byte - Flattened File Object
//
// After every file download, client could request next file with:
// []byte{0x00, 0x03}
//
// This notifies the server to send the next item header
basePathLen := len(fullPath)
rLogger.Info("Start folder download", "path", fullPath)
nextAction := make([]byte, 2)
if _, err := io.ReadFull(rwc, nextAction); err != nil {
return err
}
i := 0
err := filepath.Walk(fullPath+"/", func(path string, info os.FileInfo, err error) error {
//s.Stats.DownloadCounter += 1
i += 1
if err != nil {
return err
}
// skip dot files
if strings.HasPrefix(info.Name(), ".") {
return nil
}
hlFile, err := NewFile(fileStore, path, 0)
if err != nil {
return err
}
subPath := path[basePathLen+1:]
if i == 1 {
return nil
}
fileHeader := NewFileHeader(subPath, info.IsDir())
if _, err := io.Copy(rwc, &fileHeader); err != nil {
return fmt.Errorf("error sending file header: %w", err)
}
// Read the client's Next Action request
if _, err := io.ReadFull(rwc, nextAction); err != nil {
return err
}
var dataOffset int64
switch nextAction[1] {
case DlFldrActionResumeFile:
// get size of resumeData
resumeDataByteLen := make([]byte, 2)
if _, err := io.ReadFull(rwc, resumeDataByteLen); err != nil {
return err
}
resumeDataLen := binary.BigEndian.Uint16(resumeDataByteLen)
resumeDataBytes := make([]byte, resumeDataLen)
if _, err := io.ReadFull(rwc, resumeDataBytes); err != nil {
return err
}
var frd FileResumeData
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
}
rLogger.Info("File download started",
"fileName", info.Name(),
"TransferSize", fmt.Sprintf("%x", hlFile.Ffo.TransferSize(dataOffset)),
)
// Send file size to client
if _, err := rwc.Write(hlFile.Ffo.TransferSize(dataOffset)); err != nil {
rLogger.Error(err.Error())
return fmt.Errorf("error sending file size: %w", err)
}
// Send ffo bytes to client
_, err = io.Copy(rwc, hlFile.Ffo)
if err != nil {
return fmt.Errorf("error sending flat file object: %w", err)
}
file, err := fileStore.Open(path)
if err != nil {
return fmt.Errorf("error opening file: %w", err)
}
if _, err = io.Copy(rwc, io.TeeReader(file, fileTransfer.bytesSentCounter)); err != nil {
return fmt.Errorf("error sending file: %w", err)
}
if nextAction[1] != 2 && hlFile.Ffo.FlatFileHeader.ForkCount[1] == 3 {
err = binary.Write(rwc, binary.BigEndian, hlFile.rsrcForkHeader())
if err != nil {
return fmt.Errorf("error sending resource fork header: %w", err)
}
rFile, err := hlFile.rsrcForkFile()
if err != nil {
return fmt.Errorf("error opening resource fork: %w", err)
}
if _, err = io.Copy(rwc, io.TeeReader(rFile, fileTransfer.bytesSentCounter)); err != nil {
return fmt.Errorf("error sending resource fork: %w", err)
}
}
// Read the client's Next Action request. This is always 3, I think?
if _, err := io.ReadFull(rwc, nextAction); err != nil && err != io.EOF {
return fmt.Errorf("error reading client next action: %w", err)
}
return nil
})
if err != nil {
return err
}
return nil
}
func UploadFolderHandler(rwc io.ReadWriter, fullPath string, fileTransfer *FileTransfer, fileStore FileStore, rLogger *slog.Logger, preserveForks bool) error {
// Check if the target folder exists. If not, create it.
if _, err := fileStore.Stat(fullPath); os.IsNotExist(err) {
if err := fileStore.Mkdir(fullPath, 0777); err != nil {
return err
}
}
// Begin the folder upload flow by sending the "next file action" to client
if _, err := rwc.Write([]byte{0, DlFldrActionNextFile}); err != nil {
return err
}
fileSize := make([]byte, 4)
for i := 0; i < fileTransfer.ItemCount(); i++ {
//s.Stats.UploadCounter += 1
var fu folderUpload
if _, err := io.ReadFull(rwc, fu.DataSize[:]); err != nil {
return err
}
if _, err := io.ReadFull(rwc, fu.IsFolder[:]); err != nil {
return err
}
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 length of the DataSize and IsFolder fields
if _, err := io.ReadFull(rwc, fu.FileNamePath); err != nil {
return err
}
if fu.IsFolder == [2]byte{0, 1} {
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
}
}
// Tell client to send next file
if _, err := rwc.Write([]byte{0, DlFldrActionNextFile}); err != nil {
return err
}
} else {
nextAction := DlFldrActionSendFile
// Check if we have the full file already. If so, send dlFldrAction_NextFile to client to skip.
_, err := os.Stat(path.Join(fullPath, fu.FormattedPath()))
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
if err == nil {
nextAction = DlFldrActionNextFile
}
// Check if we have a partial file already. If so, send dlFldrAction_ResumeFile to client to resume upload.
incompleteFile, err := os.Stat(path.Join(fullPath, fu.FormattedPath()+IncompleteFileSuffix))
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}
if err == nil {
nextAction = DlFldrActionResumeFile
}
if _, err := rwc.Write([]byte{0, uint8(nextAction)}); err != nil {
return err
}
switch nextAction {
case DlFldrActionNextFile:
continue
case DlFldrActionResumeFile:
offset := make([]byte, 4)
binary.BigEndian.PutUint32(offset, uint32(incompleteFile.Size()))
file, err := os.OpenFile(fullPath+"/"+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 := rwc.Write(append(bs, b...)); err != nil {
return err
}
if _, err := io.ReadFull(rwc, fileSize); err != nil {
return err
}
if err := receiveFile(rwc, file, io.Discard, io.Discard, fileTransfer.bytesSentCounter); err != nil {
rLogger.Error(err.Error())
}
err = os.Rename(fullPath+"/"+fu.FormattedPath()+".incomplete", fullPath+"/"+fu.FormattedPath())
if err != nil {
return err
}
case DlFldrActionSendFile:
if _, err := io.ReadFull(rwc, fileSize); err != nil {
return err
}
filePath := path.Join(fullPath, fu.FormattedPath())
hlFile, err := NewFile(fileStore, filePath, 0)
if err != nil {
return err
}
rLogger.Info("Starting file transfer", "path", filePath, "fileNum", i+1, "fileSize", binary.BigEndian.Uint32(fileSize))
incWriter, err := hlFile.incFileWriter()
if err != nil {
return err
}
rForkWriter := io.Discard
iForkWriter := io.Discard
if preserveForks {
iForkWriter, err = hlFile.InfoForkWriter()
if err != nil {
return err
}
rForkWriter, err = hlFile.rsrcForkWriter()
if err != nil {
return err
}
}
if err := receiveFile(rwc, incWriter, rForkWriter, iForkWriter, fileTransfer.bytesSentCounter); err != nil {
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
}
}
// Tell client to send next file
if _, err := rwc.Write([]byte{0, DlFldrActionNextFile}); err != nil {
return err
}
}
}
rLogger.Info("Folder upload complete")
return nil
}