/
Download.go
249 lines (198 loc) · 8.89 KB
/
Download.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
/*
File Username: Download.go
Copyright: 2021 Peernet Foundation s.r.o.
Author: Peter Kleissner
*/
package webapi
import (
"encoding/hex"
"math"
"net/http"
"os"
"strconv"
"sync"
"time"
"github.com/newinfoOffical/core"
"github.com/google/uuid"
)
type apiResponseDownloadStatus struct {
APIStatus int `json:"apistatus"` // Status of the API call. See DownloadResponseX.
ID uuid.UUID `json:"id"` // Download ID. This can be used to query the latest status and take actions.
DownloadStatus int `json:"downloadstatus"` // Status of the download. See DownloadX.
File apiFile `json:"file"` // File information. Only available for status >= DownloadWaitSwarm.
Progress struct {
TotalSize uint64 `json:"totalsize"` // Total size in bytes.
DownloadedSize uint64 `json:"downloadedsize"` // Count of bytes download so far.
Percentage float64 `json:"percentage"` // Percentage downloaded. Rounded to 2 decimal points. Between 0.00 and 100.00.
} `json:"progress"` // Progress of the download. Only valid for status >= DownloadWaitSwarm.
Swarm struct {
CountPeers uint64 `json:"countpeers"` // Count of peers participating in the swarm.
} `json:"swarm"` // Information about the swarm. Only valid for status >= DownloadActive.
}
const (
DownloadResponseSuccess = 0 // Success
DownloadResponseIDNotFound = 1 // Error: Download ID not found.
DownloadResponseFileInvalid = 2 // Error: Target file cannot be used. For example, permissions denied to create it.
DownloadResponseActionInvalid = 4 // Error: Invalid action. Pausing a non-active download, resuming a non-paused download, or canceling already canceled or finished download.
DownloadResponseFileWrite = 5 // Error writing file.
)
// Download status list
const (
DownloadWaitMetadata = 0 // Wait for file metadata.
DownloadWaitSwarm = 1 // Wait to join swarm.
DownloadActive = 2 // Active downloading. It could still be stuck at any percentage (including 0%) if no seeders are available.
DownloadPause = 3 // Paused by the user.
DownloadCanceled = 4 // Canceled by the user before the download finished. Once canceled, a new download has to be started if the file shall be downloaded.
DownloadFinished = 5 // Download finished 100%.
)
/*
apiDownloadStart starts the download of a file. The path is the full path on disk to store the file.
The hash parameter identifies the file to download. The node ID identifies the blockchain (i.e., the "owner" of the file).
Request: GET /download/start?path=[target path on disk]&hash=[file hash to download]&node=[node ID]
Result: 200 with JSON structure apiResponseDownloadStatus
*/
func (api *WebapiInstance) apiDownloadStart(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
// validate hashes, must be blake3
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
nodeID, valid2 := DecodeBlake3Hash(r.Form.Get("node"))
if !valid1 || !valid2 {
http.Error(w, "", http.StatusBadRequest)
return
}
filePath := r.Form.Get("path")
if filePath == "" {
http.Error(w, "", http.StatusBadRequest)
return
}
info := &downloadInfo{backend: api.Backend, api: api, id: uuid.New(), created: time.Now(), hash: hash, nodeID: nodeID}
api.Backend.LogError("Download.DownloadStart", "output %v", downloadInfo{backend: api.Backend, api: api, id: uuid.New(), created: time.Now(), hash: hash, nodeID: nodeID})
// create the file immediately
if info.initDiskFile(filePath) != nil {
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseFileInvalid})
return
}
// add the download to the list
api.downloadAdd(info)
// start the download!
go info.Start()
api.Backend.LogError("Download.DownloadStart", "output %v", apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: DownloadWaitMetadata})
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: DownloadWaitMetadata})
}
/*
apiDownloadStatus returns the status of an active download.
Request: GET /download/status?id=[download ID]
Result: 200 with JSON structure apiResponseDownloadStatus
*/
func (api *WebapiInstance) apiDownloadStatus(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
id, err := uuid.Parse(r.Form.Get("id"))
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
info := api.downloadLookup(id)
if info == nil {
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
return
}
info.RLock()
response := apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: info.status}
if info.status >= DownloadWaitSwarm {
response.File = info.file
response.Progress.TotalSize = info.file.Size
response.Progress.DownloadedSize = info.DiskFile.StoredSize
response.Progress.Percentage = math.Round(float64(info.DiskFile.StoredSize)/float64(info.file.Size)*100*100) / 100
}
if info.status >= DownloadActive {
response.Swarm.CountPeers = info.Swarm.CountPeers
}
info.RUnlock()
api.Backend.LogError("Download.DownloadStatus", "output %v", response)
EncodeJSON(api.Backend, w, r, response)
}
/*
apiDownloadAction pauses, resumes, and cancels a download. Once canceled, a new download has to be started if the file shall be downloaded.
Only active downloads can be paused. While a download is in discovery phase (querying metadata, joining swarm), it can only be canceled.
Action: 0 = Pause, 1 = Resume, 2 = Cancel.
Request: GET /download/action?id=[download ID]&action=[action]
Result: 200 with JSON structure apiResponseDownloadStatus (using APIStatus and DownloadStatus)
*/
func (api *WebapiInstance) apiDownloadAction(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
id, err := uuid.Parse(r.Form.Get("id"))
action, err2 := strconv.Atoi(r.Form.Get("action"))
if err != nil || err2 != nil || action < 0 || action > 2 {
http.Error(w, "", http.StatusBadRequest)
return
}
info := api.downloadLookup(id)
if info == nil {
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
return
}
apiStatus := 0
switch action {
case 0: // Pause
apiStatus = info.Pause()
case 1: // Resume
apiStatus = info.Resume()
case 2: // Cancel
apiStatus = info.Cancel()
}
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: apiStatus, ID: info.id, DownloadStatus: info.status})
}
// ---- download tracking ----
type downloadInfo struct {
id uuid.UUID // Download ID
status int // Current status. See DownloadX.
sync.RWMutex // Mutext for changing the status
// input
hash []byte // File hash
nodeID []byte // Node ID of the owner
// runtime data
created time.Time // When the download was created.
ended time.Time // When the download was finished (only status = DownloadFinished).
file apiFile // File metadata (only status >= DownloadWaitSwarm)
DiskFile struct { // Target file on disk to store downloaded data
Name string // File name
Handle *os.File // Target file (on disk) to store downloaded data
StoredSize uint64 // Count of bytes downloaded and stored in the file
}
Swarm struct { // Information about the swarm. Only valid for status >= DownloadActive.
CountPeers uint64 // Count of peers participating in the swarm.
}
// live connections, to be changed
peer *core.PeerInfo
api *WebapiInstance
backend *core.Backend
}
func (api *WebapiInstance) downloadAdd(info *downloadInfo) {
api.downloadsMutex.Lock()
api.downloads[info.id] = info
api.downloadsMutex.Unlock()
}
func (api *WebapiInstance) downloadDelete(id uuid.UUID) {
api.downloadsMutex.Lock()
delete(api.downloads, id)
api.downloadsMutex.Unlock()
}
func (api *WebapiInstance) downloadLookup(id uuid.UUID) (info *downloadInfo) {
api.downloadsMutex.Lock()
info = api.downloads[id]
api.downloadsMutex.Unlock()
return info
}
// DeleteDefer deletes the download from the downloads list after the given duration.
// It does not wait for the download to be finished.
func (info *downloadInfo) DeleteDefer(Duration time.Duration) {
go func() {
<-time.After(Duration)
info.api.downloadDelete(info.id)
}()
}
// DecodeBlake3Hash decodes a blake3 hash that is hex encoded
func DecodeBlake3Hash(text string) (hash []byte, valid bool) {
hash, err := hex.DecodeString(text)
return hash, err == nil && len(hash) == 256/8
}