rclone/fs/rc/webgui/webgui.go
Nick Craig-Wood e43b5ce5e5 Remove github.com/pkg/errors and replace with std library version
This is possible now that we no longer support go1.12 and brings
rclone into line with standard practices in the Go world.

This also removes errors.New and errors.Errorf from lib/errors and
prefers the stdlib errors package over lib/errors.
2021-11-07 11:53:30 +00:00

281 lines
7.6 KiB
Go

// Define the Web GUI helpers
package webgui
import (
"archive/zip"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/lib/file"
)
// GetLatestReleaseURL returns the latest release details of the rclone-webui-react
func GetLatestReleaseURL(fetchURL string) (string, string, int, error) {
resp, err := http.Get(fetchURL)
if err != nil {
return "", "", 0, fmt.Errorf("failed getting latest release of rclone-webui: %w", err)
}
defer fs.CheckClose(resp.Body, &err)
if resp.StatusCode != http.StatusOK {
return "", "", 0, fmt.Errorf("bad HTTP status %d (%s) when fetching %s", resp.StatusCode, resp.Status, fetchURL)
}
results := gitHubRequest{}
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
return "", "", 0, fmt.Errorf("could not decode results from http request: %w", err)
}
if len(results.Assets) < 1 {
return "", "", 0, errors.New("could not find an asset in the release. " +
"check if asset was successfully added in github release assets")
}
res := results.Assets[0].BrowserDownloadURL
tag := results.TagName
size := results.Assets[0].Size
return res, tag, size, nil
}
// CheckAndDownloadWebGUIRelease is a helper function to download and setup latest release of rclone-webui-react
func CheckAndDownloadWebGUIRelease(checkUpdate bool, forceUpdate bool, fetchURL string, cacheDir string) (err error) {
cachePath := filepath.Join(cacheDir, "webgui")
tagPath := filepath.Join(cachePath, "tag")
extractPath := filepath.Join(cachePath, "current")
extractPathExist, extractPathStat, err := exists(extractPath)
if err != nil {
return err
}
if extractPathExist && !extractPathStat.IsDir() {
return errors.New("Web GUI path exists, but is a file instead of folder. Please check the path " + extractPath)
}
// Get the latest release details
WebUIURL, tag, size, err := GetLatestReleaseURL(fetchURL)
if err != nil {
return fmt.Errorf("Error checking for web gui release update, skipping update: %w", err)
}
dat, err := ioutil.ReadFile(tagPath)
tagsMatch := false
if err != nil {
fs.Errorf(nil, "Error reading tag file at %s ", tagPath)
checkUpdate = true
} else if string(dat) == tag {
tagsMatch = true
}
fs.Debugf(nil, "Current tag: %s, Release tag: %s", string(dat), tag)
if !tagsMatch {
fs.Infof(nil, "A release (%s) for gui is present at %s. Use --rc-web-gui-update to update. Your current version is (%s)", tag, WebUIURL, string(dat))
}
// if the old file exists does not exist or forced update is enforced.
// TODO: Add hashing to check integrity of the previous update.
if !extractPathExist || checkUpdate || forceUpdate {
if tagsMatch {
fs.Logf(nil, "No update to Web GUI available.")
if !forceUpdate {
return nil
}
fs.Logf(nil, "Force update the Web GUI binary.")
}
zipName := tag + ".zip"
zipPath := filepath.Join(cachePath, zipName)
cachePathExist, cachePathStat, _ := exists(cachePath)
if !cachePathExist {
if err := file.MkdirAll(cachePath, 0755); err != nil {
return errors.New("Error creating cache directory: " + cachePath)
}
}
if cachePathExist && !cachePathStat.IsDir() {
return errors.New("Web GUI path is a file instead of folder. Please check it " + extractPath)
}
fs.Logf(nil, "A new release for gui (%s) is present at %s", tag, WebUIURL)
fs.Logf(nil, "Downloading webgui binary. Please wait. [Size: %s, Path : %s]\n", strconv.Itoa(size), zipPath)
// download the zip from latest url
err = DownloadFile(zipPath, WebUIURL)
if err != nil {
return err
}
err = os.RemoveAll(extractPath)
if err != nil {
fs.Logf(nil, "No previous downloads to remove")
}
fs.Logf(nil, "Unzipping webgui binary")
err = Unzip(zipPath, extractPath)
if err != nil {
return err
}
err = os.RemoveAll(zipPath)
if err != nil {
fs.Logf(nil, "Downloaded ZIP cannot be deleted")
}
err = ioutil.WriteFile(tagPath, []byte(tag), 0644)
if err != nil {
fs.Infof(nil, "Cannot write tag file. You may be required to redownload the binary next time.")
}
} else {
fs.Logf(nil, "Web GUI exists. Update skipped.")
}
return nil
}
// DownloadFile is a helper function to download a file from url to the filepath
func DownloadFile(filepath string, url string) (err error) {
// Get the data
resp, err := http.Get(url)
if err != nil {
return err
}
defer fs.CheckClose(resp.Body, &err)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad HTTP status %d (%s) when fetching %s", resp.StatusCode, resp.Status, url)
}
// Create the file
out, err := os.Create(filepath)
if err != nil {
return err
}
defer fs.CheckClose(out, &err)
// Write the body to file
_, err = io.Copy(out, resp.Body)
return err
}
// Unzip is a helper function to Unzip a file specified in src to path dest
func Unzip(src, dest string) (err error) {
dest = filepath.Clean(dest) + string(os.PathSeparator)
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer fs.CheckClose(r, &err)
if err := file.MkdirAll(dest, 0755); err != nil {
return err
}
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
path := filepath.Join(dest, f.Name)
// Check for Zip Slip: https://github.com/rclone/rclone/issues/3529
if !strings.HasPrefix(path, dest) {
return fmt.Errorf("%s: illegal file path", path)
}
rc, err := f.Open()
if err != nil {
return err
}
defer fs.CheckClose(rc, &err)
if f.FileInfo().IsDir() {
if err := file.MkdirAll(path, 0755); err != nil {
return err
}
} else {
if err := file.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
f, err := file.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer fs.CheckClose(f, &err)
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}
func exists(path string) (existence bool, stat os.FileInfo, err error) {
stat, err = os.Stat(path)
if err == nil {
return true, stat, nil
}
if os.IsNotExist(err) {
return false, nil, nil
}
return false, stat, err
}
// CreatePathIfNotExist creates the path to a folder if it does not exist
func CreatePathIfNotExist(path string) (err error) {
exists, stat, _ := exists(path)
if !exists {
if err := file.MkdirAll(path, 0755); err != nil {
return errors.New("Error creating : " + path)
}
}
if exists && !stat.IsDir() {
return errors.New("Path is a file instead of folder. Please check it " + path)
}
return nil
}
// gitHubRequest Maps the GitHub API request to structure
type gitHubRequest struct {
URL string `json:"url"`
Prerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
TagName string `json:"tag_name"`
Assets []struct {
URL string `json:"url"`
ID int `json:"id"`
NodeID string `json:"node_id"`
Name string `json:"name"`
Label string `json:"label"`
ContentType string `json:"content_type"`
State string `json:"state"`
Size int `json:"size"`
DownloadCount int `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"`
TarballURL string `json:"tarball_url"`
ZipballURL string `json:"zipball_url"`
Body string `json:"body"`
}