225 lines
5.6 KiB
Go
225 lines
5.6 KiB
Go
package teamspeak
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/honeybbq/teamspeak-go/commands"
|
|
)
|
|
|
|
var (
|
|
errFileTransferFailed = errors.New("file transfer failed")
|
|
errUnexpectedRespType = errors.New("unexpected response type")
|
|
errFileTransferTimedOut = errors.New("timeout waiting for file transfer notification")
|
|
)
|
|
|
|
// fileTransferTracker correlates clientftfid with notifystart* responses.
|
|
type fileTransferTracker struct {
|
|
pending map[uint16]chan any
|
|
mu sync.Mutex
|
|
nextID uint16
|
|
}
|
|
|
|
func newFileTransferTracker() *fileTransferTracker {
|
|
return &fileTransferTracker{
|
|
pending: make(map[uint16]chan any),
|
|
}
|
|
}
|
|
|
|
func (t *fileTransferTracker) register() (uint16, <-chan any) {
|
|
t.mu.Lock()
|
|
t.nextID++
|
|
if t.nextID == 0 {
|
|
t.nextID++
|
|
}
|
|
cftid := t.nextID
|
|
ch := make(chan any, 1)
|
|
t.pending[cftid] = ch
|
|
t.mu.Unlock()
|
|
|
|
return cftid, ch
|
|
}
|
|
|
|
func (t *fileTransferTracker) unregister(cftid uint16) {
|
|
t.mu.Lock()
|
|
delete(t.pending, cftid)
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *fileTransferTracker) notify(cftid uint16, v any) {
|
|
t.mu.Lock()
|
|
if ch, ok := t.pending[cftid]; ok {
|
|
ch <- v
|
|
}
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *fileTransferTracker) reset() {
|
|
t.mu.Lock()
|
|
t.pending = make(map[uint16]chan any)
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
// FileTransferInitUpload sends ftinitupload to the server and waits for the
|
|
// notifystartupload response containing the TCP port and transfer key.
|
|
func (c *Client) FileTransferInitUpload(
|
|
channelID uint64, path string, password string, size uint64, overwrite bool,
|
|
) (*FileUploadInfo, error) {
|
|
cftid, ch := c.ftTrack.register()
|
|
defer c.ftTrack.unregister(cftid)
|
|
|
|
targetPath := path
|
|
if !strings.HasPrefix(targetPath, "/") {
|
|
targetPath = "/" + targetPath
|
|
}
|
|
|
|
overwriteVal := "0"
|
|
if overwrite {
|
|
overwriteVal = "1"
|
|
}
|
|
|
|
cmd := commands.BuildCommand("ftinitupload", map[string]string{
|
|
"cid": strconv.FormatUint(channelID, 10),
|
|
"name": targetPath,
|
|
"cpw": password,
|
|
"size": strconv.FormatUint(size, 10),
|
|
"clientftfid": strconv.Itoa(int(cftid)),
|
|
"overwrite": overwriteVal,
|
|
"resume": "0",
|
|
})
|
|
|
|
err := c.ExecCommand(cmd, 10*time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
select {
|
|
case res := <-ch:
|
|
switch v := res.(type) {
|
|
case FileUploadInfo:
|
|
return &v, nil
|
|
case FileTransferStatusInfo:
|
|
return nil, fmt.Errorf("%w: %s (status=%d)", errFileTransferFailed, v.Message, v.Status)
|
|
default:
|
|
return nil, fmt.Errorf("%w: %T", errUnexpectedRespType, v)
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
return nil, errFileTransferTimedOut
|
|
}
|
|
}
|
|
|
|
// FileTransferInitDownload sends ftinitdownload to the server and waits for the
|
|
// notifystartdownload response containing the TCP port and transfer key.
|
|
func (c *Client) FileTransferInitDownload(channelID uint64, path string, password string) (*FileDownloadInfo, error) {
|
|
cftid, ch := c.ftTrack.register()
|
|
defer c.ftTrack.unregister(cftid)
|
|
|
|
targetPath := path
|
|
if !strings.HasPrefix(targetPath, "/") {
|
|
targetPath = "/" + targetPath
|
|
}
|
|
|
|
cmd := commands.BuildCommand("ftinitdownload", map[string]string{
|
|
"cid": strconv.FormatUint(channelID, 10),
|
|
"name": targetPath,
|
|
"cpw": password,
|
|
"clientftfid": strconv.Itoa(int(cftid)),
|
|
"seekpos": "0",
|
|
})
|
|
|
|
err := c.ExecCommand(cmd, 10*time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
select {
|
|
case res := <-ch:
|
|
switch v := res.(type) {
|
|
case FileDownloadInfo:
|
|
return &v, nil
|
|
case FileTransferStatusInfo:
|
|
return nil, fmt.Errorf("%w: %s (status=%d)", errFileTransferFailed, v.Message, v.Status)
|
|
default:
|
|
return nil, fmt.Errorf("%w: %T", errUnexpectedRespType, v)
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
return nil, errFileTransferTimedOut
|
|
}
|
|
}
|
|
|
|
// FileTransferDeleteFile sends ftdeletefile to delete files on the server.
|
|
func (c *Client) FileTransferDeleteFile(channelID uint64, paths []string) error {
|
|
if len(paths) == 0 {
|
|
return nil
|
|
}
|
|
|
|
pathStr := strings.Join(paths, "|")
|
|
cmd := commands.BuildCommand("ftdeletefile", map[string]string{
|
|
"cid": strconv.FormatUint(channelID, 10),
|
|
"cpw": "",
|
|
"name": pathStr,
|
|
})
|
|
|
|
return c.ExecCommand(cmd, 10*time.Second)
|
|
}
|
|
|
|
// DialFileTransfer opens TCP to the TeamSpeak file-transfer port
|
|
// and performs the ftkey handshake. The caller is responsible for closing the
|
|
// returned connection.
|
|
func DialFileTransfer(host string, port uint16, key string) (net.Conn, error) {
|
|
addr := net.JoinHostPort(host, strconv.Itoa(int(port)))
|
|
dialer := &net.Dialer{Timeout: 10 * time.Second}
|
|
conn, err := dialer.DialContext(context.Background(), "tcp", addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to file transfer server %s: %w", addr, err)
|
|
}
|
|
|
|
_, err = conn.Write([]byte(key))
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
|
|
return nil, fmt.Errorf("failed to send transfer key: %w", err)
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// UploadFileData transfers data to the server using credentials from FileTransferInitUpload.
|
|
func UploadFileData(host string, info *FileUploadInfo, data io.Reader) error {
|
|
conn, err := DialFileTransfer(host, info.Port, info.FileTransferKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
_, err = io.Copy(conn, data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upload file data: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DownloadFileData receives data from the server using credentials from FileTransferInitDownload.
|
|
func DownloadFileData(host string, info *FileDownloadInfo, dest io.Writer) error {
|
|
conn, err := DialFileTransfer(host, info.Port, info.FileTransferKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
_, err = io.Copy(dest, conn)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to download file data: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|