Skip to content

Commit

Permalink
Add new async compression API.
Browse files Browse the repository at this point in the history
  • Loading branch information
retrixe committed Aug 27, 2023
1 parent 1bbfd2a commit 7b50ecc
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 9 deletions.
22 changes: 19 additions & 3 deletions API.md
Expand Up @@ -43,7 +43,8 @@ Currently, possible errors are not documented. This will be done in the future.
- [POST /server/{id}/folder?path=path](#post-serveridfolderpathpath)
- [DELETE /server/{id}/file?path=path](#delete-serveridfilepathpath)
- [PATCH /server/{id}/file](#patch-serveridfile)
- [POST /server/{id}/compress?path=path&compress=algorithm&archiveType=archiveType&basePath=path](#post-serveridcompresspathpathcompressalgorithmarchivetypearchivetypebasepathpath)
- [GET /server/{id}/compress?token=token](#get-serveridcompresstokentoken)
- [POST /server/{id}/compress?path=path&compress=algorithm&archiveType=archiveType&basePath=path&async=boolean](#post-serveridcompresspathpathcompressalgorithmarchivetypearchivetypebasepathpathasyncboolean)
- [POST /server/{id}/decompress?path=path](#post-serveriddecompresspathpath)

### GET /
Expand Down Expand Up @@ -453,18 +454,33 @@ Move or copy a file or folder in the working directory of the app.

---

### POST /server/{id}/compress?path=path&compress=algorithm&archiveType=archiveType&basePath=path
### GET /server/{id}/compress?token=token

Get the progress of an async compression request. Added in v1.2+.

**Request Query Parameters:**

- `token` - The token returned by [POST /server/{id}/compress?path=path&compress=algorithm&archiveType=archiveType&basePath=path&async=boolean](#post-serveridcompresspathpathcompressalgorithmarchivetypearchivetypebasepathpathasyncboolean) corresponding to your compression request.

**Response:**

HTTP 200 JSON body response `{"finished":true}` is returned on success. If the compression request is still in progress, the body will be `{"finished":false}`, and HTTP 500 errors will be returned if the compression request failed.

---

### POST /server/{id}/compress?path=path&compress=algorithm&archiveType=archiveType&basePath=path&async=boolean

Compress files/folders in the working directory of the app into a ZIP or TAR archive. Support for `tar(.gz/xz/zst)` archives was added in v1.2+.

⚠️ *Info:* The `POST /server/{id}/compress/v2` API is available as well, which is identical to this, but guaranteed to support `tar` archives and the `basePath` query parameter. The v2 endpoint can be used by API clients to ensure archives aren't accidentally created as ZIP files on older Octyne versions.
⚠️ *Info:* The `POST /server/{id}/compress/v2` API is available as well, which is identical to this, but guaranteed to support `tar` archives, the `basePath` and `async` query parameters. The v2 endpoint can be used by API clients to ensure archives aren't accidentally created as ZIP files on older Octyne versions.

**Request Query Parameters:**

- `path` - The location to create the archive at. This is relative to the server's root directory.
- `basePath` - Optional, default is `/`. The location containing all files in the request body. This is used as the archive's top-level directory, all file paths in the request body are calculated relative to `basePath`. Added in v1.2+.
- `archiveType` - Optional, default is `zip`. This specifies the archive type to use, currently, `zip` and `tar` are supported. Added in v1.2+.
- `compress` - Optional, default is `true`, possible values are `true`/`false`, and `gzip`/`zstd`/`xz` for `tar` archives. This specifies whether or not to compress files/folders in the archive. `true` corresponds to the default DEFLATE algorithm for `zip`, and GZIP for `tar`. ⚠️ *Warning:* This was broken in v1.0 (it accidentally used a header instead of query parameter), this was fixed in v1.1+.
- `async` - Optional, default is `false`. If `true`, the endpoint will return a token immediately instead of waiting for the archive to compress. This token can be used against [GET /server/{id}/compress?token=token](#get-serveridcompresstokentoken) to get the status of the compression request. The token expires 10 seconds after the compression request has been serviced. Added in v1.2+.

**Request Body:**

Expand Down
56 changes: 50 additions & 6 deletions endpoints_files.go
Expand Up @@ -5,6 +5,8 @@ import (
"archive/zip"
"bytes"
"compress/gzip"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand All @@ -14,8 +16,10 @@ import (
"path"
"path/filepath"
"strings"
"time"

"github.com/gorilla/mux"
"github.com/puzpuzpuz/xsync/v2"
"github.com/retrixe/octyne/system"
)

Expand Down Expand Up @@ -353,8 +357,10 @@ func (connector *Connector) registerFileRoutes() {
}
})

// POST /server/{id}/compress?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar&basePath=path
// POST /server/{id}/compress/v2?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar&basePath=path
compressionProgressMap := xsync.NewMapOf[string]()
// GET /server/{id}/compress?token=token
// POST /server/{id}/compress?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar&basePath=path&async=boolean
// POST /server/{id}/compress/v2?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar&basePath=path&async=boolean
compressionEndpoint := func(w http.ResponseWriter, r *http.Request) {
// Check with authenticator.
user := connector.Validate(w, r)
Expand All @@ -368,11 +374,28 @@ func (connector *Connector) registerFileRoutes() {
if !exists {
httpError(w, "This server does not exist!", http.StatusNotFound)
return
} else if r.Method == "GET" {
if r.URL.Query().Get("token") == "" {
httpError(w, "No token provided!", http.StatusBadRequest)
return
}
progress, valid := compressionProgressMap.Load(r.URL.Query().Get("token"))
if !valid {
httpError(w, "Invalid token!", http.StatusBadRequest)
} else if progress == "finished" {
writeJsonStringRes(w, "{\"finished\":true}")
} else if progress == "" {
writeJsonStringRes(w, "{\"finished\":false}")
} else {
httpError(w, progress, http.StatusInternalServerError)
}
return
} else if r.Method != "POST" {
httpError(w, "Only POST is allowed!", http.StatusMethodNotAllowed)
httpError(w, "Only GET and POST are allowed!", http.StatusMethodNotAllowed)
return
}
// Decode parameters.
async := r.URL.Query().Get("async") == "true"
basePath := r.URL.Query().Get("basePath")
archiveType := "zip"
compression := "true"
Expand Down Expand Up @@ -446,6 +469,13 @@ func (connector *Connector) registerFileRoutes() {
return
}
defer archiveFile.Close()
tokenBytes := make([]byte, 16)
rand.Read(tokenBytes) // Tolerate errors here, an error here is incredibly unlikely: skipcq GSC-G104
token := base64.StdEncoding.EncodeToString(tokenBytes)
if async {
compressionProgressMap.Store(token, "")
writeJsonStringRes(w, "{\"token\":\""+token+"\"}")
}
if archiveType == "zip" {
archive := zip.NewWriter(archiveFile)
defer archive.Close()
Expand All @@ -454,7 +484,11 @@ func (connector *Connector) registerFileRoutes() {
err := system.AddFileToZip(archive, joinPath(process.Directory, basePath), file, compression != "false")
if err != nil {
log.Println("An error occurred when adding "+file+" to "+archivePath, "("+process.Name+")", err)
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
if !async {
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
} else {
compressionProgressMap.Store(token, "Internal Server Error!")
}
return
}
}
Expand All @@ -476,15 +510,25 @@ func (connector *Connector) registerFileRoutes() {
err := system.AddFileToTar(archive, joinPath(process.Directory, basePath), file)
if err != nil {
log.Println("An error occurred when adding "+file+" to "+archivePath, "("+process.Name+")", err)
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
if !async {
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
} else {
compressionProgressMap.Store(token, "Internal Server Error!")
}
return
}
}
}
connector.Info("server.files.compress", "ip", GetIP(r), "user", user, "server", id,
"archive", clean(r.URL.Query().Get("path")), "archiveType", archiveType,
"compression", compression, "files", files)
writeJsonStringRes(w, "{\"success\":true}")
if async {
compressionProgressMap.Store(token, "finished")
<-time.After(10 * time.Second)
compressionProgressMap.Delete(token)
} else {
writeJsonStringRes(w, "{\"success\":true}")
}
}
connector.Router.HandleFunc("/server/{id}/compress", compressionEndpoint)
connector.Router.HandleFunc("/server/{id}/compress/v2", compressionEndpoint)
Expand Down

0 comments on commit 7b50ecc

Please sign in to comment.