mirror of
https://github.com/PeernetOfficial/Abstraction.git
synced 2026-07-17 02:57:51 +01:00
added abstracted functions for download
This commit is contained in:
70
files.go
70
files.go
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/PeernetOfficial/core/protocol"
|
||||
"github.com/PeernetOfficial/core/warehouse"
|
||||
"github.com/google/uuid"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
@@ -230,3 +231,72 @@ func SearchResult(api *webapi.WebapiInstance, jobID uuid.UUID) (*webapi.SearchRe
|
||||
|
||||
return nil, errors.New("search not successful")
|
||||
}
|
||||
|
||||
// Download and abstracted function that starts downloading a file
|
||||
// and returns the ID which can be used to track the files
|
||||
// download status
|
||||
func Download(api *webapi.WebapiInstance, hashStr string, nodeIDStr string, path string) (*uuid.UUID, error) {
|
||||
// validate hashes, must be blake3
|
||||
hash, valid1 := webapi.DecodeBlake3Hash(hashStr)
|
||||
nodeID, valid2 := webapi.DecodeBlake3Hash(nodeIDStr)
|
||||
if !valid1 || !valid2 {
|
||||
//http.Error(w, "", http.StatusBadRequest)
|
||||
return nil, errors.New("hash or node ID was not valid")
|
||||
}
|
||||
|
||||
filePath := path
|
||||
if filePath == "" {
|
||||
// http.Error(w, "", http.StatusBadRequest)
|
||||
return nil, errors.New("file path not provided")
|
||||
}
|
||||
|
||||
ID := uuid.New()
|
||||
|
||||
info := &webapi.DownloadInfo{Backend: api.Backend, Api: api, ID: ID, Created: time.Now(), Hash: hash, NodeID: nodeID}
|
||||
|
||||
// create the file immediately
|
||||
err := info.InitDiskFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add the download to the list
|
||||
api.DownloadAdd(info)
|
||||
|
||||
// start the download!
|
||||
go info.Start()
|
||||
|
||||
return &ID, nil
|
||||
}
|
||||
|
||||
// DownloadStatus Abstracted function that finds the status of a downloaded files
|
||||
// based on the download ID provided and returns with the appropriate information
|
||||
func DownloadStatus(api *webapi.WebapiInstance, DownloadID *uuid.UUID) (*webapi.ApiResponseDownloadStatus, error) {
|
||||
|
||||
info := api.DownloadLookup(*DownloadID)
|
||||
if info == nil {
|
||||
//EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
return nil, errors.New("download ID not found")
|
||||
}
|
||||
|
||||
info.RLock()
|
||||
|
||||
response := webapi.ApiResponseDownloadStatus{APIStatus: webapi.DownloadResponseSuccess, ID: info.ID, DownloadStatus: info.Status}
|
||||
|
||||
if info.Status >= webapi.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 >= webapi.DownloadActive {
|
||||
response.Swarm.CountPeers = info.Swarm.CountPeers
|
||||
}
|
||||
|
||||
info.RUnlock()
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ type WebapiInstance struct {
|
||||
allJobsMutex sync.RWMutex
|
||||
|
||||
// download info
|
||||
downloads map[uuid.UUID]*downloadInfo
|
||||
downloads map[uuid.UUID]*DownloadInfo
|
||||
downloadsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ var WSUpgrader = websocket.Upgrader{
|
||||
}
|
||||
|
||||
// Start starts the API. ListenAddresses is a list of IP:Ports.
|
||||
// The certificate file and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout.
|
||||
// The certificate File and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout.
|
||||
// The API key may be uuid.Nil to disable it although this is not recommended for security reasons.
|
||||
func Start(Backend *core.Backend, ListenAddresses []string, UseSSL bool, CertificateFile, CertificateKey string, TimeoutRead, TimeoutWrite time.Duration, APIKey uuid.UUID) (api *WebapiInstance) {
|
||||
if len(ListenAddresses) == 0 {
|
||||
@@ -59,9 +59,9 @@ func Start(Backend *core.Backend, ListenAddresses []string, UseSSL bool, Certifi
|
||||
api = &WebapiInstance{
|
||||
Backend: Backend,
|
||||
Router: mux.NewRouter(),
|
||||
AllowKeyInParam: []string{"/file/read", "/file/view"},
|
||||
AllowKeyInParam: []string{"/File/read", "/File/view"},
|
||||
allJobs: make(map[uuid.UUID]*SearchJob),
|
||||
downloads: make(map[uuid.UUID]*downloadInfo),
|
||||
downloads: make(map[uuid.UUID]*DownloadInfo),
|
||||
}
|
||||
|
||||
if APIKey != uuid.Nil {
|
||||
@@ -69,17 +69,17 @@ func Start(Backend *core.Backend, ListenAddresses []string, UseSSL bool, Certifi
|
||||
}
|
||||
|
||||
api.Router.HandleFunc("/test", apiTest).Methods("GET")
|
||||
api.Router.HandleFunc("/status", api.apiStatus).Methods("GET")
|
||||
api.Router.HandleFunc("/status/peers", api.apiStatusPeers).Methods("GET")
|
||||
api.Router.HandleFunc("/Status", api.apiStatus).Methods("GET")
|
||||
api.Router.HandleFunc("/Status/peers", api.apiStatusPeers).Methods("GET")
|
||||
api.Router.HandleFunc("/account/info", api.apiAccountInfo).Methods("GET")
|
||||
api.Router.HandleFunc("/account/delete", api.apiAccountDelete).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/header", api.apiBlockchainHeaderFunc).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/append", api.apiBlockchainAppend).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/read", api.apiBlockchainRead).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/file/add", api.apiBlockchainFileAdd).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/file/list", api.apiBlockchainFileList).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/file/delete", api.apiBlockchainFileDelete).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/file/update", api.apiBlockchainFileUpdate).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/File/add", api.apiBlockchainFileAdd).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/File/list", api.apiBlockchainFileList).Methods("GET")
|
||||
api.Router.HandleFunc("/blockchain/File/delete", api.apiBlockchainFileDelete).Methods("POST")
|
||||
api.Router.HandleFunc("/blockchain/File/update", api.apiBlockchainFileUpdate).Methods("POST")
|
||||
api.Router.HandleFunc("/profile/list", api.apiProfileList).Methods("GET")
|
||||
api.Router.HandleFunc("/profile/read", api.apiProfileRead).Methods("GET")
|
||||
api.Router.HandleFunc("/profile/write", api.apiProfileWrite).Methods("POST")
|
||||
@@ -90,17 +90,17 @@ func Start(Backend *core.Backend, ListenAddresses []string, UseSSL bool, Certifi
|
||||
api.Router.HandleFunc("/search/statistic", api.apiSearchStatistic).Methods("GET")
|
||||
api.Router.HandleFunc("/search/terminate", api.apiSearchTerminate).Methods("GET")
|
||||
api.Router.HandleFunc("/explore", api.apiExplore).Methods("GET")
|
||||
api.Router.HandleFunc("/file/format", api.apiFileFormat).Methods("GET")
|
||||
api.Router.HandleFunc("/File/format", api.apiFileFormat).Methods("GET")
|
||||
api.Router.HandleFunc("/download/start", api.apiDownloadStart).Methods("GET")
|
||||
api.Router.HandleFunc("/download/status", api.apiDownloadStatus).Methods("GET")
|
||||
api.Router.HandleFunc("/download/Status", api.apiDownloadStatus).Methods("GET")
|
||||
api.Router.HandleFunc("/download/action", api.apiDownloadAction).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/create", api.apiWarehouseCreateFile).Methods("POST")
|
||||
api.Router.HandleFunc("/warehouse/create/path", api.apiWarehouseCreateFilePath).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/read", api.apiWarehouseReadFile).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/read/path", api.apiWarehouseReadFilePath).Methods("GET")
|
||||
api.Router.HandleFunc("/warehouse/delete", api.apiWarehouseDeleteFile).Methods("GET")
|
||||
api.Router.HandleFunc("/file/read", api.apiFileRead).Methods("GET")
|
||||
api.Router.HandleFunc("/file/view", api.apiFileView).Methods("GET")
|
||||
api.Router.HandleFunc("/File/read", api.apiFileRead).Methods("GET")
|
||||
api.Router.HandleFunc("/File/view", api.apiFileView).Methods("GET")
|
||||
|
||||
for _, listen := range ListenAddresses {
|
||||
go startWebAPI(Backend, listen, UseSSL, CertificateFile, CertificateKey, api.Router, "API", TimeoutRead, TimeoutWrite)
|
||||
@@ -109,8 +109,8 @@ func Start(Backend *core.Backend, ListenAddresses []string, UseSSL bool, Certifi
|
||||
return api
|
||||
}
|
||||
|
||||
// startWebAPI starts a web-server with given parameters and logs the status. If may block forever and only returns if there is an error.
|
||||
// The certificate file and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout.
|
||||
// startWebAPI starts a web-server with given parameters and logs the Status. If may block forever and only returns if there is an error.
|
||||
// The certificate File and key are only used if SSL is enabled. The read and write timeout may be 0 for no timeout.
|
||||
func startWebAPI(Backend *core.Backend, WebListen string, UseSSL bool, CertificateFile, CertificateKey string, Handler http.Handler, Info string, ReadTimeout, WriteTimeout time.Duration) {
|
||||
Backend.LogError("startWebAPI", "Start API at '%s'\n", WebListen)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ type apiBlockchainBlockRaw struct {
|
||||
}
|
||||
|
||||
type apiBlockchainBlockStatus struct {
|
||||
Status int `json:"status"` // See blockchain.StatusX.
|
||||
Status int `json:"Status"` // See blockchain.StatusX.
|
||||
Height uint64 `json:"height"` // Height of the blockchain (number of blocks).
|
||||
Version uint64 `json:"version"` // Version of the blockchain.
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (api *WebapiInstance) apiBlockchainAppend(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
type apiBlockchainBlock struct {
|
||||
Status int `json:"status"` // See blockchain.StatusX.
|
||||
Status int `json:"Status"` // See blockchain.StatusX.
|
||||
PeerID string `json:"peerid"` // Peer ID hex encoded.
|
||||
LastBlockHash []byte `json:"lastblockhash"` // Hash of the last block. Blake3.
|
||||
BlockchainVersion uint64 `json:"blockchainversion"` // Blockchain version
|
||||
|
||||
@@ -1,202 +1,202 @@
|
||||
/*
|
||||
File Name: Download Transfer.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 (
|
||||
"bytes"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core/warehouse"
|
||||
)
|
||||
|
||||
// Starts the download.
|
||||
func (info *downloadInfo) Start() {
|
||||
// current user?
|
||||
if bytes.Equal(info.nodeID, info.backend.SelfNodeID()) {
|
||||
info.DownloadSelf()
|
||||
return
|
||||
}
|
||||
|
||||
for n := 0; n < 3 && info.peer == nil; n++ {
|
||||
_, info.peer, _ = info.backend.FindNode(info.nodeID, time.Second*5)
|
||||
|
||||
if info.status == DownloadCanceled {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if info.peer != nil {
|
||||
info.Download()
|
||||
} else {
|
||||
info.status = DownloadCanceled
|
||||
}
|
||||
}
|
||||
|
||||
func (info *downloadInfo) Download() {
|
||||
//fmt.Printf("Download start of %s\n", hex.EncodeToString(info.hash))
|
||||
|
||||
// try to download the entire file
|
||||
reader, fileSize, transferSize, err := FileStartReader(info.peer, info.hash, 0, 0, nil)
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
info.status = DownloadCanceled
|
||||
return
|
||||
} else if fileSize != transferSize {
|
||||
info.status = DownloadCanceled
|
||||
return
|
||||
}
|
||||
|
||||
info.file.Size = fileSize
|
||||
info.status = DownloadActive
|
||||
|
||||
// download in a loop
|
||||
var fileOffset, totalRead uint64
|
||||
dataRemaining := fileSize
|
||||
readSize := uint64(4096)
|
||||
|
||||
for dataRemaining > 0 {
|
||||
//fmt.Printf("data remaining: downloaded %d from total %d = %d %%\n", totalRead, fileSize, totalRead*100/fileSize)
|
||||
if dataRemaining < readSize {
|
||||
readSize = dataRemaining
|
||||
}
|
||||
|
||||
data := make([]byte, readSize)
|
||||
n, err := reader.Read(data)
|
||||
|
||||
totalRead += uint64(n)
|
||||
dataRemaining -= uint64(n)
|
||||
data = data[:n]
|
||||
|
||||
if err != nil {
|
||||
info.status = DownloadCanceled
|
||||
return
|
||||
}
|
||||
|
||||
info.storeDownloadData(data[:n], fileOffset)
|
||||
|
||||
fileOffset += uint64(n)
|
||||
}
|
||||
|
||||
//fmt.Printf("data finished: downloaded %d from total %d = %d %%\n", totalRead, fileSize, totalRead*100/fileSize)
|
||||
|
||||
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) // 666 : All uses can read/write
|
||||
|
||||
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.WriteAt(data, int64(offset)); err != nil {
|
||||
return DownloadResponseFileWrite
|
||||
}
|
||||
|
||||
info.DiskFile.StoredSize += uint64(len(data))
|
||||
|
||||
return DownloadResponseSuccess
|
||||
}
|
||||
|
||||
func (info *downloadInfo) DownloadSelf() {
|
||||
// Check if the file is available in the local warehouse.
|
||||
_, fileSize, status, _ := info.backend.UserWarehouse.FileExists(info.hash)
|
||||
if status != warehouse.StatusOK {
|
||||
info.status = DownloadCanceled
|
||||
return
|
||||
}
|
||||
|
||||
info.file.Size = fileSize
|
||||
info.status = DownloadActive
|
||||
|
||||
// read the file
|
||||
status, bytesRead, _ := info.backend.UserWarehouse.ReadFile(info.hash, 0, int64(info.file.Size), info.DiskFile.Handle)
|
||||
|
||||
info.DiskFile.StoredSize = uint64(bytesRead)
|
||||
|
||||
if status != warehouse.StatusOK {
|
||||
info.status = DownloadCanceled
|
||||
return
|
||||
}
|
||||
|
||||
info.Finish()
|
||||
info.DeleteDefer(time.Hour * 1) // cache the details for 1 hour before removing}
|
||||
}
|
||||
/*
|
||||
File Name: Download Transfer.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 (
|
||||
"bytes"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/PeernetOfficial/core/warehouse"
|
||||
)
|
||||
|
||||
// Starts the download.
|
||||
func (info *DownloadInfo) Start() {
|
||||
// current user?
|
||||
if bytes.Equal(info.NodeID, info.Backend.SelfNodeID()) {
|
||||
info.DownloadSelf()
|
||||
return
|
||||
}
|
||||
|
||||
for n := 0; n < 3 && info.Peer == nil; n++ {
|
||||
_, info.Peer, _ = info.Backend.FindNode(info.NodeID, time.Second*5)
|
||||
|
||||
if info.Status == DownloadCanceled {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if info.Peer != nil {
|
||||
info.Download()
|
||||
} else {
|
||||
info.Status = DownloadCanceled
|
||||
}
|
||||
}
|
||||
|
||||
func (info *DownloadInfo) Download() {
|
||||
//fmt.Printf("Download start of %s\n", hex.EncodeToString(info.Hash))
|
||||
|
||||
// try to download the entire File
|
||||
reader, fileSize, transferSize, err := FileStartReader(info.Peer, info.Hash, 0, 0, nil)
|
||||
if reader != nil {
|
||||
defer reader.Close()
|
||||
}
|
||||
if err != nil {
|
||||
info.Status = DownloadCanceled
|
||||
return
|
||||
} else if fileSize != transferSize {
|
||||
info.Status = DownloadCanceled
|
||||
return
|
||||
}
|
||||
|
||||
info.File.Size = fileSize
|
||||
info.Status = DownloadActive
|
||||
|
||||
// download in a loop
|
||||
var fileOffset, totalRead uint64
|
||||
dataRemaining := fileSize
|
||||
readSize := uint64(4096)
|
||||
|
||||
for dataRemaining > 0 {
|
||||
//fmt.Printf("data remaining: downloaded %d from total %d = %d %%\n", totalRead, fileSize, totalRead*100/fileSize)
|
||||
if dataRemaining < readSize {
|
||||
readSize = dataRemaining
|
||||
}
|
||||
|
||||
data := make([]byte, readSize)
|
||||
n, err := reader.Read(data)
|
||||
|
||||
totalRead += uint64(n)
|
||||
dataRemaining -= uint64(n)
|
||||
data = data[:n]
|
||||
|
||||
if err != nil {
|
||||
info.Status = DownloadCanceled
|
||||
return
|
||||
}
|
||||
|
||||
info.storeDownloadData(data[:n], fileOffset)
|
||||
|
||||
fileOffset += uint64(n)
|
||||
}
|
||||
|
||||
//fmt.Printf("data finished: downloaded %d from total %d = %d %%\n", totalRead, fileSize, totalRead*100/fileSize)
|
||||
|
||||
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) // 666 : All uses can read/write
|
||||
|
||||
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.WriteAt(data, int64(offset)); err != nil {
|
||||
return DownloadResponseFileWrite
|
||||
}
|
||||
|
||||
info.DiskFile.StoredSize += uint64(len(data))
|
||||
|
||||
return DownloadResponseSuccess
|
||||
}
|
||||
|
||||
func (info *DownloadInfo) DownloadSelf() {
|
||||
// Check if the File is available in the local warehouse.
|
||||
_, fileSize, status, _ := info.Backend.UserWarehouse.FileExists(info.Hash)
|
||||
if status != warehouse.StatusOK {
|
||||
info.Status = DownloadCanceled
|
||||
return
|
||||
}
|
||||
|
||||
info.File.Size = fileSize
|
||||
info.Status = DownloadActive
|
||||
|
||||
// read the File
|
||||
status, bytesRead, _ := info.Backend.UserWarehouse.ReadFile(info.Hash, 0, int64(info.File.Size), info.DiskFile.Handle)
|
||||
|
||||
info.DiskFile.StoredSize = uint64(bytesRead)
|
||||
|
||||
if status != warehouse.StatusOK {
|
||||
info.Status = DownloadCanceled
|
||||
return
|
||||
}
|
||||
|
||||
info.Finish()
|
||||
info.DeleteDefer(time.Hour * 1) // cache the details for 1 hour before removing}
|
||||
}
|
||||
|
||||
@@ -19,51 +19,51 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type apiResponseDownloadStatus struct {
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
} `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.
|
||||
} `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.
|
||||
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.
|
||||
DownloadResponseFileWrite = 5 // Error writing File.
|
||||
)
|
||||
|
||||
// Download status list
|
||||
// Download Status list
|
||||
const (
|
||||
DownloadWaitMetadata = 0 // Wait for file metadata.
|
||||
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.
|
||||
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).
|
||||
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
|
||||
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"))
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("Hash"))
|
||||
nodeID, valid2 := DecodeBlake3Hash(r.Form.Get("node"))
|
||||
if !valid1 || !valid2 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
@@ -76,28 +76,28 @@ func (api *WebapiInstance) apiDownloadStart(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
info := &downloadInfo{backend: api.Backend, api: api, id: uuid.New(), created: time.Now(), hash: hash, nodeID: nodeID}
|
||||
info := &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})
|
||||
// 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)
|
||||
api.DownloadAdd(info)
|
||||
|
||||
// start the download!
|
||||
go info.Start()
|
||||
|
||||
EncodeJSON(api.Backend, w, r, 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.
|
||||
apiDownloadStatus returns the Status of an active download.
|
||||
|
||||
Request: GET /download/status?ID=[download ID]
|
||||
Result: 200 with JSON structure apiResponseDownloadStatus
|
||||
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()
|
||||
@@ -107,26 +107,26 @@ func (api *WebapiInstance) apiDownloadStatus(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
info := api.downloadLookup(id)
|
||||
info := api.DownloadLookup(id)
|
||||
if info == nil {
|
||||
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
EncodeJSON(api.Backend, w, r, ApiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
return
|
||||
}
|
||||
|
||||
info.RLock()
|
||||
|
||||
response := apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: info.status}
|
||||
response := ApiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.ID, DownloadStatus: info.Status}
|
||||
|
||||
if info.status >= DownloadWaitSwarm {
|
||||
response.File = info.file
|
||||
if info.Status >= DownloadWaitSwarm {
|
||||
response.File = info.File
|
||||
|
||||
response.Progress.TotalSize = info.file.Size
|
||||
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
|
||||
response.Progress.Percentage = math.Round(float64(info.DiskFile.StoredSize)/float64(info.File.Size)*100*100) / 100
|
||||
}
|
||||
|
||||
if info.status >= DownloadActive {
|
||||
if info.Status >= DownloadActive {
|
||||
response.Swarm.CountPeers = info.Swarm.CountPeers
|
||||
}
|
||||
|
||||
@@ -136,12 +136,12 @@ func (api *WebapiInstance) apiDownloadStatus(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
/*
|
||||
apiDownloadAction pauses, resumes, and cancels a download. Once canceled, a new download has to be started if the file shall be downloaded.
|
||||
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)
|
||||
Result: 200 with JSON structure ApiResponseDownloadStatus (using APIStatus and DownloadStatus)
|
||||
*/
|
||||
func (api *WebapiInstance) apiDownloadAction(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
@@ -152,9 +152,9 @@ func (api *WebapiInstance) apiDownloadAction(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
info := api.downloadLookup(id)
|
||||
info := api.DownloadLookup(id)
|
||||
if info == nil {
|
||||
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
EncodeJSON(api.Backend, w, r, ApiResponseDownloadStatus{APIStatus: DownloadResponseIDNotFound})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -171,56 +171,56 @@ func (api *WebapiInstance) apiDownloadAction(w http.ResponseWriter, r *http.Requ
|
||||
apiStatus = info.Cancel()
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, apiResponseDownloadStatus{APIStatus: apiStatus, ID: info.id, DownloadStatus: info.status})
|
||||
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
|
||||
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
|
||||
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).
|
||||
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)
|
||||
File ApiFile // File metadata (only Status >= DownloadWaitSwarm)
|
||||
|
||||
DiskFile struct { // Target file on disk to store downloaded data
|
||||
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
|
||||
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.
|
||||
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
|
||||
Peer *core.PeerInfo
|
||||
|
||||
api *WebapiInstance
|
||||
backend *core.Backend
|
||||
Api *WebapiInstance
|
||||
Backend *core.Backend
|
||||
}
|
||||
|
||||
func (api *WebapiInstance) downloadAdd(info *downloadInfo) {
|
||||
func (api *WebapiInstance) DownloadAdd(info *DownloadInfo) {
|
||||
api.downloadsMutex.Lock()
|
||||
api.downloads[info.id] = info
|
||||
api.downloads[info.ID] = info
|
||||
api.downloadsMutex.Unlock()
|
||||
}
|
||||
|
||||
func (api *WebapiInstance) downloadDelete(id uuid.UUID) {
|
||||
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) {
|
||||
func (api *WebapiInstance) DownloadLookup(id uuid.UUID) (info *DownloadInfo) {
|
||||
api.downloadsMutex.Lock()
|
||||
info = api.downloads[id]
|
||||
api.downloadsMutex.Unlock()
|
||||
@@ -229,14 +229,14 @@ func (api *WebapiInstance) downloadLookup(id uuid.UUID) (info *downloadInfo) {
|
||||
|
||||
// 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) {
|
||||
func (info *DownloadInfo) DeleteDefer(Duration time.Duration) {
|
||||
go func() {
|
||||
<-time.After(Duration)
|
||||
info.api.downloadDelete(info.id)
|
||||
info.Api.DownloadDelete(info.ID)
|
||||
}()
|
||||
}
|
||||
|
||||
// DecodeBlake3Hash decodes a blake3 hash that is hex encoded
|
||||
// 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
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/PeernetOfficial/core"
|
||||
)
|
||||
|
||||
// PathToExtension translates a path to a file extension, if possible. It also returns the second file extension if there is one (relevant for files like "test.tar.gz").
|
||||
// PathToExtension translates a path to a File extension, if possible. It also returns the second File extension if there is one (relevant for files like "test.tar.gz").
|
||||
func PathToExtension(Path string) (extension, extension2 string, valid bool) {
|
||||
_, fileA := path.Split(Path)
|
||||
|
||||
@@ -143,7 +143,7 @@ func HTTPContentTypeToCore(httpContentType string) (fileType, fileFormat uint16)
|
||||
}
|
||||
}
|
||||
|
||||
// FileDataToHTTPContentType returns the HTTP content type based on the initial file data. It reads the first 512 bytes of the file.
|
||||
// FileDataToHTTPContentType returns the HTTP content type based on the initial File data. It reads the first 512 bytes of the File.
|
||||
func FileDataToHTTPContentType(Path string) (httpContentType string, err error) {
|
||||
file, err := os.Open(Path)
|
||||
if err != nil {
|
||||
@@ -172,10 +172,10 @@ func FileDataToHTTPContentType(Path string) (httpContentType string, err error)
|
||||
return httpContentType, nil
|
||||
}
|
||||
|
||||
// FileDetectType detects the File Type and File Format of a file. It uses the extension if available, otherwise the file data, for detection.
|
||||
// FileDetectType detects the File Type and File Format of a File. It uses the extension if available, otherwise the File data, for detection.
|
||||
func FileDetectType(Path string) (fileType, fileFormat uint16, err error) {
|
||||
// If a file extension is available, use that to detect the file type and file format.
|
||||
// Otherwise, use the initial file data for detection.
|
||||
// If a File extension is available, use that to detect the File type and File format.
|
||||
// Otherwise, use the initial File data for detection.
|
||||
if ext1, _, valid := PathToExtension(Path); valid {
|
||||
fileType, fileFormat = FileTranslateExtension(ext1)
|
||||
return fileType, fileFormat, nil
|
||||
@@ -191,16 +191,16 @@ func FileDetectType(Path string) (fileType, fileFormat uint16, err error) {
|
||||
}
|
||||
|
||||
type apiResponseFileFormat struct {
|
||||
Status int `json:"status"` // Status: 0 = Success, 1 = Error reading file
|
||||
Status int `json:"Status"` // Status: 0 = Success, 1 = Error reading File
|
||||
FileType uint16 `json:"filetype"` // File Type.
|
||||
FileFormat uint16 `json:"fileformat"` // File Format.
|
||||
}
|
||||
|
||||
/*
|
||||
apiFileFormat detects the file type and file format of the specified file.
|
||||
It will primarily use the file extension for detection. If unavailable, it uses the first 512 bytes of the file data to detect the type.
|
||||
apiFileFormat detects the File type and File format of the specified File.
|
||||
It will primarily use the File extension for detection. If unavailable, it uses the first 512 bytes of the File data to detect the type.
|
||||
|
||||
Request: GET /file/format?path=[file path on disk]
|
||||
Request: GET /File/format?path=[File path on disk]
|
||||
Result: 200 with JSON structure apiResponseFileFormat
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileFormat(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -20,26 +20,26 @@ import (
|
||||
)
|
||||
|
||||
/*
|
||||
apiFileRead reads a file immediately from a remote peer. Use the /download functions to download a file.
|
||||
apiFileRead reads a File immediately from a remote Peer. Use the /download functions to download a File.
|
||||
This endpoint supports the Range, Content-Range and Content-Length headers. Multipart ranges are not supported and result in HTTP 400.
|
||||
Instead of providing the node ID, the peer ID is also accepted in the &node= parameter.
|
||||
The default timeout for connecting to the peer is 10 seconds.
|
||||
Instead of providing the node ID, the Peer ID is also accepted in the &node= parameter.
|
||||
The default timeout for connecting to the Peer is 10 seconds.
|
||||
|
||||
Request: GET /file/read?hash=[hash]&node=[node ID]
|
||||
Request: GET /File/read?Hash=[Hash]&node=[node ID]
|
||||
Optional: &offset=[offset]&limit=[limit] or via Range header.
|
||||
Optional: &timeout=[seconds]
|
||||
Response: 200 with the content
|
||||
206 with partial content
|
||||
400 if the parameters are invalid
|
||||
404 if the file was not found or other error on transfer initiate
|
||||
502 if unable to find or connect to the remote peer in time
|
||||
404 if the File was not found or other error on transfer initiate
|
||||
502 if unable to find or connect to the remote Peer in time
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileRead(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var err error
|
||||
|
||||
// validate hashes (must be blake3) and other input
|
||||
fileHash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
fileHash, valid1 := DecodeBlake3Hash(r.Form.Get("Hash"))
|
||||
nodeID, valid2 := DecodeBlake3Hash(r.Form.Get("node"))
|
||||
publicKey, err3 := core.PublicKeyFromPeerID(r.Form.Get("node"))
|
||||
if !valid1 || (!valid2 && err3 != nil) {
|
||||
@@ -68,12 +68,12 @@ func (api *WebapiInstance) apiFileRead(w http.ResponseWriter, r *http.Request) {
|
||||
offset = ranges[0].start
|
||||
}
|
||||
|
||||
// Is the file available in the local warehouse? In that case requesting it from the remote is unnecessary.
|
||||
// Is the File available in the local warehouse? In that case requesting it from the remote is unnecessary.
|
||||
if serveFileFromWarehouse(api.Backend, w, fileHash, uint64(offset), uint64(limit), ranges) {
|
||||
return
|
||||
}
|
||||
|
||||
// try connecting via node ID or peer ID?
|
||||
// try connecting via node ID or Peer ID?
|
||||
var peer *core.PeerInfo
|
||||
|
||||
if valid2 {
|
||||
@@ -103,10 +103,10 @@ func (api *WebapiInstance) apiFileRead(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
}
|
||||
|
||||
// serveFileFromWarehouse serves the file from the warehouse. If it is not available, it returns false and does not use the writer.
|
||||
// Limit is optional, 0 means the entire file.
|
||||
// serveFileFromWarehouse serves the File from the warehouse. If it is not available, it returns false and does not use the writer.
|
||||
// Limit is optional, 0 means the entire File.
|
||||
func serveFileFromWarehouse(backend *core.Backend, w http.ResponseWriter, fileHash []byte, offset, limit uint64, ranges []HTTPRange) (valid bool) {
|
||||
// Check if the file is available in the local warehouse.
|
||||
// Check if the File is available in the local warehouse.
|
||||
_, fileSize, status, _ := backend.UserWarehouse.FileExists(fileHash)
|
||||
if status != warehouse.StatusOK {
|
||||
return false
|
||||
@@ -127,32 +127,32 @@ func serveFileFromWarehouse(backend *core.Backend, w http.ResponseWriter, fileHa
|
||||
|
||||
status, _, _ = backend.UserWarehouse.ReadFile(fileHash, int64(offset), int64(limit), w)
|
||||
|
||||
// StatusErrorReadFile must be considered success, since parts of the file may have been transferred already and recovery is not possible.
|
||||
// StatusErrorReadFile must be considered success, since parts of the File may have been transferred already and recovery is not possible.
|
||||
return status == warehouse.StatusErrorReadFile || status == warehouse.StatusOK
|
||||
}
|
||||
|
||||
/*
|
||||
apiFileView is similar to /file/read but but provides a format parameter. It sets the Content-Type and Accept-Ranges headers.
|
||||
apiFileView is similar to /File/read but but provides a format parameter. It sets the Content-Type and Accept-Ranges headers.
|
||||
This endpoint supports the Range, Content-Range and Content-Length headers. Multipart ranges are not supported and result in HTTP 400.
|
||||
Instead of providing the node ID, the peer ID is also accepted in the &node= parameter.
|
||||
The default timeout for connecting to the peer is 10 seconds.
|
||||
Instead of providing the node ID, the Peer ID is also accepted in the &node= parameter.
|
||||
The default timeout for connecting to the Peer is 10 seconds.
|
||||
Formats: 14 = Video
|
||||
|
||||
Request: GET /file/view?hash=[hash]&node=[node ID]&format=[format]
|
||||
Request: GET /File/view?Hash=[Hash]&node=[node ID]&format=[format]
|
||||
Optional: &offset=[offset]&limit=[limit] or via Range header.
|
||||
Optional: &timeout=[seconds]
|
||||
Response: 200 with the content
|
||||
206 with partial content
|
||||
400 if the parameters are invalid
|
||||
404 if the file was not found or other error on transfer initiate
|
||||
502 if unable to find or connect to the remote peer in time
|
||||
404 if the File was not found or other error on transfer initiate
|
||||
502 if unable to find or connect to the remote Peer in time
|
||||
*/
|
||||
func (api *WebapiInstance) apiFileView(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var err error
|
||||
|
||||
// validate hashes (must be blake3) and other input
|
||||
fileHash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
fileHash, valid1 := DecodeBlake3Hash(r.Form.Get("Hash"))
|
||||
nodeID, valid2 := DecodeBlake3Hash(r.Form.Get("node"))
|
||||
publicKey, err3 := core.PublicKeyFromPeerID(r.Form.Get("node"))
|
||||
if !valid1 || (!valid2 && err3 != nil) {
|
||||
@@ -191,14 +191,14 @@ func (api *WebapiInstance) apiFileView(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
}
|
||||
|
||||
// Is the file available in the local warehouse? In that case requesting it from the remote is unnecessary.
|
||||
// Is the File available in the local warehouse? In that case requesting it from the remote is unnecessary.
|
||||
if !localCacheDisable {
|
||||
if serveFileFromWarehouse(api.Backend, w, fileHash, uint64(offset), uint64(limit), ranges) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// try connecting via node ID or peer ID?
|
||||
// try connecting via node ID or Peer ID?
|
||||
var peer *core.PeerInfo
|
||||
|
||||
if valid2 {
|
||||
@@ -228,13 +228,13 @@ func (api *WebapiInstance) apiFileView(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, io.LimitReader(reader, int64(transferSize)))
|
||||
}
|
||||
|
||||
// PeerConnectPublicKey attempts to connect to the peer specified by its public key (= peer ID).
|
||||
// PeerConnectPublicKey attempts to connect to the Peer specified by its public key (= Peer ID).
|
||||
func PeerConnectPublicKey(backend *core.Backend, publicKey *btcec.PublicKey, timeout time.Duration) (peer *core.PeerInfo, err error) {
|
||||
if publicKey == nil {
|
||||
return nil, errors.New("invalid public key")
|
||||
}
|
||||
|
||||
// First look up in the peer list.
|
||||
// First look up in the Peer list.
|
||||
if peer = backend.PeerlistLookup(publicKey); peer != nil {
|
||||
return peer, nil
|
||||
}
|
||||
@@ -246,7 +246,7 @@ func PeerConnectPublicKey(backend *core.Backend, publicKey *btcec.PublicKey, tim
|
||||
}
|
||||
|
||||
// otherwise not found :(
|
||||
return nil, errors.New("peer not found")
|
||||
return nil, errors.New("Peer not found")
|
||||
}
|
||||
|
||||
// PeerConnectNode tries to connect via the node ID
|
||||
@@ -261,18 +261,18 @@ func PeerConnectNode(backend *core.Backend, nodeID []byte, timeout time.Duration
|
||||
}
|
||||
|
||||
// otherwise not found :(
|
||||
return nil, errors.New("peer not found")
|
||||
return nil, errors.New("Peer not found")
|
||||
}
|
||||
|
||||
// FileStartReader providers a reader to a remote file. The reader must be closed by the caller.
|
||||
// File Size is the full file size reported by the remote peer, regardless of the requested offset and limit. Limit is optional (0 means the entire file).
|
||||
// FileStartReader providers a reader to a remote File. The reader must be closed by the caller.
|
||||
// File Size is the full File size reported by the remote Peer, regardless of the requested offset and limit. Limit is optional (0 means the entire File).
|
||||
// Transfer Size is the size in bytes that is actually going to be transferred. The reader should be closed after reading that amount.
|
||||
// The optional cancelChan can be used to stop the file transfer at any point.
|
||||
// The optional cancelChan can be used to stop the File transfer at any point.
|
||||
func FileStartReader(peer *core.PeerInfo, hash []byte, offset, limit uint64, cancelChan <-chan struct{}) (reader io.ReadCloser, fileSize, transferSize uint64, err error) {
|
||||
if peer == nil {
|
||||
return nil, 0, 0, errors.New("peer not provided")
|
||||
return nil, 0, 0, errors.New("Peer not provided")
|
||||
} else if !peer.IsConnectionActive() {
|
||||
return nil, 0, 0, errors.New("no valid connection to peer")
|
||||
return nil, 0, 0, errors.New("no valid connection to Peer")
|
||||
}
|
||||
|
||||
udtConn, virtualConn, err := peer.FileTransferRequestUDT(hash, offset, limit)
|
||||
@@ -298,10 +298,10 @@ func FileStartReader(peer *core.PeerInfo, hash []byte, offset, limit uint64, can
|
||||
return udtConn, fileSize, transferSize, nil
|
||||
}
|
||||
|
||||
// FileReadAll downloads the file from the peer.
|
||||
// This function should only be used for testing or as a basis to fork. The caller should develop a custom download function that handles timeouts and excessive file sizes.
|
||||
// It allocates whatever size is reported by the remote peer. This could lead to an out of memory crash.
|
||||
// This function is blocking and may take a long time depending on the remote peer and the network connection.
|
||||
// FileReadAll downloads the File from the Peer.
|
||||
// This function should only be used for testing or as a basis to fork. The caller should develop a custom download function that handles timeouts and excessive File sizes.
|
||||
// It allocates whatever size is reported by the remote Peer. This could lead to an out of memory crash.
|
||||
// This function is blocking and may take a long time depending on the remote Peer and the network connection.
|
||||
func FileReadAll(peer *core.PeerInfo, hash []byte) (data []byte, err error) {
|
||||
reader, _, transferSize, err := FileStartReader(peer, hash, 0, 0, nil)
|
||||
if err != nil {
|
||||
@@ -313,7 +313,7 @@ func FileReadAll(peer *core.PeerInfo, hash []byte) (data []byte, err error) {
|
||||
data = make([]byte, transferSize) // Warning: This could lead to an out of memory crash.
|
||||
_, err = reader.Read(data)
|
||||
|
||||
// Note: This function does not verify if the returned data matches the hash and expected size.
|
||||
// Note: This function does not verify if the returned data matches the Hash and expected size.
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -29,18 +29,18 @@ type ApiFileMetadata struct {
|
||||
Number uint64 `json:"number"` // Number
|
||||
}
|
||||
|
||||
// ApiFile is the metadata of a file published on the blockchain
|
||||
// ApiFile is the metadata of a File published on the blockchain
|
||||
type ApiFile struct {
|
||||
ID uuid.UUID `json:"ID"` // Unique ID.
|
||||
Hash []byte `json:"hash"` // Blake3 hash of the file data
|
||||
Hash []byte `json:"Hash"` // Blake3 Hash of the File data
|
||||
Type uint8 `json:"type"` // File Type. For example audio or document. See TypeX.
|
||||
Format uint16 `json:"format"` // File Format. This is more granular, for example PDF or Word file. See FormatX.
|
||||
Size uint64 `json:"size"` // Size of the file
|
||||
Format uint16 `json:"format"` // File Format. This is more granular, for example PDF or Word File. See FormatX.
|
||||
Size uint64 `json:"size"` // Size of the File
|
||||
Folder string `json:"folder"` // Folder, optional
|
||||
Name string `json:"name"` // Name of the file
|
||||
Name string `json:"name"` // Name of the File
|
||||
Description string `json:"description"` // Description. This is expected to be multiline and contain hashtags!
|
||||
Date time.Time `json:"date"` // Date shared
|
||||
NodeID []byte `json:"nodeid"` // Node ID, owner of the file. Read only.
|
||||
NodeID []byte `json:"nodeid"` // Node ID, owner of the File. Read only.
|
||||
Metadata []ApiFileMetadata `json:"metadata"` // Additional metadata.
|
||||
}
|
||||
|
||||
@@ -118,17 +118,17 @@ func BlockRecordFileFromAPI(input ApiFile) (output blockchain.BlockRecordFile) {
|
||||
// ApiBlockAddFiles contains a list of files from the blockchain
|
||||
type ApiBlockAddFiles struct {
|
||||
Files []ApiFile `json:"files"` // List of files
|
||||
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API.
|
||||
Status int `json:"Status"` // Status of the operation, only used when this structure is returned from the API.
|
||||
}
|
||||
|
||||
/*
|
||||
apiBlockchainFileAdd adds a file with the provided information to the blockchain.
|
||||
Each file must be already stored in the Warehouse (virtual folders are exempt).
|
||||
If any file is not stored in the Warehouse, the function aborts with the status code StatusNotInWarehouse.
|
||||
If the block record encoding fails for any file, this function aborts with the status code StatusCorruptBlockRecord.
|
||||
apiBlockchainFileAdd adds a File with the provided information to the blockchain.
|
||||
Each File must be already stored in the Warehouse (virtual folders are exempt).
|
||||
If any File is not stored in the Warehouse, the function aborts with the Status code StatusNotInWarehouse.
|
||||
If the block record encoding fails for any File, this function aborts with the Status code StatusCorruptBlockRecord.
|
||||
In case the function aborts, the blockchain remains unchanged.
|
||||
|
||||
Request: POST /blockchain/file/add with JSON structure ApiBlockAddFiles
|
||||
Request: POST /blockchain/File/add with JSON structure ApiBlockAddFiles
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
400 if invalid input
|
||||
*/
|
||||
@@ -149,7 +149,7 @@ func (api *WebapiInstance) apiBlockchainFileAdd(w http.ResponseWriter, r *http.R
|
||||
file.ID = uuid.New()
|
||||
}
|
||||
|
||||
// Verify that the file exists in the warehouse. Folders are exempt from this check as they are only virtual.
|
||||
// Verify that the File exists in the warehouse. Folders are exempt from this check as they are only virtual.
|
||||
if !file.IsVirtualFolder() {
|
||||
if _, err := warehouse.ValidateHash(file.Hash); err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
@@ -184,7 +184,7 @@ func (api *WebapiInstance) apiBlockchainFileAdd(w http.ResponseWriter, r *http.R
|
||||
/*
|
||||
apiBlockchainFileList lists all files stored on the blockchain.
|
||||
|
||||
Request: GET /blockchain/file/list
|
||||
Request: GET /blockchain/File/list
|
||||
Response: 200 with JSON structure ApiBlockAddFiles
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainFileList(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -203,9 +203,9 @@ func (api *WebapiInstance) apiBlockchainFileList(w http.ResponseWriter, r *http.
|
||||
|
||||
/*
|
||||
apiBlockchainFileDelete deletes files with the provided IDs. Other fields are ignored.
|
||||
It will automatically delete the file in the Warehouse if there are no other references.
|
||||
It will automatically delete the File in the Warehouse if there are no other references.
|
||||
|
||||
Request: POST /blockchain/file/delete with JSON structure ApiBlockAddFiles
|
||||
Request: POST /blockchain/File/delete with JSON structure ApiBlockAddFiles
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
*/
|
||||
func (api *WebapiInstance) apiBlockchainFileDelete(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -237,7 +237,7 @@ func (api *WebapiInstance) apiBlockchainFileDelete(w http.ResponseWriter, r *htt
|
||||
/*
|
||||
apiBlockchainSelfUpdateFile updates files that are already published on the blockchain.
|
||||
|
||||
Request: POST /blockchain/file/update with JSON structure ApiBlockAddFiles
|
||||
Request: POST /blockchain/File/update with JSON structure ApiBlockAddFiles
|
||||
Response: 200 with JSON structure apiBlockchainBlockStatus
|
||||
400 if invalid input
|
||||
*/
|
||||
@@ -258,7 +258,7 @@ func (api *WebapiInstance) apiBlockchainFileUpdate(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Verify that the file exists in the warehouse. Folders are exempt from this check as they are only virtual.
|
||||
// Verify that the File exists in the warehouse. Folders are exempt from this check as they are only virtual.
|
||||
if !file.IsVirtualFolder() {
|
||||
if _, err := warehouse.ValidateHash(file.Hash); err != nil {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
@@ -312,7 +312,7 @@ func (info *ApiFileMetadata) GetNumber() uint64 {
|
||||
return info.Number
|
||||
}
|
||||
|
||||
// IsVirtualFolder returns true if the file is a virtual folder
|
||||
// IsVirtualFolder returns true if the File is a virtual folder
|
||||
func (file *ApiFile) IsVirtualFolder() bool {
|
||||
return file.Type == core.TypeFolder && file.Format == core.FormatFolder
|
||||
}
|
||||
@@ -324,7 +324,7 @@ func SetFileMerkleInfo(backend *core.Backend, file *blockchain.BlockRecordFile)
|
||||
file.MerkleRootHash = file.Hash
|
||||
file.FragmentSize = merkle.MinimumFragmentSize
|
||||
} else {
|
||||
// Get the information from the Warehouse .merkle companion file.
|
||||
// Get the information from the Warehouse .merkle companion File.
|
||||
tree, status, _ := backend.UserWarehouse.ReadMerkleTree(file.Hash, true)
|
||||
if status != warehouse.StatusOK {
|
||||
return false
|
||||
|
||||
@@ -1,109 +1,109 @@
|
||||
/*
|
||||
File Name: HTTP Range.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Fork from https://golang.org/src/net/http/fs.go
|
||||
|
||||
// HTTPRange represents an HTTP range
|
||||
type HTTPRange struct {
|
||||
start, length int
|
||||
}
|
||||
|
||||
// ParseRangeHeader parses a Range header string as per RFC 7233.
|
||||
func ParseRangeHeader(s string, size int, noMultiRange bool) ([]HTTPRange, error) {
|
||||
if s == "" {
|
||||
return nil, nil // header not present
|
||||
}
|
||||
const b = "bytes="
|
||||
if !strings.HasPrefix(s, b) {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
var ranges []HTTPRange
|
||||
for _, ra := range strings.Split(s[len(b):], ",") {
|
||||
ra = textproto.TrimString(ra)
|
||||
if ra == "" {
|
||||
continue
|
||||
}
|
||||
i := strings.Index(ra, "-")
|
||||
if i < 0 {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:])
|
||||
var r HTTPRange
|
||||
if start == "" {
|
||||
if size < 0 {
|
||||
return nil, errors.New("range start relative to end not supported")
|
||||
}
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the file.
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if int(i) > size {
|
||||
i = int64(size)
|
||||
}
|
||||
r.start = size - int(i)
|
||||
r.length = size - r.start
|
||||
} else {
|
||||
i, err := strconv.ParseInt(start, 10, 64)
|
||||
if err != nil || i < 0 {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if size > 0 && int(i) >= size {
|
||||
// If the range begins after the size of the content, then it does not overlap. -> always return error.
|
||||
return nil, errors.New("start out of range")
|
||||
}
|
||||
r.start = int(i)
|
||||
if end == "" {
|
||||
// If no end is specified, range extends to end of the file.
|
||||
if size < 0 {
|
||||
//return nil, errors.New("open range not supported")
|
||||
r.length = -1
|
||||
} else {
|
||||
r.length = size - r.start
|
||||
}
|
||||
} else {
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil || r.start > int(i) {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if size > 0 && int(i) >= size {
|
||||
i = int64(size - 1)
|
||||
}
|
||||
r.length = int(i) - r.start + 1
|
||||
}
|
||||
}
|
||||
ranges = append(ranges, r)
|
||||
if noMultiRange && len(ranges) > 1 {
|
||||
return nil, errors.New("multiple ranges not supported")
|
||||
}
|
||||
}
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
// setContentLengthRangeHeader sets the appropriate Content-Length and Content-Range headers
|
||||
func setContentLengthRangeHeader(w http.ResponseWriter, offset, transferSize, fileSize uint64, ranges []HTTPRange) {
|
||||
// Set the Content-Length header, always to the actual size of transferred data.
|
||||
w.Header().Set("Content-Length", strconv.FormatUint(transferSize, 10))
|
||||
|
||||
// Set the Content-Range header if needed.
|
||||
if len(ranges) == 1 {
|
||||
w.Header().Set("Content-Range", "bytes "+strconv.FormatUint(offset, 10)+"-"+strconv.FormatUint(offset+transferSize-1, 10)+"/"+strconv.FormatUint(fileSize, 10))
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
/*
|
||||
File Name: HTTP Range.go
|
||||
Copyright: 2021 Peernet Foundation s.r.o.
|
||||
Author: Peter Kleissner
|
||||
*/
|
||||
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Fork from https://golang.org/src/net/http/fs.go
|
||||
|
||||
// HTTPRange represents an HTTP range
|
||||
type HTTPRange struct {
|
||||
start, length int
|
||||
}
|
||||
|
||||
// ParseRangeHeader parses a Range header string as per RFC 7233.
|
||||
func ParseRangeHeader(s string, size int, noMultiRange bool) ([]HTTPRange, error) {
|
||||
if s == "" {
|
||||
return nil, nil // header not present
|
||||
}
|
||||
const b = "bytes="
|
||||
if !strings.HasPrefix(s, b) {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
var ranges []HTTPRange
|
||||
for _, ra := range strings.Split(s[len(b):], ",") {
|
||||
ra = textproto.TrimString(ra)
|
||||
if ra == "" {
|
||||
continue
|
||||
}
|
||||
i := strings.Index(ra, "-")
|
||||
if i < 0 {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
start, end := textproto.TrimString(ra[:i]), textproto.TrimString(ra[i+1:])
|
||||
var r HTTPRange
|
||||
if start == "" {
|
||||
if size < 0 {
|
||||
return nil, errors.New("range start relative to end not supported")
|
||||
}
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the File.
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if int(i) > size {
|
||||
i = int64(size)
|
||||
}
|
||||
r.start = size - int(i)
|
||||
r.length = size - r.start
|
||||
} else {
|
||||
i, err := strconv.ParseInt(start, 10, 64)
|
||||
if err != nil || i < 0 {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if size > 0 && int(i) >= size {
|
||||
// If the range begins after the size of the content, then it does not overlap. -> always return error.
|
||||
return nil, errors.New("start out of range")
|
||||
}
|
||||
r.start = int(i)
|
||||
if end == "" {
|
||||
// If no end is specified, range extends to end of the File.
|
||||
if size < 0 {
|
||||
//return nil, errors.New("open range not supported")
|
||||
r.length = -1
|
||||
} else {
|
||||
r.length = size - r.start
|
||||
}
|
||||
} else {
|
||||
i, err := strconv.ParseInt(end, 10, 64)
|
||||
if err != nil || r.start > int(i) {
|
||||
return nil, errors.New("invalid range")
|
||||
}
|
||||
if size > 0 && int(i) >= size {
|
||||
i = int64(size - 1)
|
||||
}
|
||||
r.length = int(i) - r.start + 1
|
||||
}
|
||||
}
|
||||
ranges = append(ranges, r)
|
||||
if noMultiRange && len(ranges) > 1 {
|
||||
return nil, errors.New("multiple ranges not supported")
|
||||
}
|
||||
}
|
||||
return ranges, nil
|
||||
}
|
||||
|
||||
// setContentLengthRangeHeader sets the appropriate Content-Length and Content-Range headers
|
||||
func setContentLengthRangeHeader(w http.ResponseWriter, offset, transferSize, fileSize uint64, ranges []HTTPRange) {
|
||||
// Set the Content-Length header, always to the actual size of transferred data.
|
||||
w.Header().Set("Content-Length", strconv.FormatUint(transferSize, 10))
|
||||
|
||||
// Set the Content-Range header if needed.
|
||||
if len(ranges) == 1 {
|
||||
w.Header().Set("Content-Range", "bytes "+strconv.FormatUint(offset, 10)+"-"+strconv.FormatUint(offset+transferSize-1, 10)+"/"+strconv.FormatUint(fileSize, 10))
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// apiProfileData contains profile metadata stored on the blockchain. Any data is treated as untrusted and unverified by default.
|
||||
type apiProfileData struct {
|
||||
Fields []apiBlockRecordProfile `json:"fields"` // All fields
|
||||
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See blockchain.StatusX.
|
||||
Status int `json:"Status"` // Status of the operation, only used when this structure is returned from the API. See blockchain.StatusX.
|
||||
}
|
||||
|
||||
// apiBlockRecordProfile provides information about the end user. Note that all profile data is arbitrary and shall be considered untrusted and unverified.
|
||||
|
||||
@@ -48,7 +48,7 @@ resultLoop:
|
||||
continue
|
||||
}
|
||||
|
||||
// Deduplicate based on file hash from the same peer.
|
||||
// Deduplicate based on File Hash from the same Peer.
|
||||
for n := range job.AllFiles {
|
||||
if bytes.Equal(job.AllFiles[n].Hash, file.Hash) && bytes.Equal(job.AllFiles[n].NodeID, file.NodeID) {
|
||||
continue resultLoop
|
||||
|
||||
@@ -23,8 +23,8 @@ type SearchFilter struct {
|
||||
FileType int // File type such as binary, text document etc. See core.TypeX. -1 = not used.
|
||||
FileFormat int // File format such as PDF, Word, Ebook, etc. See core.FormatX. -1 = not used.
|
||||
Sort int // Sort order. See SortX.
|
||||
SizeMin int // Min file size in bytes. -1 = not used.
|
||||
SizeMax int // Max file size in bytes. -1 = not used.
|
||||
SizeMin int // Min File size in bytes. -1 = not used.
|
||||
SizeMax int // Max File size in bytes. -1 = not used.
|
||||
}
|
||||
|
||||
// SearchJob is a collection of search jobs
|
||||
@@ -34,7 +34,7 @@ type SearchJob struct {
|
||||
timeout time.Duration // timeout set for all searches
|
||||
maxResult int // max results user-facing.
|
||||
|
||||
filtersStart SearchFilter // Filters when starting the search. They cannot be changed later on. Any incoming file is checked against them, even if there are different runtime filters.
|
||||
filtersStart SearchFilter // Filters when starting the search. They cannot be changed later on. Any incoming File is checked against them, even if there are different runtime filters.
|
||||
filtersRuntime SearchFilter // Runtime Filters. They allow filtering results after they were received.
|
||||
|
||||
// File statistics (filters are ignored) of returned results. Map value is always count of files.
|
||||
@@ -48,7 +48,7 @@ type SearchJob struct {
|
||||
|
||||
// -- result data --
|
||||
|
||||
// Status indicates the overall search status. This will be removed later when relying on search clients.
|
||||
// Status indicates the overall search Status. This will be removed later when relying on search clients.
|
||||
Status int
|
||||
|
||||
// runtime data
|
||||
@@ -65,7 +65,7 @@ type SearchJob struct {
|
||||
// List of all files. Does not change based on sorting or runtime filters. This list only gets expanded.
|
||||
AllFiles []*ApiFile
|
||||
|
||||
ResultSync sync.Mutex // ResultSync ensures unique access to the file results
|
||||
ResultSync sync.Mutex // ResultSync ensures unique access to the File results
|
||||
|
||||
currentOffset int // for always getting the next results
|
||||
}
|
||||
@@ -235,7 +235,7 @@ func (job *SearchJob) RuntimeFilter(Filter SearchFilter) {
|
||||
}
|
||||
}
|
||||
|
||||
// isFileFiltered returns true if the file conforms to the runtime filter. If there is no runtime filter, it always returns true.
|
||||
// isFileFiltered returns true if the File conforms to the runtime filter. If there is no runtime filter, it always returns true.
|
||||
func (job *SearchJob) isFileFiltered(file *ApiFile) bool {
|
||||
if job.filtersRuntime.FileType >= 0 && file.Type != uint8(job.filtersRuntime.FileType) {
|
||||
return false
|
||||
@@ -245,7 +245,7 @@ func (job *SearchJob) isFileFiltered(file *ApiFile) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Note: If the date is not available in the file, it will be filtered out. Since this is the mapped Shared Date this should normally not occur though.
|
||||
// Note: If the date is not available in the File, it will be filtered out. Since this is the mapped Shared Date this should normally not occur though.
|
||||
if job.filtersRuntime.IsDates && (file.Date.IsZero() || file.Date.Before(job.filtersRuntime.DateFrom) || file.Date.After(job.filtersRuntime.DateTo)) {
|
||||
return false
|
||||
}
|
||||
@@ -302,7 +302,7 @@ func (job *SearchJob) IsSearchResults() bool {
|
||||
return len(job.Files) > 0 || !job.IsTerminated()
|
||||
}
|
||||
|
||||
// isFileReceived checks if a file was already received, preventing double results
|
||||
// isFileReceived checks if a File was already received, preventing double results
|
||||
func (job *SearchJob) isFileReceived(id uuid.UUID) (exists bool) {
|
||||
// Future: A map would be likely faster than iterating over all results.
|
||||
for m := range job.AllFiles {
|
||||
@@ -394,8 +394,8 @@ type SearchStatisticRecord struct {
|
||||
// SearchStatisticData contains statistics on search results.
|
||||
type SearchStatisticData struct {
|
||||
Date []SearchStatisticRecordDay `json:"date"` // Files per date
|
||||
FileType []SearchStatisticRecord `json:"filetype"` // Files per file type
|
||||
FileFormat []SearchStatisticRecord `json:"fileformat"` // Files per file format
|
||||
FileType []SearchStatisticRecord `json:"filetype"` // Files per File type
|
||||
FileFormat []SearchStatisticRecord `json:"fileformat"` // Files per File format
|
||||
Total int `json:"total"` // Total count of files
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ func (job *SearchJob) statsAdd(files ...*ApiFile) {
|
||||
defer job.stats.Unlock()
|
||||
|
||||
for _, file := range files {
|
||||
// Use file's Date field if available.
|
||||
// Use File's Date field if available.
|
||||
if !file.Date.IsZero() {
|
||||
// Files per day
|
||||
date := file.Date.Truncate(24 * time.Hour)
|
||||
|
||||
@@ -34,8 +34,8 @@ type SearchRequest struct {
|
||||
TerminateID []uuid.UUID `json:"terminate"` // Optional: Previous search IDs to terminate. This is if the user makes a new search from the same tab. Same as first calling /search/terminate.
|
||||
FileType int `json:"filetype"` // File type such as binary, text document etc. See core.TypeX. -1 = not used.
|
||||
FileFormat int `json:"fileformat"` // File format such as PDF, Word, Ebook, etc. See core.FormatX. -1 = not used.
|
||||
SizeMin int `json:"sizemin"` // Min file size in bytes. -1 = not used.
|
||||
SizeMax int `json:"sizemax"` // Max file size in bytes. -1 = not used.
|
||||
SizeMin int `json:"sizemin"` // Min File size in bytes. -1 = not used.
|
||||
SizeMax int `json:"sizemax"` // Max File size in bytes. -1 = not used.
|
||||
}
|
||||
|
||||
// Sort orders
|
||||
@@ -56,12 +56,12 @@ const (
|
||||
// SearchRequestResponse is the result to the initial search request
|
||||
type SearchRequestResponse struct {
|
||||
ID uuid.UUID `json:"ID"` // ID of the search job. This is used to get the results.
|
||||
Status int `json:"status"` // Status of the search: 0 = Success (ID valid), 1 = Invalid Term, 2 = Error Max Concurrent Searches
|
||||
Status int `json:"Status"` // Status of the search: 0 = Success (ID valid), 1 = Invalid Term, 2 = Error Max Concurrent Searches
|
||||
}
|
||||
|
||||
// SearchResult contains the search results.
|
||||
type SearchResult struct {
|
||||
Status int `json:"status"` // Status: 0 = Success with results, 1 = No more results available, 2 = Search ID not found, 3 = No results yet available keep trying
|
||||
Status int `json:"Status"` // Status: 0 = Success with results, 1 = No more results available, 2 = Search ID not found, 3 = No results yet available keep trying
|
||||
Files []ApiFile `json:"files"` // List of files found
|
||||
Statistic interface{} `json:"statistic"` // Statistics of all results (independent from applied filters), if requested. Only set if files are returned (= if statistics changed). See SearchStatisticData.
|
||||
}
|
||||
@@ -69,7 +69,7 @@ type SearchResult struct {
|
||||
// SearchStatistic contains statistics on search results. Statistics are always calculated over all results, regardless of any applied runtime filters.
|
||||
type SearchStatistic struct {
|
||||
SearchStatisticData
|
||||
Status int `json:"status"` // Status: 0 = Success
|
||||
Status int `json:"Status"` // Status: 0 = Success
|
||||
IsTerminated bool `json:"terminated"` // Whether the search is terminated, meaning that statistics won't change
|
||||
}
|
||||
|
||||
@@ -119,11 +119,11 @@ Optional parameters:
|
||||
&filetype=[File Type]
|
||||
&fileformat=[File Format]
|
||||
&from=[Date From]&to=[Date To]
|
||||
&sizemin=[Minimum file size]
|
||||
&sizemax=[Maximum file size]
|
||||
&sizemin=[Minimum File size]
|
||||
&sizemax=[Maximum File size]
|
||||
&sort=[sort order]
|
||||
&offset=[absolute offset] with &limit=[records] to get items pagination style. Returned items (and ones before) are automatically frozen.
|
||||
Result: 200 with JSON structure SearchResult. Check the field status.
|
||||
Result: 200 with JSON structure SearchResult. Check the field Status.
|
||||
*/
|
||||
func (api *WebapiInstance) apiSearchResult(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
@@ -176,7 +176,7 @@ func (api *WebapiInstance) apiSearchResult(w http.ResponseWriter, r *http.Reques
|
||||
result.Files = append(result.Files, *resultFiles[n])
|
||||
}
|
||||
|
||||
// set the status
|
||||
// set the Status
|
||||
if len(result.Files) > 0 {
|
||||
if job.IsSearchResults() {
|
||||
result.Status = 0 // 0 = Success with results
|
||||
@@ -323,7 +323,7 @@ func (api *WebapiInstance) apiSearchTerminate(w http.ResponseWriter, r *http.Req
|
||||
apiSearchStatistic returns search result statistics. Statistics are always calculated over all results, regardless of any applied runtime filters.
|
||||
|
||||
Request: GET /search/result?ID=[UUID]
|
||||
Result: 200 with JSON structure SearchStatistic. Check the field status (0 = Success, 2 = ID not found).
|
||||
Result: 200 with JSON structure SearchStatistic. Check the field Status (0 = Success, 2 = ID not found).
|
||||
*/
|
||||
func (api *WebapiInstance) apiSearchStatistic(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
@@ -346,11 +346,11 @@ func (api *WebapiInstance) apiSearchStatistic(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
/*
|
||||
apiExplore returns recently shared files in Peernet. Results are returned in real-time. The file type is an optional filter. See TypeX.
|
||||
apiExplore returns recently shared files in Peernet. Results are returned in real-time. The File type is an optional filter. See TypeX.
|
||||
Special type -2 = Binary, Compressed, Container, Executable. This special type includes everything except Documents, Video, Audio, Ebooks, Picture, Text.
|
||||
|
||||
Request: GET /explore?limit=[max records]&type=[file type]&offset=[offset]
|
||||
Result: 200 with JSON structure SearchResult. Check the field status.
|
||||
Request: GET /explore?limit=[max records]&type=[File type]&offset=[offset]
|
||||
Result: 200 with JSON structure SearchResult. Check the field Status.
|
||||
*/
|
||||
func (api *WebapiInstance) apiExplore(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
|
||||
@@ -18,7 +18,7 @@ func (api *WebapiInstance) queryRecentShared(backend *core.Backend, fileType int
|
||||
limitPeer = 1
|
||||
}
|
||||
|
||||
// Use the peer list to know about active peers. Random order!
|
||||
// Use the Peer list to know about active peers. Random order!
|
||||
peerList := api.Backend.PeerlistGet()
|
||||
|
||||
// Files from peers exceeding the limit. It is used if from all peers the total limit is not reached.
|
||||
@@ -48,7 +48,7 @@ func (api *WebapiInstance) queryRecentShared(backend *core.Backend, fileType int
|
||||
file.Tags = append(file.Tags, blockchain.TagFromText(blockchain.TagSharedByGeoIP, sharedByGeoIP))
|
||||
}
|
||||
|
||||
// found a new file! append.
|
||||
// found a new File! append.
|
||||
if filesFromPeer < limitPeer {
|
||||
filesFromPeer++
|
||||
|
||||
@@ -77,7 +77,7 @@ func (api *WebapiInstance) queryRecentShared(backend *core.Backend, fileType int
|
||||
return
|
||||
}
|
||||
|
||||
// isFileTypeMatchBlock checks if the file type matches. -1 = accept any. -2 = core.TypeBinary, core.TypeCompressed, core.TypeContainer, core.TypeExecutable.
|
||||
// isFileTypeMatchBlock checks if the File type matches. -1 = accept any. -2 = core.TypeBinary, core.TypeCompressed, core.TypeContainer, core.TypeExecutable.
|
||||
func isFileTypeMatchBlock(file *blockchain.BlockRecordFile, fileType int) bool {
|
||||
if fileType == -1 {
|
||||
return true
|
||||
|
||||
@@ -19,17 +19,17 @@ func apiTest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type apiResponseStatus struct {
|
||||
Status int `json:"status"` // Status code: 0 = Ok.
|
||||
Status int `json:"Status"` // Status code: 0 = Ok.
|
||||
IsConnected bool `json:"isconnected"` // Whether connected to Peernet.
|
||||
CountPeerList int `json:"countpeerlist"` // Count of peers in the peer list. Note that this contains peers that are considered inactive, but have not yet been removed from the list.
|
||||
CountPeerList int `json:"countpeerlist"` // Count of peers in the Peer list. Note that this contains peers that are considered inactive, but have not yet been removed from the list.
|
||||
CountNetwork int `json:"countnetwork"` // Count of total peers in the network.
|
||||
// This is usually a higher number than CountPeerList, which just represents the current number of connected peers.
|
||||
// The CountNetwork number is going to be queried from root peers which may or may not have a limited view.
|
||||
}
|
||||
|
||||
/*
|
||||
apiStatus returns the current connectivity status to the network
|
||||
Request: GET /status
|
||||
apiStatus returns the current connectivity Status to the network
|
||||
Request: GET /Status
|
||||
Result: 200 with JSON structure Status
|
||||
*/
|
||||
func (api *WebapiInstance) apiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -46,7 +46,7 @@ func (api *WebapiInstance) apiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type apiResponsePeerSelf struct {
|
||||
PeerID string `json:"peerid"` // Peer ID. This is derived from the public in compressed form.
|
||||
NodeID string `json:"nodeid"` // Node ID. This is the blake3 hash of the peer ID and used in the DHT.
|
||||
NodeID string `json:"nodeid"` // Node ID. This is the blake3 Hash of the Peer ID and used in the DHT.
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -85,10 +85,10 @@ func (api *WebapiInstance) apiAccountDelete(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
/*
|
||||
apiStatusPeers returns the information about peers currently connected.
|
||||
The GeoIP information may not alawys be available, for example if the GeoIP file is not available or the mapping from IP address to location is not available.
|
||||
The GeoIP information may not alawys be available, for example if the GeoIP File is not available or the mapping from IP address to location is not available.
|
||||
Peers that are connected only via local network will not have a geo location.
|
||||
|
||||
Request: GET /status/peers
|
||||
Request: GET /Status/peers
|
||||
Result: 200 with JSON array apiResponsePeerInfo
|
||||
*/
|
||||
func (api *WebapiInstance) apiStatusPeers(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -117,10 +117,10 @@ func (api *WebapiInstance) apiStatusPeers(w http.ResponseWriter, r *http.Request
|
||||
|
||||
type apiResponsePeerInfo struct {
|
||||
PeerID []byte `json:"peerid"` // Peer ID. This is derived from the public in compressed form.
|
||||
NodeID []byte `json:"nodeid"` // Node ID. This is the blake3 hash of the peer ID and used in the DHT.
|
||||
NodeID []byte `json:"nodeid"` // Node ID. This is the blake3 Hash of the Peer ID and used in the DHT.
|
||||
GeoIP string `json:"geoip"` // GeoIP location as "Latitude,Longitude" CSV format. Empty if location not available.
|
||||
UserAgent string `json:"useragent"` // User Agent.
|
||||
IsRoot bool `json:"isroot"` // If the peer is a root peer.
|
||||
IsRoot bool `json:"isroot"` // If the Peer is a root Peer.
|
||||
BlockchainHeight uint64 `json:"blockchainheight"` // Blockchain height
|
||||
BlockchainVersion uint64 `json:"blockchainversion"` // Blockchain version
|
||||
}
|
||||
|
||||
@@ -13,31 +13,31 @@ import (
|
||||
"github.com/PeernetOfficial/core/warehouse"
|
||||
)
|
||||
|
||||
// WarehouseResult is the response to creating a new file in the warehouse
|
||||
// WarehouseResult is the response to creating a new File in the warehouse
|
||||
type WarehouseResult struct {
|
||||
Status int `json:"status"` // See warehouse.StatusX.
|
||||
Hash []byte `json:"hash"` // Hash of the file.
|
||||
Status int `json:"Status"` // See warehouse.StatusX.
|
||||
Hash []byte `json:"Hash"` // Hash of the File.
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseCreateFile creates a file in the warehouse.
|
||||
apiWarehouseCreateFile creates a File in the warehouse.
|
||||
|
||||
Request: POST /warehouse/create with raw data to create as new file
|
||||
Request: POST /warehouse/create with raw data to create as new File
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseCreateFile(w http.ResponseWriter, r *http.Request) {
|
||||
hash, status, err := api.Backend.UserWarehouse.CreateFile(r.Body, 0)
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.CreateFile", "status %d error: %v", status, err)
|
||||
api.Backend.LogError("warehouse.CreateFile", "Status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseCreateFilePath creates a file in the warehouse by copying it from an existing file.
|
||||
Warning: An attacker could supply any local file using this function, put them into storage and read them! No input path verification or limitation is done.
|
||||
apiWarehouseCreateFilePath creates a File in the warehouse by copying it from an existing File.
|
||||
Warning: An attacker could supply any local File using this function, put them into storage and read them! No input path verification or limitation is done.
|
||||
In the future the API should be secured using a random API key and setting the CORS header prohibiting regular browsers to access the API.
|
||||
|
||||
Request: GET /warehouse/create/path?path=[target path on disk]
|
||||
@@ -54,24 +54,24 @@ func (api *WebapiInstance) apiWarehouseCreateFilePath(w http.ResponseWriter, r *
|
||||
hash, status, err := api.Backend.UserWarehouse.CreateFileFromPath(filePath)
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.CreateFile", "status %d error: %v", status, err)
|
||||
api.Backend.LogError("warehouse.CreateFile", "Status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseReadFile reads a file in the warehouse.
|
||||
apiWarehouseReadFile reads a File in the warehouse.
|
||||
|
||||
Request: GET /warehouse/read?hash=[hash]
|
||||
Optional parameters &offset=[file offset]&limit=[read limit in bytes]
|
||||
Response: 200 with the raw file data
|
||||
404 if file was not found
|
||||
500 in case of internal error opening the file
|
||||
Request: GET /warehouse/read?Hash=[Hash]
|
||||
Optional parameters &offset=[File offset]&limit=[read limit in bytes]
|
||||
Response: 200 with the raw File data
|
||||
404 if File was not found
|
||||
500 in case of internal error opening the File
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseReadFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("Hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
@@ -90,23 +90,23 @@ func (api *WebapiInstance) apiWarehouseReadFile(w http.ResponseWriter, r *http.R
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
// Cannot catch warehouse.StatusErrorReadFile since data may have been already returned.
|
||||
// In the future a special header indicating the expected file length could be sent (would require a callback in ReadFile), although the caller should already know the file size based on metadata.
|
||||
// In the future a special header indicating the expected File length could be sent (would require a callback in ReadFile), although the caller should already know the File size based on metadata.
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.ReadFile", "status %d read %d error: %v", status, bytesRead, err)
|
||||
api.Backend.LogError("warehouse.ReadFile", "Status %d read %d error: %v", status, bytesRead, err)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseDeleteFile deletes a file in the warehouse.
|
||||
apiWarehouseDeleteFile deletes a File in the warehouse.
|
||||
|
||||
Request: GET /warehouse/delete?hash=[hash]
|
||||
Request: GET /warehouse/delete?Hash=[Hash]
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseDeleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("Hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
@@ -115,23 +115,23 @@ func (api *WebapiInstance) apiWarehouseDeleteFile(w http.ResponseWriter, r *http
|
||||
status, err := api.Backend.UserWarehouse.DeleteFile(hash)
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.DeleteFile", "status %d error: %v", status, err)
|
||||
api.Backend.LogError("warehouse.DeleteFile", "Status %d error: %v", status, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
}
|
||||
|
||||
/*
|
||||
apiWarehouseReadFilePath reads a file from the warehouse and stores it to the target file. It fails with StatusErrorTargetExists if the target file already exists.
|
||||
The path must include the full directory and file name.
|
||||
apiWarehouseReadFilePath reads a File from the warehouse and stores it to the target File. It fails with StatusErrorTargetExists if the target File already exists.
|
||||
The path must include the full directory and File name.
|
||||
|
||||
Request: GET /warehouse/read/path?hash=[hash]&path=[target path on disk]
|
||||
Optional parameters &offset=[file offset]&limit=[read limit in bytes]
|
||||
Request: GET /warehouse/read/path?Hash=[Hash]&path=[target path on disk]
|
||||
Optional parameters &offset=[File offset]&limit=[read limit in bytes]
|
||||
Response: 200 with JSON structure WarehouseResult
|
||||
*/
|
||||
func (api *WebapiInstance) apiWarehouseReadFilePath(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash"))
|
||||
hash, valid1 := DecodeBlake3Hash(r.Form.Get("Hash"))
|
||||
if !valid1 {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
@@ -144,7 +144,7 @@ func (api *WebapiInstance) apiWarehouseReadFilePath(w http.ResponseWriter, r *ht
|
||||
status, bytesRead, err := api.Backend.UserWarehouse.ReadFileToDisk(hash, int64(offset), int64(limit), targetFile)
|
||||
|
||||
if err != nil {
|
||||
api.Backend.LogError("warehouse.ReadFileToDisk", "status %d read %d error: %v", status, bytesRead, err)
|
||||
api.Backend.LogError("warehouse.ReadFileToDisk", "Status %d read %d error: %v", status, bytesRead, err)
|
||||
}
|
||||
|
||||
EncodeJSON(api.Backend, w, r, WarehouseResult{Status: status, Hash: hash})
|
||||
|
||||
Reference in New Issue
Block a user