Skip to content

Commit

Permalink
Add tar archive de/compression support to API.
Browse files Browse the repository at this point in the history
  • Loading branch information
retrixe committed Aug 14, 2023
1 parent 78177cc commit c04ce2c
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 66 deletions.
28 changes: 20 additions & 8 deletions API.md
Expand Up @@ -43,7 +43,7 @@ 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=boolean](#post-serveridcompresspathpathcompressboolean)
- [POST /server/{id}/compress?path=path&compress=algorithm&archiveType=archiveType](#post-serveridcompresspathpathcompressalgorithmarchivetypearchivetype)
- [POST /server/{id}/decompress?path=path](#post-serveriddecompresspathpath)

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

---

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

Compress files/folders in the working directory of the app into a ZIP file.
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+.

**Request Query Parameters:**

- `path` - The location to create the ZIP file at. This is relative to the server's root directory.
- `compress` - Optional, default is `true`. This specifies whether or not to compress files/folders in the ZIP file (using the default DEFLATE algorithm). ⚠️ *Warning:* This was broken in v1.0 (it accidentally used a header instead of query parameter), this was fixed in v1.1+.
- `path` - The location to create the archive at. This is relative to the server's root directory.
- `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+.

**Request Body:**

A JSON body containing an array of paths to compress (relative to the server's root directory), e.g. `["/config.json", "/logs"]`.

⚠️ *Warning:* This API was unable to compress folders in v1.0, this function was added in v1.1+.

**Response:**

HTTP 200 JSON body response `{"success":true}` is returned on success.
Expand All @@ -474,16 +479,23 @@ HTTP 200 JSON body response `{"success":true}` is returned on success.

### POST /server/{id}/decompress?path=path

Decompress a ZIP file in the working directory of the app.
Decompress a ZIP or TAR archive in the working directory of the app.

The full list of supported archive formats is:

- `.zip`
- `.tar(.gz/bz2/bz/zst/xz)` and their respective short forms `tgz/txz/tbz/tbz2/tzst`, support for these was added in v1.2+.

**Request Query Parameters:**

- `path` - The path of the ZIP file to decompress. This is relative to the server's root directory.
- `path` - The path of the archive to decompress. This is relative to the server's root directory.

**Request Body:**

A string containing the path to decompress the ZIP file to (relative to the server's root directory). A folder will be created at this path, to which the contents of the ZIP file will be extracted.
A string containing the path to decompress the archive to (relative to the server's root directory). A folder will be created at this path, to which the contents of the archive will be extracted.

**Response:**

HTTP 200 JSON body response `{"success":true}` is returned on success.

⚠️ Tip when attempting to decompress unsupported archive formats like `tar` on Octyne v1.1 and older: Check if the error is `An error occurred when decompressing ZIP file!` v1.2+ says `archive` instead of `ZIP file` and explicitly blocks unsupported archive types. This can help you inform the user if their Octyne installation is out of date, since decompressing these archives will fail on older versions.
170 changes: 112 additions & 58 deletions endpoints_files.go
@@ -1,8 +1,10 @@
package main

import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -351,7 +353,7 @@ func (connector *Connector) registerFileRoutes() {
}
})

// POST /server/{id}/compress?path=path&compress=true/false (compress is optional, default: true)
// POST /server/{id}/compress?path=path&compress=true/false/zstd/xz/gzip&archiveType=zip/tar
connector.Router.HandleFunc("/server/{id}/compress", func(w http.ResponseWriter, r *http.Request) {
// Check with authenticator.
user := connector.Validate(w, r)
Expand All @@ -360,84 +362,124 @@ func (connector *Connector) registerFileRoutes() {
}
// Get the process being accessed.
id := mux.Vars(r)["id"]
process, err := connector.Processes.Load(id)
process, exists := connector.Processes.Load(id)
// In case the process doesn't exist.
if !err {
if !exists {
httpError(w, "This server does not exist!", http.StatusNotFound)
return
} else if r.Method != "POST" {
httpError(w, "Only POST is allowed!", http.StatusMethodNotAllowed)
return
}
if r.Method == "POST" {
// Get the body.
var buffer bytes.Buffer
_, err := buffer.ReadFrom(r.Body)
if err != nil {
httpError(w, "Failed to read body!", http.StatusBadRequest)
return
}
// Decode the array body and send it to files.
var files []string
err = json.Unmarshal(buffer.Bytes(), &files)
if err != nil {
httpError(w, "Invalid JSON body!", http.StatusBadRequest)
// Decode parameters.
archiveType := "zip"
compression := "true"
if r.URL.Query().Get("archiveType") != "" {
archiveType = r.URL.Query().Get("archiveType")
}
if r.URL.Query().Get("compress") != "" {
compression = r.URL.Query().Get("compress")
}
if archiveType != "zip" && archiveType != "tar" {
httpError(w, "Invalid archive type!", http.StatusBadRequest)
return
} else if compression != "true" && compression != "false" &&
compression != "zstd" && compression != "xz" && compression != "gzip" {
httpError(w, "Invalid compression type!", http.StatusBadRequest)
return
}
// Get the body.
var buffer bytes.Buffer
_, err := buffer.ReadFrom(r.Body)
if err != nil {
httpError(w, "Failed to read body!", http.StatusBadRequest)
return
}
// Decode the array body and send it to files.
var files []string
err = json.Unmarshal(buffer.Bytes(), &files)
if err != nil {
httpError(w, "Invalid JSON body!", http.StatusBadRequest)
return
}
// Validate every path.
process.ServerConfigMutex.RLock()
defer process.ServerConfigMutex.RUnlock()
for _, file := range files {
filepath := joinPath(process.Directory, file)
if !strings.HasPrefix(filepath, clean(process.Directory)) {
httpError(w, "One of the paths provided is outside the server directory!", http.StatusForbidden)
return
}
// Validate every path.
process.ServerConfigMutex.RLock()
defer process.ServerConfigMutex.RUnlock()
for _, file := range files {
filepath := joinPath(process.Directory, file)
if !strings.HasPrefix(filepath, clean(process.Directory)) {
httpError(w, "One of the paths provided is outside the server directory!", http.StatusForbidden)
return
} else if _, err := os.Stat(filepath); err != nil {
if os.IsNotExist(err) {
httpError(w, "The file "+file+" does not exist!", http.StatusBadRequest)
} else {
log.Println("An error occurred when checking "+filepath+" exists for compression", "("+process.Name+")", err)
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
}
return
} else if _, err := os.Stat(filepath); err != nil {
if os.IsNotExist(err) {
httpError(w, "The file "+file+" does not exist!", http.StatusBadRequest)
} else {
log.Println("An error occurred when checking "+filepath+" exists for compression", "("+process.Name+")", err)
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
}
}
// Check if a file exists at the location of the archive.
archivePath := joinPath(process.Directory, r.URL.Query().Get("path"))
if !strings.HasPrefix(archivePath, clean(process.Directory)) {
httpError(w, "The requested archive is outside the server directory!", http.StatusForbidden)
return
}
_, exists := os.Stat(archivePath)
if !os.IsNotExist(exists) {
httpError(w, "A file/folder already exists at the path of requested archive!", http.StatusBadRequest)
return
}
}
// Check if a file exists at the location of the archive.
archivePath := joinPath(process.Directory, r.URL.Query().Get("path"))
if !strings.HasPrefix(archivePath, clean(process.Directory)) {
httpError(w, "The requested archive is outside the server directory!", http.StatusForbidden)
return
}
_, err = os.Stat(archivePath)
if !os.IsNotExist(err) {
httpError(w, "A file/folder already exists at the path of requested archive!", http.StatusBadRequest)
return
}

// Begin compressing the archive.
archiveFile, err := os.Create(archivePath)
if err != nil {
log.Println("An error occurred when creating "+archivePath+" for compression", "("+process.Name+")", err)
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
return
}
defer archiveFile.Close()
// Begin compressing the archive.
archiveFile, err := os.Create(archivePath)
if err != nil {
log.Println("An error occurred when creating "+archivePath+" for compression", "("+process.Name+")", err)
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
return
}
defer archiveFile.Close()
if archiveType == "zip" {
archive := zip.NewWriter(archiveFile)
defer archive.Close()
// Archive stuff inside.
compressed := r.URL.Query().Get("compress") != "false"
// TODO: Why is parent always process.Directory? Support different base path?
for _, file := range files {
err := system.AddFileToZip(archive, process.Directory, file, compressed)
err := system.AddFileToZip(archive, process.Directory, 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)
return
}
}
connector.Info("server.files.compress", "ip", GetIP(r), "user", user, "server", id,
"archive", clean(r.URL.Query().Get("path")), "files", files, "compressed", compressed)
writeJsonStringRes(w, "{\"success\":true}")
} else {
httpError(w, "Only POST is allowed!", http.StatusMethodNotAllowed)
var archive *tar.Writer
if compression == "true" || compression == "gzip" || compression == "" {
compressionWriter := gzip.NewWriter(archiveFile)
defer compressionWriter.Close()
archive = tar.NewWriter(compressionWriter)
} else if compression == "xz" || compression == "zstd" {
compressionWriter := system.NativeCompressionWriter(archiveFile, compression)
defer compressionWriter.Close()
archive = tar.NewWriter(compressionWriter)
} else {
archive = tar.NewWriter(archiveFile)
}
defer archive.Close()
for _, file := range files {
err := system.AddFileToTar(archive, process.Directory, file)
if err != nil {
log.Println("An error occurred when adding "+file+" to "+archivePath, "("+process.Name+")", err)
httpError(w, "Internal Server Error!", http.StatusInternalServerError)
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}")
})

// POST /server/{id}/decompress?path=path
Expand Down Expand Up @@ -506,7 +548,19 @@ func (connector *Connector) registerFileRoutes() {
return
}
// Decompress the archive.
err = system.UnzipFile(archivePath, unpackPath)
if strings.HasSuffix(archivePath, ".zip") {
err = system.UnzipFile(archivePath, unpackPath)
} else if strings.HasSuffix(archivePath, ".tar") ||
strings.HasSuffix(archivePath, ".tar.gz") || strings.HasSuffix(archivePath, ".tgz") ||
strings.HasSuffix(archivePath, ".tar.bz") || strings.HasSuffix(archivePath, ".tbz") ||
strings.HasSuffix(archivePath, ".tar.bz2") || strings.HasSuffix(archivePath, ".tbz2") ||
strings.HasSuffix(archivePath, ".tar.xz") || strings.HasSuffix(archivePath, ".txz") ||
strings.HasSuffix(archivePath, ".tar.zst") || strings.HasSuffix(archivePath, ".tzst") {
err = system.ExtractTarFile(archivePath, unpackPath)
} else {
httpError(w, "Unsupported archive file!", http.StatusBadRequest)
return
}
if err != nil {
httpError(w, "An error occurred while decompressing archive!", http.StatusInternalServerError)
return
Expand Down

0 comments on commit c04ce2c

Please sign in to comment.