diff --git a/File Metadata.go b/File Metadata.go index 3e080a5..919d38e 100644 --- a/File Metadata.go +++ b/File Metadata.go @@ -32,7 +32,9 @@ const ( // Date returns the tags data as date encoded func (tag *BlockRecordFileTag) Date() (time.Time, error) { - if len(tag.Data) != 8 { + if tag == nil { + return time.Time{}, errors.New("tag not available") + } else if len(tag.Data) != 8 { return time.Time{}, errors.New("file tag date invalid size") } @@ -89,3 +91,14 @@ func IsTagVirtual(Type uint16) bool { return false } } + +// GetTag returns the tag with the type or nil if not available. +func (file *BlockRecordFile) GetTag(Type uint16) (tag *BlockRecordFileTag) { + for n := range file.Tags { + if file.Tags[n].Type == Type { + return &file.Tags[n] + } + } + + return nil +} diff --git a/webapi/Blockchain.go b/webapi/Blockchain.go index 3a7b984..d0fb5c7 100644 --- a/webapi/Blockchain.go +++ b/webapi/Blockchain.go @@ -14,8 +14,6 @@ import ( "github.com/PeernetOfficial/core" ) -const dateFormat = "2006-01-02 15:04:05" - type apiBlockchainHeader struct { PeerID string `json:"peerid"` // Peer ID hex encoded. Version uint64 `json:"version"` // Current version number of the blockchain. diff --git a/webapi/Download Temp.go b/webapi/Download Temp.go index a0931b9..9ff7564 100644 --- a/webapi/Download Temp.go +++ b/webapi/Download Temp.go @@ -19,8 +19,7 @@ func (info *downloadInfo) Start() { time.Sleep(time.Second * time.Duration(rand.Intn(5))) // request metadata - info.file = createTestResult(-1) - info.fileU = blockRecordFileToAPI(info.file) + info.file = blockRecordFileToAPI(createTestResult(-1)) // join swarm diff --git a/webapi/Download.go b/webapi/Download.go index fd3d76f..d5c958a 100644 --- a/webapi/Download.go +++ b/webapi/Download.go @@ -15,15 +15,14 @@ import ( "sync" "time" - "github.com/PeernetOfficial/core" "github.com/google/uuid" ) type apiResponseDownloadStatus struct { - APIStatus int `json:"apistatus"` // Status of the API call. See DownloadResponseX. - ID uuid.UUID `json:"id"` // Download ID. This can be used to query the latest status and take actions. - DownloadStatus int `json:"downloadstatus"` // Status of the download. See DownloadX. - File apiBlockRecordFile `json:"file"` // File information. Only available for status >= DownloadWaitSwarm. + APIStatus int `json:"apistatus"` // Status of the API call. See DownloadResponseX. + ID uuid.UUID `json:"id"` // Download ID. This can be used to query the latest status and take actions. + DownloadStatus int `json:"downloadstatus"` // Status of the download. See DownloadX. + File apiFile `json:"file"` // File information. Only available for status >= DownloadWaitSwarm. Progress struct { TotalSize uint64 `json:"totalsize"` // Total size in bytes. DownloadedSize uint64 `json:"downloadedsize"` // Count of bytes download so far. @@ -38,7 +37,7 @@ 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. + 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. ) @@ -46,7 +45,7 @@ const ( 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. + DownloadActive = 2 // Active downloading. It could still be stuck at any percentage (including 0%) if no seeders are available. DownloadPause = 3 // Paused by the user. DownloadCanceled = 4 // Canceled by the user before the download finished. Once canceled, a new download has to be started if the file shall be downloaded. DownloadFinished = 5 // Download finished 100%. @@ -118,7 +117,7 @@ func apiDownloadStatus(w http.ResponseWriter, r *http.Request) { response := apiResponseDownloadStatus{APIStatus: DownloadResponseSuccess, ID: info.id, DownloadStatus: info.status} if info.status >= DownloadWaitSwarm { - response.File = info.fileU + response.File = info.file response.Progress.TotalSize = info.file.Size response.Progress.DownloadedSize = info.DiskFile.StoredSize @@ -189,8 +188,7 @@ type downloadInfo struct { 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 + file apiFile // File metadata (only status >= DownloadWaitSwarm) DiskFile struct { // Target file on disk to store downloaded data Name string // File name diff --git a/webapi/File.go b/webapi/File.go index 5038b6e..edca1bb 100644 --- a/webapi/File.go +++ b/webapi/File.go @@ -25,8 +25,8 @@ type apiFileMetadata struct { Number uint64 `json:"number"` // Number } -// apiBlockRecordFile is the metadata of a file published on the blockchain -type apiBlockRecordFile struct { +// 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 Type uint8 `json:"type"` // File Type. For example audio or document. See TypeX. @@ -42,8 +42,8 @@ type apiBlockRecordFile struct { // --- conversion from core to API data --- -func blockRecordFileToAPI(input core.BlockRecordFile) (output apiBlockRecordFile) { - output = apiBlockRecordFile{ID: input.ID, Hash: input.Hash, NodeID: input.NodeID, Type: input.Type, Format: input.Format, Size: input.Size, Metadata: []apiFileMetadata{}} +func blockRecordFileToAPI(input core.BlockRecordFile) (output apiFile) { + output = apiFile{ID: input.ID, Hash: input.Hash, NodeID: input.NodeID, Type: input.Type, Format: input.Format, Size: input.Size, Metadata: []apiFileMetadata{}} for _, tag := range input.Tags { switch tag.Type { @@ -77,7 +77,7 @@ func blockRecordFileToAPI(input core.BlockRecordFile) (output apiBlockRecordFile return output } -func blockRecordFileFromAPI(input apiBlockRecordFile) (output core.BlockRecordFile) { +func blockRecordFileFromAPI(input apiFile) (output core.BlockRecordFile) { output = core.BlockRecordFile{ID: input.ID, Hash: input.Hash, Type: input.Type, Format: input.Format, Size: input.Size} if input.Name != "" { @@ -113,8 +113,8 @@ func blockRecordFileFromAPI(input apiBlockRecordFile) (output core.BlockRecordFi // apiBlockAddFiles contains a list of files from the blockchain type apiBlockAddFiles struct { - Files []apiBlockRecordFile `json:"files"` // List of files - Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. + 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. } /* diff --git a/webapi/Search Job.go b/webapi/Search Job.go new file mode 100644 index 0000000..6527515 --- /dev/null +++ b/webapi/Search Job.go @@ -0,0 +1,376 @@ +/* +File Name: Search Job.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package webapi + +import ( + "sort" + "sync" + "time" + + "github.com/google/uuid" +) + +// SearchFilter allows to filter search results based on the criteria. +type SearchFilter struct { + IsDates bool // Whether the from/to dates are valid, both are required. + DateFrom time.Time // Optional date from + DateTo time.Time // Optional date to + 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. +} + +// SearchJob is a collection of search jobs +type SearchJob struct { + // input settings + id uuid.UUID // The job id + 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. + filtersRuntime SearchFilter // Runtime Filters. They allow filtering results after they were received. + + // -- result data -- + + // runtime data + //clients []*SearchClient // all search clients + clientsMutex sync.Mutex // mutex for manipulating client list + + // List of files found but not yet returned via API to the caller. They are subject to sorting. + Files []*apiFile + requireSort bool // if Files requires sort before returning the results + + // FreezeFiles is a list of items that were already finally delivered via the API. They may NOT change in sorting. + FreezeFiles []*apiFile + + // 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 + + currentOffset int // for always getting the next results +} + +// CreateSearchJob creates a new search job and adds it to the lookup list. +// Timeout and MaxResults must be set and must not be 0. +func CreateSearchJob(Timeout time.Duration, MaxResults, Sort, FileType, FileFormat int, DateFrom, DateTo time.Time) (job *SearchJob) { + job = &SearchJob{} + job.id = uuid.New() + job.timeout = Timeout + job.maxResult = MaxResults + + job.filtersStart = SearchFilter{Sort: Sort, FileType: FileType, FileFormat: FileFormat} + + if !DateFrom.IsZero() && !DateTo.IsZero() && DateFrom.Before(DateTo) { + job.filtersStart.DateFrom = DateFrom + job.filtersStart.DateTo = DateTo + job.filtersStart.IsDates = true + } + + // initialize the runtime filters as the same + job.filtersRuntime = job.filtersStart + + // add to the list of jobs + allJobsMutex.Lock() + allJobs[job.id] = job + allJobsMutex.Unlock() + + return +} + +// ReturnResult returns the selected results. +func (job *SearchJob) ReturnResult(Offset, Limit int) (Result []*apiFile) { + if Limit == 0 { + return Result + } + + job.ResultSync.Lock() + defer job.ResultSync.Unlock() + + // serve files from freezed list? + if Offset < len(job.FreezeFiles) { + countCopy := len(job.FreezeFiles) - Offset + if countCopy > Limit { + countCopy = Limit + } + Result = job.FreezeFiles[Offset : Offset+countCopy] + Limit -= countCopy + Offset = 0 + } else { + Offset -= len(job.FreezeFiles) + } + + if Limit == 0 { + return Result + } + + // go through the live results and fill the list + if Offset >= len(job.Files) { // offset wants to skip entire queue? + job.FreezeFiles = append(job.FreezeFiles, job.Files...) + job.Files = nil + return Result + } + + // check if a sort is required before using this queue + if job.requireSort { + job.requireSort = false + + job.Files = SortItems(job.Files, job.filtersRuntime.Sort) + } + + // set the amount of items to copy + countCopy := len(job.Files) - Offset + if countCopy > Limit { + countCopy = Limit + } + + // copy the results and freeze them + Result = append(Result, job.Files[Offset:Offset+countCopy]...) + + // note that freeze disregards the offset, it has to freeze any elements before! + job.FreezeFiles = append(job.FreezeFiles, job.Files[:Offset+countCopy]...) + job.Files = job.Files[Offset+countCopy:] + + //Limit -= countCopy + + return Result +} + +// ReturnNext returns the next results. Call must be serialized. +func (job *SearchJob) ReturnNext(Limit int) (Result []*apiFile) { + Result = job.ReturnResult(job.currentOffset, Limit) + job.currentOffset += len(Result) + + return +} + +// PeekResult returns the selected results but will not change any freezed items or impact auto offset +func (job *SearchJob) PeekResult(Offset, Limit int) (Result []*apiFile) { + job.ResultSync.Lock() + defer job.ResultSync.Unlock() + + // serve items from freezed list? + if Offset < len(job.FreezeFiles) { + countCopy := len(job.FreezeFiles) - Offset + if countCopy > Limit { + countCopy = Limit + } + Result = job.FreezeFiles[Offset : Offset+countCopy] + Limit -= countCopy + Offset = 0 + } else { + Offset -= len(job.FreezeFiles) + } + + if Limit == 0 || Offset >= len(job.Files) { // offset wants to skip entire queue? + return Result + } + + // check if a sort is required before using this queue + if job.requireSort { + job.requireSort = false + + job.Files = SortItems(job.Files, job.filtersRuntime.Sort) + } + + countCopy := len(job.Files) - Offset + if countCopy > Limit { + countCopy = Limit + } + + // copy the results + Result = append(Result, job.Files[Offset:Offset+countCopy]...) + + return Result +} + +// RuntimeFilter allows to apply filters at runtime to search jobs that already started. +// To remove the filters, call this function without the filters set. Sort 0 = none, file type and format -1 = not used, dates 0 = not used. +func (job *SearchJob) RuntimeFilter(Sort, FileType, FileFormat int, DateFrom, DateTo time.Time) { + job.ResultSync.Lock() + defer job.ResultSync.Unlock() + + // FreezeFiles and current offset is reset + job.FreezeFiles = nil + job.currentOffset = 0 + + job.filtersRuntime.Sort = Sort + job.filtersRuntime.FileType = FileType + job.filtersRuntime.FileFormat = FileFormat + + if !DateFrom.IsZero() && !DateTo.IsZero() { + job.filtersRuntime.DateFrom = DateFrom + job.filtersRuntime.DateTo = DateTo + job.filtersRuntime.IsDates = true + } else { + job.filtersRuntime.DateFrom = time.Time{} + job.filtersRuntime.DateTo = time.Time{} + job.filtersRuntime.IsDates = false + } + + // files remain in AllFiles, but Files needs to be filtered based on the new filter + job.Files = []*apiFile{} + job.requireSort = false // Sorting is done immediately below + + // set Files based on AllFiles with the filter + for m := range job.AllFiles { + // only append if filter is matching + if job.isFileFiltered(job.AllFiles[m]) { + job.Files = append(job.Files, job.AllFiles[m]) + } + + // sort, if a sort order is defined + if job.filtersRuntime.Sort > 0 { + job.Files = SortItems(job.Files, job.filtersRuntime.Sort) + } + } +} + +// 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 + } + + if job.filtersRuntime.FileFormat >= 0 && file.Format != uint16(job.filtersRuntime.FileFormat) { + 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. + if job.filtersRuntime.IsDates && (file.Date.IsZero() || file.Date.Before(job.filtersRuntime.DateFrom) || file.Date.After(job.filtersRuntime.DateTo)) { + return false + } + + return true +} + +// SortItems sorts a list of files. It returns a sorted list. 0 = no sorting, 1 = Relevance ASC, 2 = Relevance DESC, 3 = Date ASC, 4 = Date DESC, 5 = Name ASC, 6 = Name DESC +func SortItems(files []*apiFile, Sort int) (sorted []*apiFile) { + if Sort == 0 { + return files + } + + // sort! + switch Sort { + case 1: // Relevance Score ASC + sort.SliceStable(files, func(i, j int) bool { return files[i].Date.Before(files[j].Date) }) // first as date for secondary sorting + //sort.SliceStable(files, func(i, j int) bool { return files[i].Score < files[j].Score }) // TODO + case 2: // Relevance Score DESC + sort.SliceStable(files, func(i, j int) bool { return files[j].Date.Before(files[i].Date) }) // first as date for secondary sorting + //sort.SliceStable(files, func(i, j int) bool { return files[i].Score > files[j].Score }) // TODO + case 3: // Date ASC + sort.SliceStable(files, func(i, j int) bool { return files[i].Date.Before(files[j].Date) }) + case 4: // Date DESC + sort.SliceStable(files, func(i, j int) bool { return files[j].Date.Before(files[i].Date) }) + } + + return files +} + +// IsSearchResults checks if search results may be expected (either files are in queue or a search is running) +func (job *SearchJob) IsSearchResults() bool { + // check for any available results. Do not use any lock here as this is read only. + return len(job.Files) > 0 || !job.IsTerminated() +} + +// 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 { + if id == job.AllFiles[m].ID { + return true + } + } + + return false +} + +// ---- job list management ---- +var ( + allJobs map[uuid.UUID]*SearchJob = make(map[uuid.UUID]*SearchJob) + allJobsMutex sync.RWMutex +) + +// Remove removes the structure from the list. Terminate should be called before. Unless the search is manually removed, it stays forever in the list. +func (job *SearchJob) Remove() { + allJobsMutex.Lock() + delete(allJobs, job.id) // delete is safe to call multiple times, so auto-removal and manual one are fine and need no syncing + allJobsMutex.Unlock() +} + +// RemoveDefer removes the search job after a given time after all searches are terminated. This can be used for automated time delayed removal. Do not create additional search clients after deferal removing. +func (job *SearchJob) RemoveDefer(Duration time.Duration) { + go func() { + // for _, client := range job.clients { + // <-client.TerminateSignal + // } + + <-time.After(Duration) + job.Remove() + }() +} + +// JobLookup looks up a job. Returns nil if not found. +func JobLookup(id uuid.UUID) (job *SearchJob) { + allJobsMutex.RLock() + job = allJobs[id] + allJobsMutex.RUnlock() + + return job +} + +// IsTerminated checks if all searches are finished. The job itself does not record a termination signal. +func (job *SearchJob) IsTerminated() bool { + job.clientsMutex.Lock() + defer job.clientsMutex.Unlock() + + // for n := range job.clients { + // if !job.clients[n].IsTerminated { + // return false + // } + // } + + return false +} + +// Terminate terminates all searches +func (job *SearchJob) Terminate() { + job.clientsMutex.Lock() + defer job.clientsMutex.Unlock() + + // for n := range job.clients { + // if !job.clients[n].IsTerminated { + // job.clients[n].Terminate(true) + // } + // } +} + +// WaitTerminate waits until all search clients are terminated. Do not create additional search clients after calling this function. +func (job *SearchJob) WaitTerminate() { + //for _, client := range job.clients { + // <-client.TerminateSignal + //} +} + +// ---- statistics ---- + +// ---- actual search & retrieving results ---- + +// appendSearchClient appends a search client to a job + +func (job *SearchJob) SearchAway() { + job.clientsMutex.Lock() + defer job.clientsMutex.Unlock() + + // TODO +} + +//func (job *SearchJob) appendSearchClient(search *dht.SearchClient) { +//} + +//func (job *SearchJob) receiveClientResults(client *dht.SearchClient) { +//} diff --git a/webapi/Search Temp.go b/webapi/Search Temp.go index c5c82fe..8d469b5 100644 --- a/webapi/Search Temp.go +++ b/webapi/Search Temp.go @@ -12,7 +12,6 @@ import ( "encoding/hex" "fmt" "math/rand" - "sync" "time" "github.com/PeernetOfficial/core" @@ -20,11 +19,10 @@ import ( ) func dispatchSearch(input SearchRequest) (job *SearchJob) { - job = &SearchJob{id: uuid.New(), timeout: time.Duration(input.Timeout) * time.Second, maxResult: input.MaxResults, sortOrder: input.Sort, fileType: -1, fileFormat: -1} + Timeout, FileType, FileFormat, DateFrom, DateTo := input.Parse() - allJobsMutex.Lock() - allJobs[job.id] = job - allJobsMutex.Unlock() + // create the search job + job = CreateSearchJob(Timeout, input.MaxResults, input.Sort, FileType, FileFormat, DateFrom, DateTo) // Create test data // * Between 0-100 results @@ -39,117 +37,24 @@ func dispatchSearch(input SearchRequest) (job *SearchJob) { job.ResultSync.Lock() for n := 0; n < countResults; n++ { - newFile := createTestResult(-1) - job.filesCurrent = append(job.filesCurrent, &newFile) + newFile := blockRecordFileToAPI(createTestResult(-1)) + + // TODO: Move to channel! + job.Files = append(job.Files, &newFile) + job.AllFiles = append(job.AllFiles, &newFile) + job.requireSort = true } job.ResultSync.Unlock() + job.Terminate() }(job) - job.RemoveDefer(job.timeout + time.Second*20) + job.RemoveDefer(job.timeout + time.Minute*10) return job } -// ---- job list management ---- - -// SearchJob is a collection of search jobs -type SearchJob struct { - // input settings - id uuid.UUID // The job id - timeout time.Duration // timeout set for all searches - maxResult int // max results user-facing. - sortOrder int // 0 = No sorting, 1 = Relevance ASC, 2 = Relevance DESC, 3 = Date ASC, 4 = Date DESC - - // additional optional filters - isDates bool // whether the from/to dates are valid, both are required. - dateFrom time.Time // optional date from - dateTo time.Time // optional date to - 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. - - // runtime data - //clients []*SearchClient // all search clients - //clientsMutex sync.Mutex // mutex for manipulating client list - - filesCurrent []*core.BlockRecordFile // Current result list of files, not yet fetched by caller. - FilesAll []*core.BlockRecordFile // List of all files. This list only gets expanded. - ResultSync sync.Mutex // ResultSync ensures unique access to the file results -} - -// job list management -var ( - allJobs map[uuid.UUID]*SearchJob = make(map[uuid.UUID]*SearchJob) - allJobsMutex sync.RWMutex -) - -// Remove removes the structure from the list. Terminate should be called before. Unless the search is manually removed, it stays forever in the list. -func (job *SearchJob) Remove() { - allJobsMutex.Lock() - delete(allJobs, job.id) // delete is safe to call multiple times, so auto-removal and manual one are fine and need no syncing - allJobsMutex.Unlock() -} - -// RemoveDefer removes the search job after a given time after all searches are terminated. This can be used for automated time delayed removal. Do not create additional search clients after deferal removing. -func (job *SearchJob) RemoveDefer(Duration time.Duration) { - go func() { - // for _, client := range job.clients { - // <-client.TerminateSignal - // } - - <-time.After(Duration) - job.Remove() - }() -} - -// JobLookup looks up a job. Returns nil if not found. -func JobLookup(id uuid.UUID) (job *SearchJob) { - allJobsMutex.RLock() - job = allJobs[id] - allJobsMutex.RUnlock() - - return job -} - -// Terminate terminates all searches -func (job *SearchJob) Terminate() { - //job.clientsMutex.Lock() - //defer job.clientsMutex.Unlock() - - // for n := range job.clients { - // if !job.clients[n].IsTerminated { - // job.clients[n].Terminate(true) - // } - // } -} - -// ReturnResult returns the selected results. -func (job *SearchJob) ReturnResult(Limit int) (Result []*core.BlockRecordFile) { - if Limit == 0 { - return Result - } - - job.ResultSync.Lock() - defer job.ResultSync.Unlock() - - if len(job.filesCurrent) == 0 { - return Result - } else if Limit > len(job.filesCurrent) { - Limit = len(job.filesCurrent) - } - - Result = job.filesCurrent[:Limit] - job.filesCurrent = job.filesCurrent[Limit:] - - return Result -} - -// ReturnNext returns the next results. Call must be serialized. -func (job *SearchJob) ReturnNext(Limit int) (Result []*core.BlockRecordFile) { - return job.ReturnResult(Limit) -} - // createTestResult creates a test file. fileType = -1 for any. func createTestResult(fileType int) (file core.BlockRecordFile) { randomData := make([]byte, 10*1024) diff --git a/webapi/Search.go b/webapi/Search.go index abbdffd..c04162d 100644 --- a/webapi/Search.go +++ b/webapi/Search.go @@ -18,24 +18,35 @@ package webapi import ( "net/http" "strconv" + "time" "github.com/google/uuid" ) -// SearchRequest is the information from the end-user for the search. +// SearchRequest is the information from the end-user for the search. Filters and sort order may be applied when starting the search, or at runtime when getting the results. type SearchRequest struct { Term string `json:"term"` // Search term. Timeout int `json:"timeout"` // Timeout in seconds. 0 means default. This is the entire time the search may take. Found results are still available after this timeout. MaxResults int `json:"maxresults"` // Total number of max results. 0 means default. - DateFrom string `json:"datefrom"` // Date from, both from/to are required if set. - DateTo string `json:"dateto"` // Date to, both from/to are required if set. - Sort int `json:"sort"` // Sort order: 0 = No sorting, 1 = Relevance ASC, 2 = Relevance DESC (this should be default), 3 = Date ASC, 4 = Date DESC + DateFrom string `json:"datefrom"` // Date from, both from/to are required if set. Format "2006-01-02 15:04:05". + DateTo string `json:"dateto"` // Date to, both from/to are required if set. Format "2006-01-02 15:04:05". + Sort int `json:"sort"` // See SortX. 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. - TypeFilter int `json:"typefilter"` // 0 = No filters used, 1 = Use file type filter, 2 = Use file format filter. - FileType int `json:"filetype"` // File type such as binary, text document etc. See core.TypeX. - FileFormat int `json:"fileformat"` // File format such as PDF, Word, Ebook, etc. See core.FormatX. + 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. } +// Sort orders +const ( + SortNone = 0 // No sorting. Results are returned as they come in. + SortRelevanceAsc = 1 // Least relevant results first. + SortRelevanceDec = 2 // Most relevant results first. + SortDateAsc = 3 // Oldest first. + SortDateDesc = 4 // Newest first. + SortNameAsc = 5 // File name ascending. The folder name is not used for sorting. + SortNameDesc = 6 // File name descending. The folder name is not used for sorting. +) + // 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. @@ -44,12 +55,15 @@ type SearchRequestResponse struct { // 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 - Files []apiBlockRecordFile `json:"files"` // List of files found + 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 } +const apiDateFormat = "2006-01-02 15:04:05" + /* apiSearch submits a search request + Request: POST /search with JSON SearchRequest Result: 200 on success with JSON SearchRequestResponse 400 on invalid JSON @@ -83,7 +97,15 @@ func apiSearch(w http.ResponseWriter, r *http.Request) { /* apiSearchResult returns results. The default limit is 100. +If reset is set, all results will be filtered and sorted according to the settings. This means that the new first result will be returned again and internal result offset is set to 0. + Request: GET /search/result?id=[UUID]&limit=[max records] +Optional parameters: + &reset=[0|1] to reset the filters or sort orders with any of the below parameters (all required): + &filetype=[File Type] + &fileformat=[File Format] + &from=[Date From]&to=[Date To] + &sort=[sort order] Result: 200 with JSON structure SearchResult. Check the field status. */ func apiSearchResult(w http.ResponseWriter, r *http.Request) { @@ -98,6 +120,14 @@ func apiSearchResult(w http.ResponseWriter, r *http.Request) { limit = 100 } + // filters and sort parameter + filterReset, _ := strconv.ParseBool(r.Form.Get("reset")) + fileType, _ := strconv.Atoi(r.Form.Get("filetype")) + fileFormat, _ := strconv.Atoi(r.Form.Get("fileformat")) + dateFrom, _ := time.Parse(apiDateFormat, r.Form.Get("from")) + dateTo, _ := time.Parse(apiDateFormat, r.Form.Get("to")) + sort, _ := strconv.Atoi(r.Form.Get("sort")) + // find the job ID job := JobLookup(jobID) if job == nil { @@ -105,21 +135,27 @@ func apiSearchResult(w http.ResponseWriter, r *http.Request) { return } + // apply runtime filters, if any + if filterReset { + job.RuntimeFilter(sort, fileType, fileFormat, dateFrom, dateTo) + } + // query all results resultFiles := job.ReturnNext(limit) var result SearchResult - result.Files = []apiBlockRecordFile{} + result.Files = []apiFile{} // loop over results for n := range resultFiles { - result.Files = append(result.Files, blockRecordFileToAPI(*resultFiles[n])) + result.Files = append(result.Files, *resultFiles[n]) } if len(result.Files) == 0 { result.Status = 3 // No results yet available keep trying } + //if !job.IsSearchResults() { // if no fresh results to be expected result.Status = 1 // No more results to expect EncodeJSON(w, r, result) @@ -127,6 +163,7 @@ func apiSearchResult(w http.ResponseWriter, r *http.Request) { /* apiSearchResultStream runs a websocket to return results + Request: GET /search/result/ws?id=[UUID]&limit=[optional max records] Result: If successful, upgrades to a web-socket and sends JSON structure SearchResult messages Limit is optional. Not used if ommitted or 0. @@ -161,7 +198,7 @@ func apiSearchResultStream(w http.ResponseWriter, r *http.Request) { // query all results var result SearchResult - result.Files = []apiBlockRecordFile{} + result.Files = []apiFile{} // loop over the results @@ -186,6 +223,7 @@ func apiSearchResultStream(w http.ResponseWriter, r *http.Request) { /* apiSearchTerminate terminates a search + Request: GET /search/terminate?id=[UUID] Response: 204 Empty 400 Invalid input @@ -235,7 +273,7 @@ func apiExplore(w http.ResponseWriter, r *http.Request) { resultFiles := queryRecentShared(fileType, limit) var result SearchResult - result.Files = []apiBlockRecordFile{} + result.Files = []apiFile{} // loop over results for n := range resultFiles { @@ -250,3 +288,19 @@ func apiExplore(w http.ResponseWriter, r *http.Request) { EncodeJSON(w, r, result) } + +func (input *SearchRequest) Parse() (Timeout time.Duration, FileType, FileFormat int, DateFrom, DateTo time.Time) { + if input.Timeout == 0 { + Timeout = time.Second * 20 // default timeout: 20 seconds + } else { + Timeout = time.Duration(input.Timeout) * time.Second + } + + FileType = input.FileType + FileFormat = input.FileFormat + + DateFrom, _ = time.Parse(apiDateFormat, input.DateFrom) + DateTo, _ = time.Parse(apiDateFormat, input.DateTo) + + return +} diff --git a/webapi/readme.md b/webapi/readme.md index eb60ed8..da9d090 100644 --- a/webapi/readme.md +++ b/webapi/readme.md @@ -70,12 +70,12 @@ Response: 200 with JSON structure apiResponseStatus ```go type apiResponseStatus struct { - 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. - 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 into the network. + 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. + 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 into the network. } ``` @@ -92,8 +92,8 @@ The peer and node IDs are encoded as hex encoded strings. ```go 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. + 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. } ``` @@ -103,11 +103,11 @@ Common status codes returned by various endpoints: ```go const ( - BlockchainStatusOK = 0 // No problems in the blockchain detected. - BlockchainStatusBlockNotFound = 1 // Missing block in the blockchain. - BlockchainStatusCorruptBlock = 2 // Error block encoding - BlockchainStatusCorruptBlockRecord = 3 // Error block record encoding - BlockchainStatusDataNotFound = 4 // Requested data not available in the blockchain + BlockchainStatusOK = 0 // No problems in the blockchain detected. + BlockchainStatusBlockNotFound = 1 // Missing block in the blockchain. + BlockchainStatusCorruptBlock = 2 // Error block encoding + BlockchainStatusCorruptBlockRecord = 3 // Error block record encoding + BlockchainStatusDataNotFound = 4 // Requested data not available in the blockchain ) ``` @@ -122,9 +122,9 @@ Response: 200 with JSON structure apiBlockchainHeader ```go type apiBlockchainHeader struct { - PeerID string `json:"peerid"` // Peer ID hex encoded. - Version uint64 `json:"version"` // Current version number of the blockchain. - Height uint64 `json:"height"` // Height of the blockchain (number of blocks). If 0, no data exists. + PeerID string `json:"peerid"` // Peer ID hex encoded. + Version uint64 `json:"version"` // Current version number of the blockchain. + Height uint64 `json:"height"` // Height of the blockchain (number of blocks). If 0, no data exists. } ``` @@ -140,18 +140,18 @@ Response: 200 with JSON structure apiBlockchainBlockStatus ```go type apiBlockRecordRaw struct { - Type uint8 `json:"type"` // Record Type. See core.RecordTypeX. - Data []byte `json:"data"` // Data according to the type. + Type uint8 `json:"type"` // Record Type. See core.RecordTypeX. + Data []byte `json:"data"` // Data according to the type. } type apiBlockchainBlockRaw struct { - Records []apiBlockRecordRaw `json:"records"` // Block records in encoded raw format. + Records []apiBlockRecordRaw `json:"records"` // Block records in encoded raw format. } type apiBlockchainBlockStatus struct { - Status int `json:"status"` // Status: 0 = Success, 1 = Error invalid data - Height uint64 `json:"height"` // Height of the blockchain (number of blocks). - Version uint64 `json:"version"` // Version of the blockchain. + Status int `json:"status"` // Status: 0 = Success, 1 = Error invalid data + Height uint64 `json:"height"` // Height of the blockchain (number of blocks). + Version uint64 `json:"version"` // Version of the blockchain. } ``` @@ -166,47 +166,47 @@ Response: 200 with JSON structure apiBlockchainBlock ```go type apiBlockchainBlock struct { - Status int `json:"status"` // Status: 0 = Success, 1 = Error block not found, 2 = Error block encoding (indicates that the blockchain is corrupt) - PeerID string `json:"peerid"` // Peer ID hex encoded. - LastBlockHash []byte `json:"lastblockhash"` // Hash of the last block. Blake3. - BlockchainVersion uint64 `json:"blockchainversion"` // Blockchain version - Number uint64 `json:"blocknumber"` // Block number - RecordsRaw []apiBlockRecordRaw `json:"recordsraw"` // Records raw. Successfully decoded records are parsed into the below fields. - RecordsDecoded []interface{} `json:"recordsdecoded"` // Records decoded. The encoding for each record depends on its type. + Status int `json:"status"` // Status: 0 = Success, 1 = Error block not found, 2 = Error block encoding (indicates that the blockchain is corrupt) + PeerID string `json:"peerid"` // Peer ID hex encoded. + LastBlockHash []byte `json:"lastblockhash"` // Hash of the last block. Blake3. + BlockchainVersion uint64 `json:"blockchainversion"` // Blockchain version + Number uint64 `json:"blocknumber"` // Block number + RecordsRaw []apiBlockRecordRaw `json:"recordsraw"` // Records raw. Successfully decoded records are parsed into the below fields. + RecordsDecoded []interface{} `json:"recordsdecoded"` // Records decoded. The encoding for each record depends on its type. } ``` The array `RecordsDecoded` will contain any present record of the following: * Profile records, see `apiBlockRecordProfile` -* File records, see `apiBlockRecordFile` +* File records, see `apiFile` ## File Functions These functions allow adding, deleting, and listing files stored on the users blockchain. Only metadata is actually stored on the blockchain. To download a remote file both the file hash and the node ID are required. The node ID specifies the owner of the file. ```go -type apiBlockRecordFile struct { - ID uuid.UUID `json:"id"` // Unique ID. - 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 - Folder string `json:"folder"` // Folder, optional - 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 - Metadata []apiFileMetadata `json:"metadata"` // Additional metadata. +type apiFile struct { + ID uuid.UUID `json:"id"` // Unique ID. + 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 + Folder string `json:"folder"` // Folder, optional + 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 + Metadata []apiFileMetadata `json:"metadata"` // Additional metadata. } type apiFileMetadata struct { - Type uint16 `json:"type"` // See core.TagX constants. - Name string `json:"name"` // User friendly name of the metadata type. Use the Type fields to identify the metadata as this name may change. - // Depending on the exact type, one of the below fields is used for proper encoding: - Text string `json:"text"` // Text value. UTF-8 encoding. - Blob []byte `json:"blob"` // Binary data - Date time.Time `json:"date"` // Date - Number uint64 `json:"number"` // Number + Type uint16 `json:"type"` // See core.TagX constants. + Name string `json:"name"` // User friendly name of the metadata type. Use the Type fields to identify the metadata as this name may change. + // Depending on the exact type, one of the below fields is used for proper encoding: + Text string `json:"text"` // Text value. UTF-8 encoding. + Blob []byte `json:"blob"` // Binary data + Date time.Time `json:"date"` // Date + Number uint64 `json:"number"` // Number } ``` @@ -235,7 +235,8 @@ Response: 200 with JSON structure apiBlockchainBlockStatus ```go type apiBlockAddFiles struct { - Files []apiBlockRecordFile `json:"files"` + 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. } ``` @@ -384,15 +385,15 @@ Response: 200 with JSON structure apiProfileData ```go 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 core.BlockchainStatusX. + 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 core.BlockchainStatusX. } type apiBlockRecordProfile struct { - Type uint16 `json:"type"` // See ProfileX constants. - // Depending on the exact type, one of the below fields is used for proper encoding: - Text string `json:"text"` // Text value. UTF-8 encoding. - Blob []byte `json:"blob"` // Binary data + Type uint16 `json:"type"` // See ProfileX constants. + // Depending on the exact type, one of the below fields is used for proper encoding: + Text string `json:"text"` // Text value. UTF-8 encoding. + Blob []byte `json:"blob"` // Binary data } ``` @@ -492,7 +493,27 @@ Example POST request to `http://127.0.0.1:112/profile/delete` (deleting the prof The search API provides a high-level function to search for files in Peernet. Searching is always asynchronous. `/search` returns an UUID which is used to loop over `/search/result` until the search is terminated. -The current implementation of the underlying search algorithm only searches file names. Optional filters are supported. +The current implementation of the underlying search algorithm only searches file names. + +Filters and sort order may be applied when starting the search at `/search`, or at runtime when returning the results at `/search/result`. + +These are the available sort options: + +| Sort | Constant | Info | +|------|----------------|-------------------------------| +| 0 | SortNone | No sorting. Results are returned as they come in. | +| 1 | SortRelevanceAsc | Least relevant results first. | +| 2 | SortRelevanceDec | Most relevant results first. | +| 3 | SortDateAsc | Oldest first. | +| 4 | SortDateDesc | Newest first. | +| 5 | SortNameAsc | File name ascending. The folder name is not used for sorting. | +| 6 | SortNameDesc | File name descending. The folder name is not used for sorting. | + +The following filters are supported: + +* Filter by date from and to. Both dates are required. The inclusion check for the 'from date' is >= and 'to date' <. +* File type such as binary, text document etc. See core.TypeX. +* File format (which is more granular) such as PDF, Word, Ebook, etc. See core.FormatX. ### Submitting a Search Request @@ -503,21 +524,20 @@ Response: 200 on success with JSON SearchRequestResponse ```go type SearchRequest struct { - Term string `json:"term"` // Search term. - Timeout time.Duration `json:"timeout"` // Timeout in seconds. 0 means default. This is the entire time the search may take. Found results are still available after this timeout. - MaxResults int `json:"maxresults"` // Total number of max results. 0 means default. - DateFrom string `json:"datefrom"` // Date from, both from/to are required if set. - DateTo string `json:"dateto"` // Date to, both from/to are required if set. - Sort int `json:"sort"` // Sort order: 0 = No sorting, 1 = Relevance ASC, 2 = Relevance DESC (this should be default), 3 = Date ASC, 4 = Date DESC - 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. - TypeFilter int `json:"typefilter"` // 0 = No filters used, 1 = Use file type filter, 2 = Use file format filter. - FileType int `json:"filetype"` // File type such as binary, text document etc. See core.TypeX. - FileFormat int `json:"fileformat"` // File format such as PDF, Word, Ebook, etc. See core.FormatX. + Term string `json:"term"` // Search term. + Timeout int `json:"timeout"` // Timeout in seconds. 0 means default. This is the entire time the search may take. Found results are still available after this timeout. + MaxResults int `json:"maxresults"` // Total number of max results. 0 means default. + DateFrom string `json:"datefrom"` // Date from, both from/to are required if set. Format "2006-01-02 15:04:05". + DateTo string `json:"dateto"` // Date to, both from/to are required if set. Format "2006-01-02 15:04:05". + Sort int `json:"sort"` // See SortX. + 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. } 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 + 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 } ``` @@ -542,17 +562,24 @@ Example response: ### Returning Search Results -This function returns search results. +This function returns search results. The default limit is 100. +If reset is set, all results will be filtered and sorted according to the settings. This means that the new first result will be returned again and internal result offset is set to 0. ``` Request: GET /search/result?id=[UUID]&limit=[max records] + Optional parameters: + &reset=[0|1] to reset the filters or sort orders with any of the below parameters (all required): + &filetype=[File Type] + &fileformat=[File Format] + &from=[Date From]&to=[Date To] + &sort=[sort order] Result: 200 with JSON structure SearchResult. Check the field status. ``` ```go 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 - Files []apiBlockRecordFile `json:"files"` // List of files found + 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 } ``` @@ -610,7 +637,7 @@ Downloads can have these status types: |------|----------------|-------------------------------| | 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. | +| 2 | DownloadActive | Active downloading. 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%. | @@ -622,13 +649,13 @@ The API response codes for download functions are: | 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. | +| 3 | DownloadResponseActionInvalid | Error: Invalid action. Pausing a non-active download, resuming a non-paused download, or canceling already canceled or finished download. | | 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 node ID identifies the 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). The hash and node must be hex-encoded. ``` Request: GET /download/start?path=[target path on disk]&hash=[file hash to download]&node=[node ID] @@ -637,21 +664,23 @@ Result: 200 with JSON structure apiResponseDownloadStatus ```go type apiResponseDownloadStatus struct { - APIStatus int `json:"apistatus"` // Status of the API call. See DownloadResponseX. - ID uuid.UUID `json:"id"` // Download ID. This can be used to query the latest status and take actions. - DownloadStatus int `json:"downloadstatus"` // Status of the download. See DownloadX. - File 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. + APIStatus int `json:"apistatus"` // Status of the API call. See DownloadResponseX. + ID uuid.UUID `json:"id"` // Download ID. This can be used to query the latest status and take actions. + DownloadStatus int `json:"downloadstatus"` // Status of the download. See DownloadX. + File apiFile `json:"file"` // File information. Only available for status >= DownloadWaitSwarm. + Progress struct { + TotalSize uint64 `json:"totalsize"` // Total size in bytes. + DownloadedSize uint64 `json:"downloadedsize"` // Count of bytes download so far. + Percentage float64 `json:"percentage"` // Percentage downloaded. Rounded to 2 decimal points. Between 0.00 and 100.00. + } `json:"progress"` // Progress of the download. Only valid for status >= DownloadWaitSwarm. + Swarm struct { + CountPeers uint64 `json:"countpeers"` // Count of peers participating in the swarm. + } `json:"swarm"` // Information about the swarm. Only valid for status >= DownloadActive. } ``` +Example request: `http://127.0.0.1:112/download/start?path=test.bin&hash=cde13a55f41e387480391c47238acfe9c0136dd56bf365b01416aec03eec7dc4&node=5a0f712822ddc49633d27df6009d3efa27f19cb371319837f04160bdbda38544` + Example response (only apistatus, id, and downloadstatus are used): ```json @@ -743,9 +772,9 @@ Result: 200 with JSON structure apiResponseFileFormat ```go type apiResponseFileFormat struct { - Status int `json:"status"` // Status: 0 = Success, 1 = Error reading file - FileType uint16 `json:"filetype"` // File Type. - FileFormat uint16 `json:"fileformat"` // File Format. + Status int `json:"status"` // Status: 0 = Success, 1 = Error reading file + FileType uint16 `json:"filetype"` // File Type. + FileFormat uint16 `json:"fileformat"` // File Format. } ```