diff --git a/warehouse/Store.go b/warehouse/Store.go index 7fac4af..3f55106 100644 --- a/warehouse/Store.go +++ b/warehouse/Store.go @@ -29,6 +29,8 @@ const ( StatusErrorDeleteFile = 10 // Error deleting file. StatusErrorReadFile = 11 // Error reading file. StatusErrorSeekFile = 12 // Error seeking to position in file. + StatusErrorTargetExists = 13 // Target file already exists. + StatusErrorCreateTarget = 14 // Error creating target file. ) // CreateFile creates a new file in the warehouse @@ -115,6 +117,7 @@ func (wh *Warehouse) CreateFileFromPath(file string) (hash []byte, status int, e // ReadFile reads a file from the warehouse and outputs it to the writer // Offset is the position in the file to start reading. Limit (0 = not used) defines how many bytes to read starting at the offset. +// Return status codes: StatusInvalidHash, StatusFileNotFound, StatusErrorOpenFile, StatusErrorSeekFile, StatusErrorReadFile, StatusOK func (wh *Warehouse) ReadFile(hash []byte, offset, limit int64, writer io.Writer) (status int, bytesRead int64, err error) { path, _, status, err := wh.FileExists(hash) if status != StatusOK { // file does not exist or invalid hash @@ -178,7 +181,7 @@ func (wh *Warehouse) DeleteFile(hash []byte) (status int, err error) { return StatusOK, nil } -// FileExists checks if the file exists +// FileExists checks if the file exists. It returns StatusInvalidHash, StatusFileNotFound, or StatusOK. func (wh *Warehouse) FileExists(hash []byte) (path string, fileInfo os.FileInfo, status int, err error) { hashA, err := ValidateHash(hash) if err != nil { @@ -204,3 +207,22 @@ func (wh *Warehouse) DeleteWarehouse() (err error) { return true }) } + +// ReadFileToDisk reads a file from the warehouse and outputs it to the target file. The function fails with StatusErrorTargetExists if the target file already exists. +// Offset is the position in the file to start reading. Limit (0 = not used) defines how many bytes to read starting at the offset. +// Return status codes: StatusInvalidHash, StatusFileNotFound, StatusErrorTargetExists, StatusErrorCreateTarget, StatusErrorOpenFile, StatusErrorSeekFile, StatusErrorReadFile, StatusOK +func (wh *Warehouse) ReadFileToDisk(hash []byte, offset, limit int64, fileTarget string) (status int, bytesRead int64, err error) { + // check if the target file already exist + if _, err := os.Stat(fileTarget); err == nil { + return StatusErrorTargetExists, 0, nil + } + + // create the target file + fileT, err := os.OpenFile(fileTarget, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return StatusErrorCreateTarget, 0, err + } + defer fileT.Close() + + return wh.ReadFile(hash, offset, limit, fileT) +} diff --git a/webapi/API.go b/webapi/API.go index 7b92b6c..2578625 100644 --- a/webapi/API.go +++ b/webapi/API.go @@ -74,6 +74,7 @@ func Start(ListenAddresses []string, UseSSL bool, CertificateFile, CertificateKe Router.HandleFunc("/warehouse/create", apiWarehouseCreateFile).Methods("POST") Router.HandleFunc("/warehouse/create/path", apiWarehouseCreateFilePath).Methods("GET") Router.HandleFunc("/warehouse/read", apiWarehouseReadFile).Methods("GET") + Router.HandleFunc("/warehouse/read/path", apiWarehouseReadFilePath).Methods("GET") Router.HandleFunc("/warehouse/delete", apiWarehouseDeleteFile).Methods("GET") Router.HandleFunc("/file/read", apiFileRead).Methods("GET") Router.HandleFunc("/file/view", apiFileView).Methods("GET") diff --git a/webapi/Warehouse.go b/webapi/Warehouse.go index d50e0e6..defc5f6 100644 --- a/webapi/Warehouse.go +++ b/webapi/Warehouse.go @@ -30,7 +30,7 @@ func apiWarehouseCreateFile(w http.ResponseWriter, r *http.Request) { hash, status, err := core.UserWarehouse.CreateFile(r.Body) if err != nil { - core.Filters.LogError("warehouese.CreateFile", "status %d error: %v", status, err) + core.Filters.LogError("warehouse.CreateFile", "status %d error: %v", status, err) } EncodeJSON(w, r, WarehouseResult{Status: status, Hash: hash}) @@ -55,7 +55,7 @@ func apiWarehouseCreateFilePath(w http.ResponseWriter, r *http.Request) { hash, status, err := core.UserWarehouse.CreateFileFromPath(filePath) if err != nil { - core.Filters.LogError("warehouese.CreateFile", "status %d error: %v", status, err) + core.Filters.LogError("warehouse.CreateFile", "status %d error: %v", status, err) } EncodeJSON(w, r, WarehouseResult{Status: status, Hash: hash}) @@ -90,12 +90,12 @@ func apiWarehouseReadFile(w http.ResponseWriter, r *http.Request) { case warehouse.StatusInvalidHash, warehouse.StatusErrorOpenFile, warehouse.StatusErrorSeekFile: w.WriteHeader(http.StatusInternalServerError) return - // Cannot catch WarehouseStatusErrorReadFile since data may have been already returned. + // 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. } if err != nil { - core.Filters.LogError("warehouese.ReadFile", "status %d read %d error: %v", status, bytesRead, err) + core.Filters.LogError("warehouse.ReadFile", "status %d read %d error: %v", status, bytesRead, err) } } @@ -116,7 +116,36 @@ func apiWarehouseDeleteFile(w http.ResponseWriter, r *http.Request) { status, err := core.UserWarehouse.DeleteFile(hash) if err != nil { - core.Filters.LogError("warehouese.DeleteFile", "status %d error: %v", status, err) + core.Filters.LogError("warehouse.DeleteFile", "status %d error: %v", status, err) + } + + EncodeJSON(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. + +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 apiWarehouseReadFilePath(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + hash, valid1 := DecodeBlake3Hash(r.Form.Get("hash")) + if !valid1 { + http.Error(w, "", http.StatusBadRequest) + return + } + + targetFile := r.Form.Get("path") + offset, _ := strconv.Atoi(r.Form.Get("offset")) + limit, _ := strconv.Atoi(r.Form.Get("limit")) + + status, bytesRead, err := core.UserWarehouse.ReadFileToDisk(hash, int64(offset), int64(limit), targetFile) + + if err != nil { + core.Filters.LogError("warehouse.ReadFileToDisk", "status %d read %d error: %v", status, bytesRead, err) } EncodeJSON(w, r, WarehouseResult{Status: status, Hash: hash}) diff --git a/webapi/readme.md b/webapi/readme.md index 6e53c73..3c0d5a3 100644 --- a/webapi/readme.md +++ b/webapi/readme.md @@ -70,6 +70,7 @@ These are the functions provided by the API: /warehouse/create Create a file in the warehouse /warehouse/create/path Create a file in the warehouse via copy /warehouse/read Read a file in the warehouse +/warehouse/read/path Read a file in the warehouse to disk /warehouse/delete Delete a file in the warehouse ``` @@ -135,14 +136,14 @@ Result: 204 if the user choses not to delete the account Common status codes returned by various endpoints in the `blockchain` package: -| Status | Constant | Info | -|------|----------------|-------------------------------| -| 0 | StatusOK | Successful operation. | -| 1 | StatusBlockNotFound | Missing block in the blockchain. | -| 2 | StatusCorruptBlock | Error block encoding. | -| 3 | StatusCorruptBlockRecord | Error block record encoding. | -| 4 | StatusDataNotFound | Requested data not available in the blockchain. | -| 5 | StatusNotInWarehouse | File to be added to blockchain does not exist in the Warehouse. | +| Status | Constant | Info | +| ------ | ------------------------ | --------------------------------------------------------------- | +| 0 | StatusOK | Successful operation. | +| 1 | StatusBlockNotFound | Missing block in the blockchain. | +| 2 | StatusCorruptBlock | Error block encoding. | +| 3 | StatusCorruptBlockRecord | Error block record encoding. | +| 4 | StatusDataNotFound | Requested data not available in the blockchain. | +| 5 | StatusNotInWarehouse | File to be added to blockchain does not exist in the Warehouse. | ### Blockchain Header @@ -245,15 +246,15 @@ type apiFileMetadata struct { Below is the list of defined metadata types. Undefined types may be used by clients, but are always mapped into the `blob` field. Virtual tags are generated at runtime and are read-only. They cannot be stored on the blockchain. -| Type | Constant | Encoding | Virtual | Info | -|------|----------------|----------|---------|------------------------------------------------------------------------------------------------------------------------| -| 0 | TagName | Text | | Mapped into Name field. Name of file. | -| 1 | TagFolder | Text | | Mapped into Folder field. Folder name. | -| 2 | TagDescription | Text | | Mapped into Description field. Arbitrary description of the file. May contain hashtags. | -| 3 | TagDateShared | Date | x | Mapped into Date field. When the file was published on the blockchain. | -| 4 | TagDateCreated | Date | | Date when the file was originally created. | -| 5 | TagSharedByCount | Number | x | Count of peers that share the file. | -| 6 | TagSharedByGeoIP | Text/CSV | x | GeoIP data of peers that are sharing the file. CSV encoded with header "latitude,longitude". | +| Type | Constant | Encoding | Virtual | Info | +| ---- | ---------------- | -------- | ------- | -------------------------------------------------------------------------------------------- | +| 0 | TagName | Text | | Mapped into Name field. Name of file. | +| 1 | TagFolder | Text | | Mapped into Folder field. Folder name. | +| 2 | TagDescription | Text | | Mapped into Description field. Arbitrary description of the file. May contain hashtags. | +| 3 | TagDateShared | Date | x | Mapped into Date field. When the file was published on the blockchain. | +| 4 | TagDateCreated | Date | | Date when the file was originally created. | +| 5 | TagSharedByCount | Number | x | Count of peers that share the file. | +| 6 | TagSharedByGeoIP | Text/CSV | x | GeoIP data of peers that are sharing the file. CSV encoded with header "latitude,longitude". | ### Add File @@ -422,7 +423,7 @@ Note that all profile data is arbitrary and shall be considered untrusted and un Below is the list of well known profile information. Clients may define additional fields. The purpose of this defined list is to provide a common mapping across different client software. Undefined types are always mapped into the `blob` field. | Type | Constant | Encoding | Info | -|------|----------------|----------|-------------------------------| +| ---- | -------------- | -------- | ----------------------------- | | 0 | ProfileName | Text | Arbitrary username | | 1 | ProfileEmail | Text | Email address | | 2 | ProfileWebsite | Text | Website address | @@ -556,19 +557,19 @@ Filters and sort order may be applied when starting the search at `/search`, or 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. | -| 7 | SortSizeAsc | File size ascending. Smallest files first. | -| 8 | SortSizeDesc | File size descending. Largest files first. | -| 9 | SortSharedByCountAsc | Shared by count ascending. Files that are shared by the least count of peers first. | -| 10 | SortSharedByCountDesc | Shared by count descending. Files that are shared by the most count of peers first. | +| 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. | +| 7 | SortSizeAsc | File size ascending. Smallest files first. | +| 8 | SortSizeDesc | File size descending. Largest files first. | +| 9 | SortSharedByCountAsc | Shared by count ascending. Files that are shared by the least count of peers first. | +| 10 | SortSharedByCountDesc | Shared by count descending. Files that are shared by the most count of peers first. | The following filters are supported: @@ -759,24 +760,24 @@ Response: 204 Empty Downloads can have these status types: -| Status | Constant | Info | -|------|----------------|-------------------------------| -| 0 | DownloadWaitMetadata | Wait for file metadata. | -| 1 | DownloadWaitSwarm | Wait to join swarm. | -| 2 | DownloadActive | Active downloading. 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%. | +| Status | Constant | Info | +| ------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| 0 | DownloadWaitMetadata | Wait for file metadata. | +| 1 | DownloadWaitSwarm | Wait to join swarm. | +| 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%. | The API response codes for download functions are: -| Status | Constant | Info | -|------|----------------|-------------------------------| -| 0 | DownloadResponseSuccess | Success | -| 1 | DownloadResponseIDNotFound | Error: Download ID not found. | -| 2 | DownloadResponseFileInvalid | Error: Target file cannot be used. For example, permissions denied to create it. | -| 3 | DownloadResponseActionInvalid | Error: Invalid action. Pausing a non-active download, resuming a non-paused download, or canceling already canceled or finished download. | -| 4 | DownloadResponseFileWrite | Error writing file. | +| Status | Constant | Info | +| ------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | DownloadResponseSuccess | Success | +| 1 | DownloadResponseIDNotFound | Error: Download ID not found. | +| 2 | DownloadResponseFileInvalid | Error: Target file cannot be used. For example, permissions denied to create it. | +| 3 | DownloadResponseActionInvalid | Error: Invalid action. Pausing a non-active download, resuming a non-paused download, or canceling already canceled or finished download. | +| 4 | DownloadResponseFileWrite | Error writing file. | ### Start Download @@ -929,20 +930,21 @@ Note: The Warehouse does NOT store files downloaded from other users. It strictl Status codes: -| Status | Constant | Info | -|------|----------------|-------------------------------| -| 0 | StatusOK | Success | -| 1 | StatusErrorCreateTempFile | Error creating a temporary file. | -| 2 | StatusErrorWriteTempFile | Error writing temporary file. | -| 3 | StatusErrorCloseTempFile | Error closing temporary file. | -| 4 | StatusErrorRenameTempFile | Error renaming temporary file. | -| 5 | StatusErrorCreatePath | Error creating path for target file in warehouse. | -| 7 | StatusErrorOpenFile | Error opening file. | -| 8 | StatusInvalidHash | Invalid hash. | -| 9 | StatusFileNotFound | File not found. | -| 10 | StatusErrorDeleteFile | Error deleting file. | -| 11 | StatusErrorReadFile | Error reading file. | -| 12 | StatusErrorSeekFile | Error seeking to position in file. | +| Status | Constant | Info | +| ------ | ------------------------- | ------------------------------------------------- | +| 0 | StatusOK | Success | +| 1 | StatusErrorCreateTempFile | Error creating a temporary file. | +| 2 | StatusErrorWriteTempFile | Error writing temporary file. | +| 3 | StatusErrorCloseTempFile | Error closing temporary file. | +| 4 | StatusErrorRenameTempFile | Error renaming temporary file. | +| 5 | StatusErrorCreatePath | Error creating path for target file in warehouse. | +| 7 | StatusErrorOpenFile | Error opening file. | +| 8 | StatusInvalidHash | Invalid hash. | +| 9 | StatusFileNotFound | File not found. | +| 10 | StatusErrorDeleteFile | Error deleting file. | +| 11 | StatusErrorReadFile | Error reading file. | +| 12 | StatusErrorSeekFile | Error seeking to position in file. | +| 13 | StatusErrorTargetExists | Target file already exists. | ### Create File @@ -1012,6 +1014,19 @@ Response: 200 with the raw file data Example request: `http://127.0.0.1:112/warehouse/read?hash=dbf344f23e7820261329883ae26f64929a7c9977549001d28dc40c9202d7651e` +### Read File To Disk + +This 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] +Response: 200 with JSON structure WarehouseResult +``` + +Example request: `http://127.0.0.1:112/warehouse/read/path?hash=dbf344f23e7820261329883ae26f64929a7c9977549001d28dc40c9202d7651e&path=C%3A%5CTest%20File%202.bin` + ### Delete File This deletes a file in the warehouse. This is normally not needed, since `/blockchain/file/delete` will automatically delete files in the Warehouse if there are no active references.