feat(export): platform-native delivery — TG popup+downloadFile, VK viewer, mobile share sheet
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Failing after 1m7s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped

Per the on-device review, the last hop is now the most native affordance
per platform, all fed by the same signed URL:

- Telegram: the native showPopup chooser returns (safe here — the whole
  TG chain is bridge calls, which need no user activation) and both
  formats go to the native downloadFile dialog on iOS and Android alike.
- VK: the PNG opens in VK's native photo viewer (VKWebAppShowImages) —
  on screen at once, saved/shared from the viewer's own controls; the
  GCG keeps VKWebAppDownloadFile; either falls back to a plain anchor
  download (the desktop iframe). The gateway now serves /dl/* via
  http.ServeContent (Content-Length + Range/206) — another swing at the
  VK Android DownloadManager hang; if it persists, the next step is the
  clipboard fallback for the VK-Android GCG.
- Mobile browsers: the OS share sheet with the fetched file (the proven
  fetch-then-share pattern) — nothing lands in Downloads first; desktop
  keeps the anchor download.

Legacy Telegram (< 8.0, no downloadFile) keeps the app modal + GCG
clipboard copy and hides the image option.
This commit is contained in:
Ilia Denisov
2026-07-02 22:19:46 +02:00
parent c3c27bba5e
commit c3c26bbbc0
11 changed files with 189 additions and 60 deletions
+6 -5
View File
@@ -7,6 +7,7 @@
package connectsrv
import (
"bytes"
"context"
"crypto/subtle"
"encoding/json"
@@ -14,7 +15,6 @@ import (
"io"
"net"
"net/http"
"strconv"
"strings"
"time"
@@ -466,10 +466,11 @@ func (s *Server) exportDownloadHandler() http.Handler {
}
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Cache-Control", "private, max-age=0")
// Explicit length, or the body goes out chunked and Android's system
// DownloadManager (the VKWebAppDownloadFile executor) hangs on it.
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
_, _ = w.Write(data)
// ServeContent (not a bare Write): the platforms' native downloaders are picky —
// Android's system DownloadManager (the VKWebAppDownloadFile executor) hangs on a
// chunked body of unknown length and may probe with Range. ServeContent emits
// Content-Length, honours Range/If-* and answers 206s from the buffered artifact.
http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(data))
})
}