-
Notifications
You must be signed in to change notification settings - Fork 2
/
sync.go
342 lines (307 loc) · 8.63 KB
/
sync.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
package adb
import (
"bytes"
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"time"
"github.com/prife/goadb/wire"
)
func ListAllSubDirs(localDir string) (list []string, err error) {
err = filepath.WalkDir(localDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if path == localDir {
return nil
}
// ignore special file
if d.IsDir() {
relativePath, _ := filepath.Rel(localDir, path)
list = append(list, relativePath)
}
return nil
})
return
}
// adb shell mkdir
// # Android 14
//
// OP5929L1:/ $ mkdir /a /b /c
// mkdir: '/a': Read-only file system
// mkdir: '/b': Read-only file system
// mkdir: '/c': Read-only file system
// 1|OP5929L1:/ $
//
// OP5929L1:/ $ mkdir /sdcard/a /sdcard/b /sdcard/c
// OP5929L1:/ $ mkdir /sdcard/a /sdcard/b /sdcard/c
// mkdir: '/sdcard/a': File exists
// mkdir: '/sdcard/b': File exists
// mkdir: '/sdcard/c': File exists
// 1|OP5929L1:/ $
//
// OP5929L1:/ $ mkdir /data/a /data/b /data/c
// mkdir: '/data/a': Permission denied
// mkdir: '/data/b': Permission denied
// mkdir: '/data/c': Permission denied
// 1|OP5929L1:/ $
//
// OP5929L1:/ $ mkdir /sd/a /sd/b /sd/c
// mkdir: '/sd/a': No such file or directory
// mkdir: '/sd/b': No such file or directory
// mkdir: '/sd/c': No such file or directory
//
// # Android 5.1
//
// shell@A33:/ $ mkdir /sdcard/a /sdcard/b /sdcard/c
// shell@A33:/ $ mkdir /sdcard/a /sdcard/b /sdcard/c
// mkdir failed for /sdcard/a, File exists
// 255|shell@A33:/ $
//
// shell@A33:/ $ mkdir /sd/a /sd/b /sd/c
// mkdir failed for /sd/a, No such file or directory
// 255|shell@A33:/ $
//
// shell@A33:/ $ mkdir /a /b /c
// mkdir failed for /a, Read-only file system
// 255|shell@A33:/ $
//
// shell@A33:/ $ mkdir /data/a /data/b /data/c
// mkdir failed for /data/a, Permission denied
// 255|shell@A33:/ $
func filterFileExistedError(resp []byte) (errs []error) {
lines := bytes.Split(resp, []byte("\n"))
for _, line := range lines {
line := bytes.TrimSpace(line)
if len(line) > 0 && !bytes.HasSuffix(line, []byte("File exists")) {
errs = append(errs, errors.New(string(line)))
}
}
return
}
func (c *Device) Mkdirs(list []string) error {
return c.MkdirsWithParent(list, false)
}
// adb shell mkdir [-p] <dir1> <dir2> ...
func (c *Device) MkdirsWithParent(list []string, withParent bool) error {
var commands []string
var commandsLen int
var errs []error
if withParent {
commands = append(commands, "-p")
}
for _, l := range list {
// adb 这里的长度是32768,但是由于wire/conn.go 中判断最大长度为 MaxPayloadV1Length 4096
// 因此这里使用 4000
if commandsLen+len(l) > 4000 {
resp, err := c.RunCommandTimeout(time.Second*15, "mkdir", commands...)
if err != nil {
return err
}
if len(resp) > 0 {
// fmt.Println("resp:", string(resp))
errs = append(errs, filterFileExistedError(resp)...)
}
commands = make([]string, 0)
if withParent {
commands = append(commands, "-p")
}
commandsLen = 0
}
commands = append(commands, l)
commandsLen = commandsLen + len(l) + 1 // and one space
}
if commandsLen > 0 {
resp, err := c.RunCommandTimeout(time.Second*15, "mkdir", commands...)
if err != nil {
return err
}
if len(resp) > 0 {
errs = append(errs, filterFileExistedError(resp)...)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// Rm run `adb shell rm -rf xx xx`
// it returns is meaning less in most cases, so just ignore error is ok
func (c *Device) Rm(list []string) error {
var commands []string
var commandsLen int
var errs []error
commands = append(commands, "-rf")
for _, l := range list {
if commandsLen+len(l) > (32768 - 7) { // len('rm -rf ') == 6
resp, err := c.RunCommandTimeout(time.Second*15, "rm", commands...)
if err != nil {
return err
}
if len(resp) > 0 {
errs = append(errs, errors.New(string(resp)))
}
// reset commands
commands = make([]string, 0)
commands = append(commands, "-rf")
commandsLen = 0
}
commands = append(commands, l)
commandsLen = commandsLen + len(l) + 1 // and one space
}
if commandsLen > 0 {
resp, err := c.RunCommandTimeout(time.Second*15, "rm", commands...)
if err != nil {
return err
}
if len(resp) > 0 {
errs = append(errs, errors.New(string(resp)))
}
}
return errors.Join(errs...)
}
func (c *Device) PushFile(localPath, remotePath string, handler wire.SyncFileHandler) error {
return c.PushFileCtx(context.Background(), localPath, remotePath, handler)
}
func (c *Device) PushFileCtx(ctx context.Context, localPath, remotePath string, handler wire.SyncFileHandler) error {
linfo, err := os.Lstat(localPath)
if err != nil {
return err
}
if !linfo.Mode().IsRegular() {
return fmt.Errorf("not regular file: %s", localPath)
}
// features, err := c.DeviceFeatures()
// if err != nil {
// return fmt.Errorf("get device features: %w", err)
// }
fconn, err := c.NewSyncConn()
if err != nil {
return err
}
defer fconn.Close()
// if remotePath is dir, just append src file name
rinfo, err := fconn.Stat(remotePath)
if err == nil && rinfo.Mode.IsDir() {
remotePath = remotePath + "/" + linfo.Name()
}
var syncHandler func(n uint64)
if handler != nil {
total := uint64(linfo.Size())
sent := uint64(0)
startTime := time.Now()
percent := 0
syncHandler = func(n uint64) {
sent += n
curPercent := float64(sent) / float64(total) * 100
if int(curPercent) > percent {
speedMBPerSecond := float64(sent) * float64(time.Second) / 1024.0 / 1024.0 / (float64(time.Since(startTime)))
handler(total, sent, curPercent, speedMBPerSecond)
}
percent = int(curPercent)
}
}
ch := make(chan error, 2)
go func() {
err := fconn.PushFile(localPath, remotePath, syncHandler)
ch <- err
}()
select {
case <-ctx.Done():
return fmt.Errorf("push failed by ctx done: %w", ctx.Err())
case err := <-ch:
if err != nil {
return fmt.Errorf("push failed: %w", err)
}
return nil
}
}
// PushDir support push dir
// push 文件夹:
// adb push src-dir dest-dir具有两种行为,与cp命令效果一致
// 1.如果'dest-dir'路径不存在,会创建'dest-dir',其内容与`src-dir`完全一致
// 2.如果'dest-dir'路径存在,会创建'dest-dir/src-dir',其内容与`src-dir`完全一致
//
// 本函数行为如下
// 当 withSrcDir 为 true,永远会在手机上创建src-dir
// 当 withSrcDir 为 false,则仅会 src-dir 的子文件/目录推送到目标文件夹下
func (c *Device) PushDir(local, remote string, withSrcDir bool, handler wire.SyncHandler) (err error) {
return c.PushDirCtx(context.Background(), local, remote, withSrcDir, handler)
}
func (c *Device) PushDirCtx(ctx context.Context, local, remote string, withSrcDir bool, handler wire.SyncHandler) (err error) {
// Android 12 之后,push 可能遇到文件夹权限问题,解决办法
// 1. 先在手机上创建所有文件夹,如果失败则直接返回错误
// 2. 再推送文件
if err := MakeDirs(c, local, remote, withSrcDir); err != nil {
return err
}
// push files
fconn, err := c.NewSyncConn()
if err != nil {
return err
}
defer fconn.Close()
ch := make(chan error, 2)
go func() {
err := fconn.PushDir(withSrcDir, local, remote, handler)
ch <- err
}()
select {
case <-ctx.Done():
return fmt.Errorf("push failed by ctx done: %w", ctx.Err())
case err := <-ch:
if err != nil {
return fmt.Errorf("push failed: %w", err)
}
return nil
}
}
func MakeDirs(c *Device, local string, remote string, withSrcDir bool) (err error) {
local, err = filepath.Abs(local)
if err != nil {
return fmt.Errorf("pushd: get abs path of %s failed: %w", local, err)
}
var baseName string
if withSrcDir {
// use filepath.Base, not path.Base
// https://stackoverflow.com/questions/48050724/how-to-get-correct-file-base-name-on-windows-using-golang
baseName = filepath.Base(local)
}
linfo, err := os.Lstat(local)
if err != nil {
return err
}
if !linfo.IsDir() {
return fmt.Errorf("not dir: %s", local)
}
// mkdir sub dirs
subdirs, err := ListAllSubDirs(local)
if err != nil {
return
}
remoteSubDirs := make([]string, 1+len(subdirs))
if baseName != "" {
remoteSubDirs[0] = remote + "/" + baseName
for i, d := range subdirs {
remoteSubDirs[i+1] = remote + "/" + baseName + "/" + d
}
} else {
remoteSubDirs[0] = remote
for i, d := range subdirs {
remoteSubDirs[i+1] = remote + "/" + d
}
}
err = c.MkdirsWithParent(remoteSubDirs, true)
if err != nil {
// 当创建很多文件夹时(比如推送游戏资源包到手机中),可能会返回一个超长的错误,截断处理
errStr := err.Error()
if len(errStr) > 1024 {
errStr = errStr[:1024]
}
return fmt.Errorf("mkdirs failed: %s", errStr)
}
return nil
}