1
1
package updater
2
2
3
3
import (
4
+ "archive/tar"
4
5
"archive/zip"
6
+ "compress/gzip"
5
7
"fmt"
6
8
"io"
7
9
"os"
@@ -22,6 +24,14 @@ type UIUpdater struct {
22
24
mutex sync.Mutex
23
25
}
24
26
27
+ type compressionType int
28
+
29
+ const (
30
+ typeUnknown compressionType = iota
31
+ typeZip
32
+ typeTarGzip
33
+ )
34
+
25
35
var DefaultUiUpdater = & UIUpdater {}
26
36
27
37
func NewUiUpdater (externalUI , externalUIURL , externalUIName string ) * UIUpdater {
@@ -70,6 +80,24 @@ func (u *UIUpdater) DownloadUI() error {
70
80
return u .downloadUI ()
71
81
}
72
82
83
+ func detectFileType (data []byte ) compressionType {
84
+ if len (data ) < 4 {
85
+ return typeUnknown
86
+ }
87
+
88
+ // Zip: 0x50 0x4B 0x03 0x04
89
+ if data [0 ] == 0x50 && data [1 ] == 0x4B && data [2 ] == 0x03 && data [3 ] == 0x04 {
90
+ return typeZip
91
+ }
92
+
93
+ // GZip: 0x1F 0x8B
94
+ if data [0 ] == 0x1F && data [1 ] == 0x8B {
95
+ return typeTarGzip
96
+ }
97
+
98
+ return typeUnknown
99
+ }
100
+
73
101
func (u * UIUpdater ) downloadUI () error {
74
102
err := u .prepareUIPath ()
75
103
if err != nil {
@@ -78,12 +106,23 @@ func (u *UIUpdater) downloadUI() error {
78
106
79
107
data , err := downloadForBytes (u .externalUIURL )
80
108
if err != nil {
81
- return fmt .Errorf ("can't download file: %w" , err )
109
+ return fmt .Errorf ("can't download file: %w" , err )
110
+ }
111
+
112
+ fileType := detectFileType (data )
113
+ if fileType == typeUnknown {
114
+ return fmt .Errorf ("unknown or unsupported file type" )
82
115
}
83
116
84
- saved := path .Join (C .Path .HomeDir (), "download.zip" )
117
+ ext := ".zip"
118
+ if fileType == typeTarGzip {
119
+ ext = ".tgz"
120
+ }
121
+
122
+ saved := path .Join (C .Path .HomeDir (), "download" + ext )
123
+ log .Debugln ("compression Type: %s" , ext )
85
124
if err = saveFile (data , saved ); err != nil {
86
- return fmt .Errorf ("can't save zip file: %w" , err )
125
+ return fmt .Errorf ("can't save compressed file: %w" , err )
87
126
}
88
127
defer os .Remove (saved )
89
128
@@ -94,12 +133,12 @@ func (u *UIUpdater) downloadUI() error {
94
133
}
95
134
}
96
135
97
- unzipFolder , err := unzip (saved , C .Path .HomeDir ())
136
+ extractedFolder , err := extract (saved , C .Path .HomeDir ())
98
137
if err != nil {
99
- return fmt .Errorf ("can't extract zip file: %w" , err )
138
+ return fmt .Errorf ("can't extract compressed file: %w" , err )
100
139
}
101
140
102
- err = os .Rename (unzipFolder , u .externalUIPath )
141
+ err = os .Rename (extractedFolder , u .externalUIPath )
103
142
if err != nil {
104
143
return fmt .Errorf ("rename UI folder failed: %w" , err )
105
144
}
@@ -122,9 +161,66 @@ func unzip(src, dest string) (string, error) {
122
161
return "" , err
123
162
}
124
163
defer r .Close ()
164
+
165
+ // check whether or not only exists singleRoot dir
166
+ rootDir := ""
167
+ isSingleRoot := true
168
+ rootItemCount := 0
169
+ for _ , f := range r .File {
170
+ parts := strings .Split (strings .Trim (f .Name , "/" ), "/" )
171
+ if len (parts ) == 0 {
172
+ continue
173
+ }
174
+
175
+ if len (parts ) == 1 {
176
+ isDir := strings .HasSuffix (f .Name , "/" )
177
+ if ! isDir {
178
+ isSingleRoot = false
179
+ break
180
+ }
181
+
182
+ if rootDir == "" {
183
+ rootDir = parts [0 ]
184
+ }
185
+ rootItemCount ++
186
+ }
187
+ }
188
+
189
+ if rootItemCount != 1 {
190
+ isSingleRoot = false
191
+ }
192
+
193
+ // build the dir of extraction
125
194
var extractedFolder string
195
+ if isSingleRoot && rootDir != "" {
196
+ // if the singleRoot, use it directly
197
+ log .Debugln ("Match the singleRoot" )
198
+ extractedFolder = filepath .Join (dest , rootDir )
199
+ log .Debugln ("extractedFolder: %s" , extractedFolder )
200
+ } else {
201
+ log .Debugln ("Match the multiRoot" )
202
+ // or put the files/dirs into new dir
203
+ baseName := filepath .Base (src )
204
+ baseName = strings .TrimSuffix (baseName , filepath .Ext (baseName ))
205
+ extractedFolder = filepath .Join (dest , baseName )
206
+
207
+ for i := 1 ; ; i ++ {
208
+ if _ , err := os .Stat (extractedFolder ); os .IsNotExist (err ) {
209
+ break
210
+ }
211
+ extractedFolder = filepath .Join (dest , fmt .Sprintf ("%s_%d" , baseName , i ))
212
+ }
213
+ log .Debugln ("extractedFolder: %s" , extractedFolder )
214
+ }
215
+
126
216
for _ , f := range r .File {
127
- fpath := filepath .Join (dest , f .Name )
217
+ var fpath string
218
+ if isSingleRoot && rootDir != "" {
219
+ fpath = filepath .Join (dest , f .Name )
220
+ } else {
221
+ fpath = filepath .Join (extractedFolder , f .Name )
222
+ }
223
+
128
224
if ! strings .HasPrefix (fpath , filepath .Clean (dest )+ string (os .PathSeparator )) {
129
225
return "" , fmt .Errorf ("invalid file path: %s" , fpath )
130
226
}
@@ -149,13 +245,158 @@ func unzip(src, dest string) (string, error) {
149
245
if err != nil {
150
246
return "" , err
151
247
}
152
- if extractedFolder == "" {
153
- extractedFolder = filepath .Dir (fpath )
248
+ }
249
+ return extractedFolder , nil
250
+ }
251
+
252
+ func untgz (src , dest string ) (string , error ) {
253
+ file , err := os .Open (src )
254
+ if err != nil {
255
+ return "" , err
256
+ }
257
+ defer file .Close ()
258
+
259
+ gzr , err := gzip .NewReader (file )
260
+ if err != nil {
261
+ return "" , err
262
+ }
263
+ defer gzr .Close ()
264
+
265
+ tr := tar .NewReader (gzr )
266
+
267
+ rootDir := ""
268
+ isSingleRoot := true
269
+ rootItemCount := 0
270
+ for {
271
+ header , err := tr .Next ()
272
+ if err == io .EOF {
273
+ break
274
+ }
275
+ if err != nil {
276
+ return "" , err
277
+ }
278
+
279
+ parts := strings .Split (cleanTarPath (header .Name ), string (os .PathSeparator ))
280
+ if len (parts ) == 0 {
281
+ continue
282
+ }
283
+
284
+ if len (parts ) == 1 {
285
+ isDir := header .Typeflag == tar .TypeDir
286
+ if ! isDir {
287
+ isSingleRoot = false
288
+ break
289
+ }
290
+
291
+ if rootDir == "" {
292
+ rootDir = parts [0 ]
293
+ }
294
+ rootItemCount ++
295
+ }
296
+ }
297
+
298
+ if rootItemCount != 1 {
299
+ isSingleRoot = false
300
+ }
301
+
302
+ file .Seek (0 , 0 )
303
+ gzr , _ = gzip .NewReader (file )
304
+ tr = tar .NewReader (gzr )
305
+
306
+ var extractedFolder string
307
+ if isSingleRoot && rootDir != "" {
308
+ log .Debugln ("Match the singleRoot" )
309
+ extractedFolder = filepath .Join (dest , rootDir )
310
+ log .Debugln ("extractedFolder: %s" , extractedFolder )
311
+ } else {
312
+ log .Debugln ("Match the multiRoot" )
313
+ baseName := filepath .Base (src )
314
+ baseName = strings .TrimSuffix (baseName , filepath .Ext (baseName ))
315
+ baseName = strings .TrimSuffix (baseName , ".tar" )
316
+ extractedFolder = filepath .Join (dest , baseName )
317
+
318
+ for i := 1 ; ; i ++ {
319
+ if _ , err := os .Stat (extractedFolder ); os .IsNotExist (err ) {
320
+ break
321
+ }
322
+ extractedFolder = filepath .Join (dest , fmt .Sprintf ("%s_%d" , baseName , i ))
323
+ }
324
+ log .Debugln ("extractedFolder: %s" , extractedFolder )
325
+ }
326
+
327
+ for {
328
+ header , err := tr .Next ()
329
+ if err == io .EOF {
330
+ break
331
+ }
332
+ if err != nil {
333
+ return "" , err
334
+ }
335
+
336
+ var fpath string
337
+ if isSingleRoot && rootDir != "" {
338
+ fpath = filepath .Join (dest , cleanTarPath (header .Name ))
339
+ } else {
340
+ fpath = filepath .Join (extractedFolder , cleanTarPath (header .Name ))
341
+ }
342
+
343
+ if ! strings .HasPrefix (fpath , filepath .Clean (dest )+ string (os .PathSeparator )) {
344
+ return "" , fmt .Errorf ("invalid file path: %s" , fpath )
345
+ }
346
+
347
+ switch header .Typeflag {
348
+ case tar .TypeDir :
349
+ if err = os .MkdirAll (fpath , os .FileMode (header .Mode )); err != nil {
350
+ return "" , err
351
+ }
352
+ case tar .TypeReg :
353
+ if err = os .MkdirAll (filepath .Dir (fpath ), os .ModePerm ); err != nil {
354
+ return "" , err
355
+ }
356
+ outFile , err := os .OpenFile (fpath , os .O_WRONLY | os .O_CREATE | os .O_TRUNC , os .FileMode (header .Mode ))
357
+ if err != nil {
358
+ return "" , err
359
+ }
360
+ if _ , err := io .Copy (outFile , tr ); err != nil {
361
+ outFile .Close ()
362
+ return "" , err
363
+ }
364
+ outFile .Close ()
154
365
}
155
366
}
156
367
return extractedFolder , nil
157
368
}
158
369
370
+ func extract (src , dest string ) (string , error ) {
371
+ srcLower := strings .ToLower (src )
372
+ switch {
373
+ case strings .HasSuffix (srcLower , ".tar.gz" ) ||
374
+ strings .HasSuffix (srcLower , ".tgz" ):
375
+ return untgz (src , dest )
376
+ case strings .HasSuffix (srcLower , ".zip" ):
377
+ return unzip (src , dest )
378
+ default :
379
+ return "" , fmt .Errorf ("unsupported file format: %s" , src )
380
+ }
381
+ }
382
+
383
+ func cleanTarPath (path string ) string {
384
+ // remove prefix ./ or ../
385
+ path = strings .TrimPrefix (path , "./" )
386
+ path = strings .TrimPrefix (path , "../" )
387
+
388
+ // normalize path
389
+ path = filepath .Clean (path )
390
+
391
+ // transfer delimiters to system std
392
+ path = filepath .FromSlash (path )
393
+
394
+ // remove prefix path delimiters
395
+ path = strings .TrimPrefix (path , string (os .PathSeparator ))
396
+
397
+ return path
398
+ }
399
+
159
400
func cleanup (root string ) error {
160
401
if _ , err := os .Stat (root ); os .IsNotExist (err ) {
161
402
return nil
0 commit comments