adb-toolkit/backend_transfer.go
jegly 26d82c68d5 Add GSI Loader, APK Audit, Firmware, Intent Lab, Screen Mirror, Magisk, and App Lock features; bump to v1.2.0
- New sidebar views: GSI Loader (DSU + permanent fastboot flash), APK Audit,
  Firmware, Intent Lab, Screen Mirror
- New backend modules: backend_gsi.go, backend_apkaudit*.go, backend_firmware.go,
  backend_intent.go, backend_magisk.go, backend_applock.go, backend_scrcpy.go,
  backend_payload.go, backend_privacy.go, backend_overview.go, backend_bootinfo.go,
  backend_flasher.go, backend_transfer.go, backend_filehttp.go
- App Lock / danger-gate infrastructure (DangerGate, LockGate, applock.ts)
- Privileged uninstall via embedded Android helper dex (android-helper/)
- nfpm packaging version bump to 1.2.0
2026-07-06 15:17:59 +10:00

211 lines
5.6 KiB
Go

package main
import (
"bufio"
"bytes"
"context"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// Streaming push/pull with live progress. adb prints transfer progress as
// `[ 42%] path` lines terminated by a carriage return; we split on \r as well
// as \n so each update is its own token, parse the percentage, and forward it
// to the frontend via transfer:* events. ETA is computed frontend-side from the
// percentage over elapsed time. Integrates with beginCancellableOp so the
// existing Cancel button (CancelOperation) aborts a transfer.
var transferPercentRe = regexp.MustCompile(`(\d+)%`)
// scanCRLF is a bufio.SplitFunc that breaks on either \n or \r, so adb's
// carriage-return progress updates surface as discrete tokens.
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
for i, b := range data {
if b == '\n' || b == '\r' {
return i + 1, data[:i], nil
}
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
func baseName(p string) string {
p = strings.TrimRight(p, "/")
if i := strings.LastIndexAny(p, `/\`); i >= 0 {
return p[i+1:]
}
return p
}
// runTransfer runs an adb push/pull, emitting transfer:progress events as it
// goes. kind is "push" or "pull"; label is the item name shown in the UI.
func (a *App) runTransfer(ctx context.Context, kind, label string, args ...string) error {
adbPath, err := a.getBinaryPath("adb")
if err != nil {
return err
}
cmd := exec.CommandContext(ctx, adbPath, args...)
setCommandSysProcAttr(cmd)
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("stdout pipe: %w", err)
}
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start adb %s: %w", kind, err)
}
emit := func(percent int) {
runtime.EventsEmit(a.ctx, "transfer:progress", map[string]interface{}{
"kind": kind, "label": label, "percent": percent,
})
}
emit(0)
scanner := bufio.NewScanner(stdout)
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
scanner.Split(scanCRLF)
last := -1
for scanner.Scan() {
tok := strings.TrimSpace(scanner.Text())
if tok == "" {
continue
}
if m := transferPercentRe.FindStringSubmatch(tok); m != nil {
if p, perr := strconv.Atoi(m[1]); perr == nil && p != last {
last = p
emit(p)
}
}
}
if werr := cmd.Wait(); werr != nil {
if ctx.Err() == context.Canceled {
return fmt.Errorf("cancelled")
}
msg := strings.TrimSpace(stderr.String())
if msg == "" {
msg = werr.Error()
}
return fmt.Errorf("%s", msg)
}
emit(100)
return nil
}
// PushWithProgress pushes a local file into remoteDir, streaming progress.
func (a *App) PushWithProgress(localPath, remoteDir string) (string, error) {
ctx, cancel := a.beginCancellableOp(60 * time.Minute)
defer cancel()
name := baseName(localPath)
err := a.runTransfer(ctx, "push", name, "push", localPath, remoteDir)
runtime.EventsEmit(a.ctx, "transfer:done", nil)
if err != nil {
if strings.Contains(err.Error(), "cancelled") {
return "", fmt.Errorf("push cancelled")
}
return "", fmt.Errorf("push failed: %w", err)
}
return fmt.Sprintf("Pushed %s", name), nil
}
// PushPathsWithProgress pushes the given local files into remoteDir on the
// device, one at a time, each with its own progress bar. Used by the Computer
// browser's "Push to device" action.
func (a *App) PushPathsWithProgress(localPaths []string, remoteDir string) (string, error) {
if len(localPaths) == 0 {
return "", fmt.Errorf("no files selected")
}
if strings.TrimSpace(remoteDir) == "" {
return "", fmt.Errorf("no device destination")
}
ctx, cancel := a.beginCancellableOp(60 * time.Minute)
defer cancel()
var ok, fail int
var details strings.Builder
for _, lp := range localPaths {
name := baseName(lp)
err := a.runTransfer(ctx, "push", name, "push", lp, remoteDir)
if err != nil {
if strings.Contains(err.Error(), "cancelled") {
details.WriteString(fmt.Sprintf("• %s: cancelled\n", name))
fail++
break
}
fail++
details.WriteString(fmt.Sprintf("• %s: %v\n", name, err))
} else {
ok++
}
}
runtime.EventsEmit(a.ctx, "transfer:done", nil)
summary := fmt.Sprintf("Pushed %d item(s) to %s.", ok, remoteDir)
if fail > 0 {
summary += fmt.Sprintf(" Failed: %d\n%s", fail, details.String())
}
return summary, nil
}
// PullPathsWithProgress pulls the given remote paths into a user-chosen local
// directory, one at a time, each with its own progress bar.
func (a *App) PullPathsWithProgress(remotePaths []string) (string, error) {
if len(remotePaths) == 0 {
return "", fmt.Errorf("no files selected")
}
localDir, err := a.SelectDirectoryForPull()
if err != nil {
return "", fmt.Errorf("folder dialog failed: %w", err)
}
if localDir == "" {
return "Pull cancelled.", nil
}
ctx, cancel := a.beginCancellableOp(0)
defer cancel()
var ok, fail int
var details strings.Builder
for _, rp := range remotePaths {
name := baseName(rp)
// -a preserves timestamps
err := a.runTransfer(ctx, "pull", name, "pull", "-a", rp, localDir)
if err != nil {
if strings.Contains(err.Error(), "cancelled") {
details.WriteString(fmt.Sprintf("• %s: cancelled\n", name))
fail++
break
}
fail++
details.WriteString(fmt.Sprintf("• %s: %v\n", name, err))
} else {
ok++
}
}
runtime.EventsEmit(a.ctx, "transfer:done", nil)
summary := fmt.Sprintf("Pulled %d item(s) to %s.", ok, localDir)
if fail > 0 {
summary += fmt.Sprintf(" Failed: %d\n%s", fail, details.String())
}
return summary, nil
}