upload.go 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. package send
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "net"
  8. "net/http"
  9. "os"
  10. "strconv"
  11. "time"
  12. "TengTuToolbox/localsend/logging"
  13. "TengTuToolbox/localsend/model"
  14. )
  15. // uploadFile streams one local file to the receiver over /upload.
  16. func uploadFile(ctx context.Context, client *http.Client, device *model.Device, filePath, fileID, sessionID, token, scheme string, logger *logging.Logger) error {
  17. if logger == nil {
  18. logger = logging.NewQuiet()
  19. }
  20. file, err := os.Open(filePath)
  21. if err != nil {
  22. return fmt.Errorf("failed to open file: %w", err)
  23. }
  24. defer file.Close()
  25. stat, err := file.Stat()
  26. if err != nil {
  27. return fmt.Errorf("failed to get file stats: %w", err)
  28. }
  29. url := fmt.Sprintf("%s://%s/api/localsend/v2/upload?sessionId=%s&fileId=%s&token=%s",
  30. scheme, net.JoinHostPort(device.IP, strconv.Itoa(device.Port)), sessionID, fileID, token)
  31. var body io.ReadCloser = file
  32. // Wrap with idle timeout: cancel request if no data flows for 15s.
  33. uploadCtx, cancel := context.WithCancel(ctx)
  34. defer cancel()
  35. body = newIdleTimeoutReader(body, 15*time.Second, cancel)
  36. defer body.Close()
  37. req, err := http.NewRequestWithContext(uploadCtx, http.MethodPost, url, body)
  38. if err != nil {
  39. cancel()
  40. return fmt.Errorf("failed to create upload request: %w", err)
  41. }
  42. req.Header.Set("Content-Type", "application/octet-stream")
  43. req.ContentLength = stat.Size()
  44. resp, err := client.Do(req)
  45. if err != nil {
  46. if errors.Is(err, context.Canceled) {
  47. return errors.New("upload stalled: no data transmitted for 15s")
  48. }
  49. return fmt.Errorf("failed to send upload request: %w", err)
  50. }
  51. defer resp.Body.Close()
  52. if resp.StatusCode != http.StatusOK {
  53. if resp.StatusCode == http.StatusUnprocessableEntity {
  54. return errors.New("upload rejected: receiver failed integrity verification (422)")
  55. }
  56. return fmt.Errorf("upload request failed with status: %s", resp.Status)
  57. }
  58. return nil
  59. }
  60. // idleTimeoutReader cancels the context if no data is read within the timeout.
  61. type idleTimeoutReader struct {
  62. r io.ReadCloser
  63. idleTimeout time.Duration
  64. timer *time.Timer
  65. cancel func()
  66. }
  67. func newIdleTimeoutReader(r io.ReadCloser, timeout time.Duration, cancel func()) *idleTimeoutReader {
  68. tr := &idleTimeoutReader{r: r, idleTimeout: timeout, cancel: cancel}
  69. tr.timer = time.AfterFunc(timeout, func() { tr.cancel() })
  70. return tr
  71. }
  72. func (tr *idleTimeoutReader) Read(p []byte) (int, error) {
  73. tr.timer.Reset(tr.idleTimeout)
  74. n, err := tr.r.Read(p)
  75. if err != nil {
  76. tr.timer.Stop()
  77. }
  78. return n, err
  79. }
  80. func (tr *idleTimeoutReader) Close() error {
  81. tr.timer.Stop()
  82. return tr.r.Close()
  83. }