File: downloader.go

package info (click to toggle)
golang-gitlab-ubports-development-core-go-ldm 0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 120 kB
  • sloc: makefile: 4
file content (410 lines) | stat: -rw-r--r-- 12,604 bytes parent folder | download
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
/*
 * Copyright 2014 Canonical Ltd.
 * Copyright 2024 Guido Berhoerster <guido+ubports@berhoerster.name>
 *
 * Authors:
 * Manuel de la Pena: manuel.delapena@canonical.com
 *
 * This file is part of lomiri-download-manager.
 *
 * lomiri-download-manager is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 3.
 *
 * lomiri-download-manager is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

// Package ldm provides a go interface to work with the lomiri download manager
package ldm

import (
	"errors"
	"runtime"
	"strings"

	"github.com/godbus/dbus/v5"
)

const (
	DOWNLOAD_SERVICE           = "com.lomiri.applications.Downloader"
	DOWNLOAD_INTERFACE         = "com.lomiri.applications.Download"
	DOWNLOAD_MANAGER_INTERFACE = "com.lomiri.applications.DownloadManager"
)

type hashType string

const (
	MD5    hashType = "md5"
	SHA1   hashType = "sha1"
	SHA224 hashType = "sha224"
	SHA256 hashType = "sha256"
	SHA384 hashType = "sha384"
	SHA512 hashType = "sha512"
)

const (
	LOCAL_PATH            = "local-path"
	OBJECT_PATH           = "objectpath"
	POST_DOWNLOAD_COMMAND = "post-download-command"
)

// Download is the common interface of a download. It provides all the required
// methods to interact with a download created by ldm.
type Download interface {
	TotalSize() (uint64, error)
	Progress() (uint64, error)
	Metadata() (map[string]string, error)
	SetThrottle(uint64) error
	Throttle() (uint64, error)
	AllowMobileDownload(bool) error
	SetDestinationDir(string) error
	IsMobileDownload() (bool, error)
	Start() error
	Pause() error
	Resume() error
	Cancel() error
	Started() chan bool
	Paused() chan bool
	DownloadProgress() chan Progress
	Resumed() chan bool
	Canceled() chan bool
	Finished() chan string
	Error() chan error
}

// FileDownload represents a single file being downloaded by ldm.
type FileDownload struct {
	conn       *dbus.Conn
	proxy      proxy
	path       dbus.ObjectPath
	signals    chan *dbus.Signal
	started    chan bool
	paused     chan bool
	resumed    chan bool
	canceled   chan bool
	finished   chan string
	errors     chan error
	progress   chan Progress
	matchOpts  []dbus.MatchOption
}

func (down *FileDownload) free() {
	down.conn.RemoveMatchSignal(down.matchOpts...)
	down.conn.RemoveSignal(down.signals)
	close(down.signals)
}

func cleanDownloadData(down *FileDownload) {
	down.free()
}

func newFileDownload(conn *dbus.Conn, path dbus.ObjectPath) (*FileDownload, error) {
	proxy := conn.Object(DOWNLOAD_SERVICE, path)
	matchOpts := []dbus.MatchOption{
		dbus.WithMatchPathNamespace(path),
		dbus.WithMatchInterface(DOWNLOAD_INTERFACE),
		dbus.WithMatchSender(DOWNLOAD_SERVICE),
	}
	err := conn.AddMatchSignal(matchOpts...)
	if err != nil {
		return nil, err
	}

	down := &FileDownload{
		conn:     conn,
		proxy:    proxy,
		path:     path,
		signals:  make(chan *dbus.Signal),
		started:  make(chan bool),
		paused:   make(chan bool),
		resumed:  make(chan bool),
		canceled: make(chan bool),
		finished: make(chan string),
		errors:   make(chan error),
		progress: make(chan Progress),
		matchOpts: matchOpts,
	}

	// deliver signals over respective channels
	go func() {
		for s := range down.signals {
			i := strings.LastIndex(s.Name, ".")
			iface, member := s.Name[:i], s.Name[i+1:]
			if s.Sender != DOWNLOAD_SERVICE || s.Path != path || iface != DOWNLOAD_INTERFACE {
				continue
			}
			switch member {
			case "started":
				var started bool
				readSignalArgs(s, &started)
				down.started <- started
			case "paused":
				var paused bool
				readSignalArgs(s, &paused)
				down.paused <- paused
			case "resumed":
				var resumed bool
				readSignalArgs(s, &resumed)
				down.resumed <- resumed
			case "canceled":
				var canceled bool
				readSignalArgs(s, &canceled)
				down.canceled <- canceled
			case "finished":
				var path string
				readSignalArgs(s, &path)
				down.finished <- path
			case "error":
				var reason string
				readSignalArgs(s, &reason)
				down.errors <- errors.New(reason)
			case "progress":
				var received uint64
				var total uint64
				readSignalArgs(s, &received, &total)
				down.progress <- Progress{received, total}
			}
		}
		close(down.started)
		close(down.paused)
		close(down.resumed)
		close(down.canceled)
		close(down.finished)
		close(down.errors)
		close(down.progress)
	}()
	conn.Signal(down.signals)

	runtime.SetFinalizer(down, cleanDownloadData)
	return down, nil
}

// TotalSize returns the total size of the file being downloaded.
func (down *FileDownload) TotalSize() (size uint64, err error) {
	return getUint64Value(down.proxy, DOWNLOAD_INTERFACE + ".totalSize")
}

// Process returns the process so far in downloading the file.
func (down *FileDownload) Progress() (progress uint64, err error) {
	return getUint64Value(down.proxy, DOWNLOAD_INTERFACE + ".progress")
}

// Metadata returns the metadata that was provided at creating time to the download.
func (down *FileDownload) Metadata() (metadata map[string]string, err error) {
	return getMetadataMap(down.proxy, DOWNLOAD_INTERFACE + ".metadata")
}

// SetThrottle sets the network throttle to be used in the download.
func (down *FileDownload) SetThrottle(throttle uint64) (err error) {
	return setUint64Value(down.proxy, DOWNLOAD_INTERFACE + ".setThrottle", throttle)
}

// Throttle returns the network throttle that is currently used in the download.
func (down *FileDownload) Throttle() (throttle uint64, err error) {
	return getUint64Value(down.proxy, DOWNLOAD_INTERFACE + ".throttle")
}

// AllowMobileDownload returns if the download is allow to use the mobile connect
// connection.
func (down *FileDownload) AllowMobileDownload(allowed bool) (err error) {
	return down.proxy.Call(DOWNLOAD_INTERFACE + ".allowGSMDownload", 0, allowed).Err
}

// SetDestinationDir permits unconfined applications to set the destination
// directory of the download. This method must be called BEFORE the download
// is started else an error will be returned.
func (down *FileDownload) SetDestinationDir(path string) (err error) {
	return setStringValue(down.proxy, DOWNLOAD_INTERFACE + ".setDestinationDir", path)
}

// IsMobileDownload returns if the download will be performed over the mobile data.
func (down *FileDownload) IsMobileDownload() (allowed bool, err error) {
	return getBoolValue(down.proxy, DOWNLOAD_INTERFACE + ".isGSMDownloadAllowed")
}

// Start tells ldm that the download is ready to be peformed and that the client is
// ready to recieve signals. The following is a common pattern to be used when
// creating downloads in ldm.
//
//     man, err := ldm.NewDownloadManager()
//     if err != nil {
//     }
//
//     // variables used to create the download
//
//     url := "http://www.python.org/ftp/python/3.3.3/Python-3.3.3.tar.xz"
//     hash := "8af44d33ea3a1528fc56b3a362924500"
//     hashAlgo := MD5
//     var metadata map[string]interface{}
//     var headers map[string]string
//
//     // create the download BUT do not start downloading just yet
//     down, err := man.CreateDownload(url, hash, hashAlgo, metadata, headers)
//
//     // connect routines to the download channels so that we can get the
//     // information of the download the channel will not get any data until the
//     // Start is called.
//
//     started_signal := down.Started()
//     go func() {
//         <-started_signal
//         fmt.Println("Download started")
//     }()
//     progress_signal := down.DownloadProgress()
//     go func() {
//         for progress := range p {
//             fmt.Printf("Recieved %d out of %d\n", progress.Received, progress.Total)
//         }
//     }()
//
//     finished_signal := down.Finished()
//
//     // start download
//     down.Start()
//
//     // block until we are finished downloading
//     <- finished_signal
func (down *FileDownload) Start() (err error) {
	return down.proxy.Call(DOWNLOAD_INTERFACE + ".start", 0).Err
}

// Pause pauses a download that was started and if not nothing is done.
func (down *FileDownload) Pause() (err error) {
	return down.proxy.Call(DOWNLOAD_INTERFACE + ".pause", 0).Err
}

// Resumes a download that was paused or does nothing otherwise.
func (down *FileDownload) Resume() (err error) {
	return down.proxy.Call(DOWNLOAD_INTERFACE + ".resume", 0).Err
}

// Cancel cancels a download that was in process and deletes any local files
// that were created.
func (down *FileDownload) Cancel() (err error) {
	return down.proxy.Call(DOWNLOAD_INTERFACE + ".cancel", 0).Err
}

// Started returns a channel that will be used to communicate the started signals.
func (down *FileDownload) Started() chan bool {
	return down.started
}

// Paused returns a channel that will be used to communicate the paused signals.
func (down *FileDownload) Paused() chan bool {
	return down.paused
}

// DownloadProgress returns a channel that will be used to communicate the progress
// signals.
func (down *FileDownload) DownloadProgress() chan Progress {
	return down.progress
}

// Resumed returns a channel that will be used to communicate the paused signals.
func (down *FileDownload) Resumed() chan bool {
	return down.resumed
}

// Canceled returns a channel that will be used to communicate the canceled signals.
func (down *FileDownload) Canceled() chan bool {
	return down.canceled
}

// Finished returns a channel that will ne used to communicate the finished signals.
func (down *FileDownload) Finished() chan string {
	return down.finished
}

// Error returns the channel that will be used to communicate the error signals.
func (down *FileDownload) Error() chan error {
	return down.errors
}

type DownloadManager struct {
	conn  *dbus.Conn
	proxy dbus.BusObject
}

// NewDownloadManager creates a new manager that can be used to create download in the
// ldm daemon.
func NewDownloadManager() (*DownloadManager, error) {
	conn, err := dbus.ConnectSessionBus()
	if err != nil {
		return nil, err
	}

	proxy := conn.Object(DOWNLOAD_SERVICE, "/")
	d := DownloadManager{conn, proxy}
	return &d, nil
}

// CreateDownload creates a new download in the ldm daemon that can be used to get
// a remote resource. LDM allows to pass a hash signature and method that will be
// check once the download has been complited.
//
// The download hash can be one of the the following constants:
//
//   MD5
//   SHA1
//   SHA224
//   SHA256
//   SHA384
//   SHA512
//
// The metadata attribute can be used to pass extra information to the ldm daemon
// that will just be considered if the caller is not a apparmor confined application.
//
//     LOCAL_PATH => allows to provide the local path for the download.
//     OBJECT_PATH => allows to provide the object path to be used in the dbus daemon.
//     POST_DOWNLOAD_COMMAND => allows to provide a command that will be executed on the
//         download
//
// The headers attribute allows to provide extra headers to be used in the request used
// to perform the download.
func (man *DownloadManager) CreateDownload(url string, hash string, algo hashType, metadata map[string]interface{}, headers map[string]string) (down Download, err error) {
	var t map[string]dbus.Variant
	for key, value := range metadata {
		t[key] = dbus.MakeVariant(value)
	}
	s := struct {
		U  string
		H  string
		A  string
		M  map[string]dbus.Variant
		HD map[string]string
	}{url, hash, string(algo), t, headers}
	c := man.proxy.Call(DOWNLOAD_MANAGER_INTERFACE + ".createDownload", 0, s)
	if c.Err != nil {
		return nil, c.Err
	}

	var path dbus.ObjectPath
	if err = readCallArgs(c, &path); err != nil {
		return nil, err
	}
	down, err = newFileDownload(man.conn, path)
	return down, err
}

// CreateMmsDownload creates an mms download that will be performed right away. An
// mms download only uses mobile that and an apn proxy to download a multime media
// message.
func (man *DownloadManager) CreateMmsDownload(url string, hostname string, port int32) (down Download, err error) {
	var path dbus.ObjectPath
	c := man.proxy.Call(DOWNLOAD_MANAGER_INTERFACE + ".createMmsDownload", 0, url, hostname, port)
	if c.Err != nil {
		return nil, c.Err
	}

	if err = readCallArgs(c, &path); err != nil {
		return nil, err
	}
	down, err = newFileDownload(man.conn, path)
	return down, err
}