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() }