mirror of
https://github.com/PeernetOfficial/core.git
synced 2026-07-17 02:47:51 +01:00
Finished dummy download API.
This commit is contained in:
@@ -60,6 +60,7 @@ func Start(ListenAddresses []string, UseSSL bool, CertificateFile, CertificateKe
|
||||
Router.HandleFunc("/file/format", apiFileFormat).Methods("GET")
|
||||
Router.HandleFunc("/download/start", apiDownloadStart).Methods("GET")
|
||||
Router.HandleFunc("/download/status", apiDownloadStatus).Methods("GET")
|
||||
Router.HandleFunc("/download/action", apiDownloadAction).Methods("GET")
|
||||
|
||||
for _, listen := range ListenAddresses {
|
||||
go startWebServer(listen, UseSSL, CertificateFile, CertificateKey, Router, "API", TimeoutRead, TimeoutWrite)
|
||||
|
||||
129
webapi/Download Temp.go
Normal file
129
webapi/Download Temp.go
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
File Name: Download Temp.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
|
||||
Temporary download code to provide dummy results for testing. To be replaced!
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Start is a dummy downloader
|
||||
func (info *downloadInfo) Start() {
|
||||
time.Sleep(time.Second * time.Duration(rand.Intn(5)))
|
||||
|
||||
// request metadata
|
||||
info.file = createTestResult(-1)
|
||||
info.fileU = blockRecordFileToAPI(info.file)
|
||||
|
||||
// join swarm
|
||||
|
||||
info.status = DownloadActive
|
||||
|
||||
// start download
|
||||
for n := uint64(0); n < 10; n++ {
|
||||
time.Sleep(time.Second * time.Duration(rand.Intn(5)))
|
||||
|
||||
randomData := make([]byte, info.file.Size/10)
|
||||
rand.Read(randomData)
|
||||
|
||||
info.storeDownloadData(randomData, n*info.file.Size/10)
|
||||
}
|
||||
|
||||
info.Finish()
|
||||
info.DeleteDefer(time.Hour * 1) // cache the details for 1 hour before removing
|
||||
}
|
||||
|
||||
// Pause pauses the download. Status is DownloadResponseX.
|
||||
func (info *downloadInfo) Pause() (status int) {
|
||||
info.Lock()
|
||||
defer info.Unlock()
|
||||
|
||||
if info.status != DownloadActive { // The download must be active to be paused.
|
||||
return DownloadResponseActionInvalid
|
||||
}
|
||||
|
||||
info.status = DownloadPause
|
||||
|
||||
return DownloadResponseSuccess
|
||||
}
|
||||
|
||||
// Resume resumes the download. Status is DownloadResponseX.
|
||||
func (info *downloadInfo) Resume() (status int) {
|
||||
info.Lock()
|
||||
defer info.Unlock()
|
||||
|
||||
if info.status != DownloadPause { // The download must be paused to resume.
|
||||
return DownloadResponseActionInvalid
|
||||
}
|
||||
|
||||
info.status = DownloadActive
|
||||
|
||||
return DownloadResponseSuccess
|
||||
}
|
||||
|
||||
// Cancel cancels the download. Status is DownloadResponseX.
|
||||
func (info *downloadInfo) Cancel() (status int) {
|
||||
info.Lock()
|
||||
defer info.Unlock()
|
||||
|
||||
if info.status >= DownloadCanceled { // The download must not be already canceled or finished.
|
||||
return DownloadResponseActionInvalid
|
||||
}
|
||||
|
||||
info.status = DownloadCanceled
|
||||
info.DiskFile.Handle.Close()
|
||||
|
||||
return DownloadResponseSuccess
|
||||
}
|
||||
|
||||
// Finish marks the download as finished.
|
||||
func (info *downloadInfo) Finish() (status int) {
|
||||
info.Lock()
|
||||
defer info.Unlock()
|
||||
|
||||
if info.status != DownloadActive { // The download must be active.
|
||||
return DownloadResponseActionInvalid
|
||||
}
|
||||
|
||||
info.status = DownloadFinished
|
||||
info.DiskFile.Handle.Close()
|
||||
|
||||
return DownloadResponseSuccess
|
||||
}
|
||||
|
||||
// initDiskFile creates the target file
|
||||
func (info *downloadInfo) initDiskFile(path string) (err error) {
|
||||
info.DiskFile.Name = path
|
||||
info.DiskFile.Handle, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// storeDownloadData stores downloaded data. It does not change the download status.
|
||||
func (info *downloadInfo) storeDownloadData(data []byte, offset uint64) (status int) {
|
||||
info.Lock()
|
||||
defer info.Unlock()
|
||||
|
||||
if info.status != DownloadActive { // The download must be active.
|
||||
return DownloadResponseActionInvalid
|
||||
}
|
||||
|
||||
//if _, err := info.DiskFile.Handle.Seek(int64(offset), 0); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
if _, err := info.DiskFile.Handle.WriteAt(data, int64(offset)); err != nil {
|
||||
return DownloadResponseFileWrite
|
||||
}
|
||||
|
||||
info.DiskFile.StoredSize += uint64(len(data))
|
||||
|
||||
return DownloadResponseSuccess
|
||||
}
|
||||
@@ -6,51 +6,238 @@ Author: Peter Kleissner
|
||||
|
||||
package webapi
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type apiResponseDownloadStatus struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error download not found
|
||||
// TODO: Add progress size, total size, number of peers in swarm, etc.
|
||||
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 apiBlockRecordFile `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.
|
||||
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. This only means it joined a swarm. 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 blockchain parameter is the node ID of the peer who shared the file on its blockchain (i.e., the "owner" of 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]&blockchain=[node ID]
|
||||
Request: GET /download/start?path=[target path on disk]&hash=[file hash to download]&node=[node ID]
|
||||
Result: 200 with JSON structure apiResponseDownloadStatus
|
||||
*/
|
||||
func apiDownloadStart(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
filePath := r.Form.Get("path")
|
||||
hash := r.Form.Get("hash")
|
||||
blockchain := r.Form.Get("blockchain")
|
||||
if filePath == "" || hash == "" || blockchain == "" {
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// TODO
|
||||
filePath := r.Form.Get("path")
|
||||
if filePath == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
EncodeJSON(w, r, apiResponseDownloadStatus{Status: 0})
|
||||
info := &downloadInfo{id: uuid.New(), created: time.Now(), hash: hash, nodeID: nodeID}
|
||||
|
||||
// create the file immediately
|
||||
if info.initDiskFile(filePath) != nil {
|
||||
EncodeJSON(w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseFileInvalid})
|
||||
return
|
||||
}
|
||||
|
||||
// add the download to the list
|
||||
downloadAdd(info)
|
||||
|
||||
// start the download!
|
||||
go info.Start()
|
||||
|
||||
EncodeJSON(w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: DownloadWaitMetadata})
|
||||
}
|
||||
|
||||
/*
|
||||
apiDownloadStatus returns the status of an active download. The hash and blockchain parameters must be the same as /download/start.
|
||||
apiDownloadStatus returns the status of an active download.
|
||||
|
||||
Request: GET /download/status?hash=[file hash]&blockchain=[node ID]
|
||||
Request: GET /download/status?id=[download ID]
|
||||
Result: 200 with JSON structure apiResponseDownloadStatus
|
||||
*/
|
||||
func apiDownloadStatus(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash := r.Form.Get("hash")
|
||||
blockchain := r.Form.Get("blockchain")
|
||||
if hash == "" || blockchain == "" {
|
||||
id, err := uuid.Parse(r.Form.Get("id"))
|
||||
if err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
info := downloadLookup(id)
|
||||
if info == nil {
|
||||
EncodeJSON(w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
return
|
||||
}
|
||||
|
||||
EncodeJSON(w, r, apiResponseDownloadStatus{Status: 1})
|
||||
info.RLock()
|
||||
|
||||
response := apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: info.status}
|
||||
|
||||
if info.status >= DownloadWaitSwarm {
|
||||
response.File = info.fileU
|
||||
|
||||
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()
|
||||
|
||||
EncodeJSON(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 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 := downloadLookup(id)
|
||||
if info == nil {
|
||||
EncodeJSON(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(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 core.BlockRecordFile // File metadata (only status >= DownloadWaitSwarm)
|
||||
fileU apiBlockRecordFile // Same as file metadata, but encoded for API
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
downloads = make(map[uuid.UUID]*downloadInfo)
|
||||
downloadsMutex sync.RWMutex
|
||||
)
|
||||
|
||||
func downloadAdd(info *downloadInfo) {
|
||||
downloadsMutex.Lock()
|
||||
downloads[info.id] = info
|
||||
downloadsMutex.Unlock()
|
||||
}
|
||||
|
||||
func downloadDelete(id uuid.UUID) {
|
||||
downloadsMutex.Lock()
|
||||
delete(downloads, id)
|
||||
downloadsMutex.Unlock()
|
||||
}
|
||||
|
||||
func downloadLookup(id uuid.UUID) (info *downloadInfo) {
|
||||
downloadsMutex.Lock()
|
||||
info = downloads[id]
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func (job *SearchJob) ReturnNext(Limit int) (Result []*core.BlockRecordFile) {
|
||||
|
||||
// createTestResult creates a test file. fileType = -1 for any.
|
||||
func createTestResult(fileType int) (file core.BlockRecordFile) {
|
||||
randomData := make([]byte, 10)
|
||||
randomData := make([]byte, 10*1024)
|
||||
rand.Read(randomData)
|
||||
|
||||
file.Hash = core.Data2Hash(randomData)
|
||||
|
||||
@@ -48,6 +48,7 @@ These are the functions provided by the API:
|
||||
|
||||
/download/start Start the download of a file
|
||||
/download/status Get the status of a download
|
||||
/download/action Pause, resume, and cancel a download
|
||||
|
||||
/explore List recently shared files
|
||||
|
||||
@@ -601,31 +602,117 @@ Request: GET /search/terminate?id=[UUID]
|
||||
Response: 204 Empty
|
||||
```
|
||||
|
||||
## Download API
|
||||
|
||||
Downloads can have these status types:
|
||||
|
||||
| Status | Constant | Info |
|
||||
|------|----------------|-------------------------------|
|
||||
| 0 | DownloadWaitMetadata | Wait for file metadata. |
|
||||
| 1 | DownloadWaitSwarm | Wait to join swarm. |
|
||||
| 2 | DownloadActive | Active downloading. This only means it joined a swarm. It could still be stuck at any percentage (including 0%) if no seeders are available. |
|
||||
| 3 | DownloadPause | Paused by the user. |
|
||||
| 4 | DownloadCanceled | Canceled by the user before the download finished. Once canceled, a new download has to be started if the file shall be downloaded. |
|
||||
| 5 | DownloadFinished | Download finished 100%. |
|
||||
|
||||
The API response codes for download functions are:
|
||||
|
||||
| Status | Constant | Info |
|
||||
|------|----------------|-------------------------------|
|
||||
| 0 | DownloadResponseSuccess | Success |
|
||||
| 1 | DownloadResponseIDNotFound | Error: Download ID not found. |
|
||||
| 2 | DownloadResponseFileInvalid | Error: Target file cannot be used. For example, permissions denied to create it. |
|
||||
| 3 | DownloadResponseActionInvalid | Error: Invalid action. Pausing a non-active download, resuming a non-paused download, or canceling already canceled or finished. |
|
||||
| 4 | DownloadResponseFileWrite | Error writing file. |
|
||||
|
||||
### Start Download
|
||||
|
||||
This 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 blockchain parameter is the node ID of the peer who shared the file on its blockchain (i.e., the "owner" of 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]&blockchain=[node ID]
|
||||
Request: GET /download/start?path=[target path on disk]&hash=[file hash to download]&node=[node ID]
|
||||
Result: 200 with JSON structure apiResponseDownloadStatus
|
||||
```
|
||||
|
||||
```go
|
||||
type apiResponseDownloadStatus struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error download not found
|
||||
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 apiBlockRecordFile `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.
|
||||
}
|
||||
```
|
||||
|
||||
Example response (only apistatus, id, and downloadstatus are used):
|
||||
|
||||
```json
|
||||
{
|
||||
"apistatus": 0,
|
||||
"id": "a6107122-9e31-42d3-b663-0df64263c6bc",
|
||||
"downloadstatus": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Get Download Status
|
||||
|
||||
This returns the status of an active download. The hash and blockchain parameters must be the same as `/download/start`.
|
||||
This returns the status of an active download.
|
||||
|
||||
```
|
||||
Request: GET /download/status?hash=[file hash]&blockchain=[node ID]
|
||||
Request: GET /download/status?id=[download ID]
|
||||
Result: 200 with JSON structure apiResponseDownloadStatus
|
||||
```
|
||||
|
||||
Example request: `http://127.0.0.1:112/download/status?id=a6107122-9e31-42d3-b663-0df64263c6bc`
|
||||
|
||||
```json
|
||||
{
|
||||
"apistatus": 0,
|
||||
"id": "950316e8-23b4-49c7-83dd-c021e793129e",
|
||||
"downloadstatus": 5,
|
||||
"file": {
|
||||
"id": "78ac46dc-6731-4f3d-a9d4-22c9a4eb5fb9",
|
||||
"hash": "LiQUdqPD78+e6j1eS+0VmSUdCgUXVDN74ELVTRcgmWc=",
|
||||
"type": 0,
|
||||
"format": 13,
|
||||
"size": 10240,
|
||||
"folder": "",
|
||||
"name": "a96dc7b6a4a7a401c48f93c442f01de9.bin",
|
||||
"description": "",
|
||||
"date": "2021-10-04T04:37:17Z",
|
||||
"nodeid": "lMP3/nYMjoE/PfGKRDZi+ms5h7jWUrdIZaKSvLAAq6A=",
|
||||
"metadata": []
|
||||
},
|
||||
"progress": {
|
||||
"totalsize": 10240,
|
||||
"downloadedsize": 1024,
|
||||
"percentage": 10
|
||||
},
|
||||
"swarm": {
|
||||
"countpeers": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pause, Resume, and Cancel a Download
|
||||
|
||||
This 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)
|
||||
```
|
||||
|
||||
## Explore
|
||||
|
||||
### List Recently Shared Files
|
||||
|
||||
Reference in New Issue
Block a user