| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- package send
- import (
- "context"
- "errors"
- "fmt"
- "io"
- "net"
- "net/http"
- "os"
- "strconv"
- "time"
- "TengTuToolbox/localsend/logging"
- "TengTuToolbox/localsend/model"
- )
- // uploadFile streams one local file to the receiver over /upload.
- func uploadFile(ctx context.Context, client *http.Client, device *model.Device, filePath, fileID, sessionID, token, scheme string, logger *logging.Logger) error {
- if logger == nil {
- logger = logging.NewQuiet()
- }
- file, err := os.Open(filePath)
- if err != nil {
- return fmt.Errorf("failed to open file: %w", err)
- }
- defer file.Close()
- stat, err := file.Stat()
- if err != nil {
- return fmt.Errorf("failed to get file stats: %w", err)
- }
- url := fmt.Sprintf("%s://%s/api/localsend/v2/upload?sessionId=%s&fileId=%s&token=%s",
- scheme, net.JoinHostPort(device.IP, strconv.Itoa(device.Port)), sessionID, fileID, token)
- var body io.ReadCloser = file
- // Wrap with idle timeout: cancel request if no data flows for 15s.
- uploadCtx, cancel := context.WithCancel(ctx)
- defer cancel()
- body = newIdleTimeoutReader(body, 15*time.Second, cancel)
- defer body.Close()
- req, err := http.NewRequestWithContext(uploadCtx, http.MethodPost, url, body)
- if err != nil {
- cancel()
- return fmt.Errorf("failed to create upload request: %w", err)
- }
- req.Header.Set("Content-Type", "application/octet-stream")
- req.ContentLength = stat.Size()
- resp, err := client.Do(req)
- if err != nil {
- if errors.Is(err, context.Canceled) {
- return errors.New("upload stalled: no data transmitted for 15s")
- }
- return fmt.Errorf("failed to send upload request: %w", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- if resp.StatusCode == http.StatusUnprocessableEntity {
- return errors.New("upload rejected: receiver failed integrity verification (422)")
- }
- return fmt.Errorf("upload request failed with status: %s", resp.Status)
- }
- return nil
- }
- // idleTimeoutReader cancels the context if no data is read within the timeout.
- type idleTimeoutReader struct {
- r io.ReadCloser
- idleTimeout time.Duration
- timer *time.Timer
- cancel func()
- }
- func newIdleTimeoutReader(r io.ReadCloser, timeout time.Duration, cancel func()) *idleTimeoutReader {
- tr := &idleTimeoutReader{r: r, idleTimeout: timeout, cancel: cancel}
- tr.timer = time.AfterFunc(timeout, func() { tr.cancel() })
- return tr
- }
- func (tr *idleTimeoutReader) Read(p []byte) (int, error) {
- tr.timer.Reset(tr.idleTimeout)
- n, err := tr.r.Read(p)
- if err != nil {
- tr.timer.Stop()
- }
- return n, err
- }
- func (tr *idleTimeoutReader) Close() error {
- tr.timer.Stop()
- return tr.r.Close()
- }
|