rc: deprecate --rc-web-gui flag

This commit removes the workings of the old web ui which hasn't been
maintained for 6 years. If users supply --rc-web-gui then rclone will
exit with an error pointing users at the maintained `rclone gui`
command.
This commit is contained in:
Nick Craig-Wood
2026-08-19 12:15:00 +01:00
parent 00496f5dfb
commit 2f7d4e3f9f
6 changed files with 17 additions and 1110 deletions
+15 -10
View File
@@ -46,28 +46,33 @@ var OptionsInfo = fs.Options{{
}, {
Name: "rc_web_gui",
Default: false,
Help: "Launch WebGUI on localhost",
Help: "Launch WebGUI on localhost **DEPRECATED**",
Groups: "RC",
Hide: fs.OptionHideBoth,
}, {
Name: "rc_web_gui_update",
Default: false,
Help: "Check and update to latest version of web gui",
Help: "Check and update to latest version of web gui **DEPRECATED**",
Groups: "RC",
Hide: fs.OptionHideBoth,
}, {
Name: "rc_web_gui_force_update",
Default: false,
Help: "Force update to latest version of web gui",
Help: "Force update to latest version of web gui **DEPRECATED**",
Groups: "RC",
Hide: fs.OptionHideBoth,
}, {
Name: "rc_web_gui_no_open_browser",
Default: false,
Help: "Don't open the browser automatically",
Help: "Don't open the browser automatically **DEPRECATED**",
Groups: "RC",
Hide: fs.OptionHideBoth,
}, {
Name: "rc_web_fetch_url",
Default: "https://api.github.com/repos/rclone/rclone-webui-react/releases/latest",
Help: "URL to fetch the releases for webgui",
Help: "URL to fetch the releases for webgui **DEPRECATED**",
Groups: "RC",
Hide: fs.OptionHideBoth,
}, {
Name: "rc_enable_metrics",
Default: false,
@@ -107,11 +112,11 @@ type Options struct {
Serve bool `config:"rc_serve"` // set to serve files from remotes
ServeNoModTime bool `config:"rc_serve_no_modtime"` // don't read the modification time
NoAuth bool `config:"rc_no_auth"` // set to disable auth checks on methods which require it
WebUI bool `config:"rc_web_gui"` // set to launch the web ui
WebGUIUpdate bool `config:"rc_web_gui_update"` // set to check new update
WebGUIForceUpdate bool `config:"rc_web_gui_force_update"` // set to force download new update
WebGUINoOpenBrowser bool `config:"rc_web_gui_no_open_browser"` // set to disable auto opening browser
WebGUIFetchURL string `config:"rc_web_fetch_url"` // set the default url for fetching webgui
WebUI bool `config:"rc_web_gui"` // set to launch the web ui **DEPRECATED**
WebGUIUpdate bool `config:"rc_web_gui_update"` // set to check new update **DEPRECATED**
WebGUIForceUpdate bool `config:"rc_web_gui_force_update"` // set to force download new update **DEPRECATED**
WebGUINoOpenBrowser bool `config:"rc_web_gui_no_open_browser"` // set to disable auto opening browser **DEPRECATED**
WebGUIFetchURL string `config:"rc_web_fetch_url"` // set the default url for fetching webgui **DEPRECATED**
EnableMetrics bool `config:"rc_enable_metrics"` // set to disable prometheus metrics on /metrics
MetricsHTTP libhttp.Config `config:"metrics"`
MetricsAuth libhttp.AuthConfig `config:"metrics"`
+2 -49
View File
@@ -11,7 +11,6 @@ import (
"mime"
"net/http"
"net/url"
"path/filepath"
"regexp"
"sort"
"strings"
@@ -25,10 +24,8 @@ import (
"github.com/rclone/rclone/fs/list"
"github.com/rclone/rclone/fs/rc"
"github.com/rclone/rclone/fs/rc/jobs"
"github.com/rclone/rclone/fs/rc/webgui"
libhttp "github.com/rclone/rclone/lib/http"
"github.com/rclone/rclone/lib/http/serve"
"github.com/rclone/rclone/lib/random"
"github.com/skratchdot/open-golang/open"
)
@@ -65,41 +62,12 @@ func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) (*Serve
_ = mime.AddExtensionType(".wasm", "application/wasm")
_ = mime.AddExtensionType(".js", "application/javascript")
cachePath := filepath.Join(config.GetCacheDir(), "webgui")
extractPath := filepath.Join(cachePath, "current/build")
// File handling
if opt.Files != "" {
if opt.WebUI {
fs.Logf(nil, "--rc-files overrides --rc-web-gui command\n")
}
fs.Logf(nil, "Serving files from %q", opt.Files)
fileHandler = http.FileServer(http.Dir(opt.Files))
} else if opt.WebUI {
if err := webgui.CheckAndDownloadWebGUIRelease(opt.WebGUIUpdate, opt.WebGUIForceUpdate, opt.WebGUIFetchURL, config.GetCacheDir()); err != nil {
fs.Errorf(nil, "Error while fetching the latest release of Web GUI: %v", err)
}
if opt.NoAuth {
fs.Logf(nil, "It is recommended to use web gui with auth.")
} else {
if opt.Auth.BasicUser == "" && opt.Auth.HtPasswd == "" {
opt.Auth.BasicUser = "gui"
fs.Infof(nil, "No username specified. Using default username: %s \n", rc.Opt.Auth.BasicUser)
}
if opt.Auth.BasicPass == "" && opt.Auth.HtPasswd == "" {
randomPass, err := random.Password(128)
if err != nil {
fs.Fatalf(nil, "Failed to make password: %v", err)
}
opt.Auth.BasicPass = randomPass
fs.Infof(nil, "No password specified. Using random password: %s \n", randomPass)
}
}
opt.Serve = true
fs.Logf(nil, "Serving Web GUI")
fileHandler = http.FileServer(http.Dir(extractPath))
pluginsHandler = http.FileServer(http.Dir(webgui.PluginsPath))
return nil, errors.New("--rc-web-gui has been superseded by the `rclone gui` command")
}
s := &Server{
@@ -170,7 +138,7 @@ func (s *Server) Serve() error {
openURL.RawPath = "/#/login"
}
// Don't open browser if serving in testing environment or required not to do so.
if flag.Lookup("test.v") == nil && !s.opt.WebGUINoOpenBrowser {
if flag.Lookup("test.v") == nil {
if err := open.Start(openURL.String()); err != nil {
fs.Errorf(nil, "Failed to open Web GUI in browser: %v. Manually access it at: %s", err, openURL.String())
}
@@ -459,21 +427,6 @@ func (s *Server) handleGet(w http.ResponseWriter, r *http.Request, path string)
s.serveRoot(w, r)
return
case s.files != nil:
if s.opt.WebUI {
pluginsMatchResult := webgui.PluginsMatch.FindStringSubmatch(path)
if len(pluginsMatchResult) > 2 {
ok := webgui.ServePluginOK(w, r, pluginsMatchResult)
if !ok {
r.URL.Path = fmt.Sprintf("/%s/%s/app/build/%s", pluginsMatchResult[1], pluginsMatchResult[2], pluginsMatchResult[3])
s.pluginsHandler.ServeHTTP(w, r)
return
}
return
} else if webgui.ServePluginWithReferrerOK(w, r, path) {
return
}
}
// Serve the files
r.URL.Path = "/" + path
s.files.ServeHTTP(w, r)
-299
View File
@@ -1,299 +0,0 @@
// Package webgui provides plugin functionality to the Web GUI.
package webgui
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/rc"
)
// PackageJSON is the structure of package.json of a plugin
type PackageJSON struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
Author string `json:"author"`
Copyright string `json:"copyright"`
License string `json:"license"`
Private bool `json:"private"`
Homepage string `json:"homepage"`
TestURL string `json:"testUrl"`
Repository struct {
Type string `json:"type"`
URL string `json:"url"`
} `json:"repository"`
Bugs struct {
URL string `json:"url"`
} `json:"bugs"`
Rclone RcloneConfig `json:"rclone"`
}
// RcloneConfig represents the rclone specific config
type RcloneConfig struct {
HandlesType []string `json:"handlesType"`
PluginType string `json:"pluginType"`
RedirectReferrer bool `json:"redirectReferrer"`
Test bool `json:"-"`
}
func (r *PackageJSON) isTesting() bool {
return r.Rclone.Test
}
var (
//loadedTestPlugins *Plugins
cachePath string
loadedPlugins *Plugins
pluginsProxy = &httputil.ReverseProxy{}
// PluginsMatch is used for matching author and plugin name in the url path
PluginsMatch = regexp.MustCompile(`^plugins\/([^\/]*)\/([^\/\?]+)[\/]?(.*)$`)
// PluginsPath is the base path where webgui plugins are stored
PluginsPath string
pluginsConfigPath string
availablePluginsJSONPath = "availablePlugins.json"
initSuccess = false
initMutex = &sync.Mutex{}
)
// Plugins represents the structure how plugins are saved onto disk
type Plugins struct {
mutex sync.Mutex
LoadedPlugins map[string]PackageJSON `json:"loadedPlugins"`
fileName string
}
func newPlugins(fileName string) *Plugins {
p := Plugins{LoadedPlugins: map[string]PackageJSON{}}
p.fileName = fileName
p.mutex = sync.Mutex{}
return &p
}
func initPluginsOrError() error {
if !rc.Opt.WebUI {
return errors.New("WebUI needs to be enabled for plugins to work")
}
initMutex.Lock()
defer initMutex.Unlock()
if !initSuccess {
cachePath = filepath.Join(config.GetCacheDir(), "webgui")
PluginsPath = filepath.Join(cachePath, "plugins")
pluginsConfigPath = filepath.Join(PluginsPath, "config")
loadedPlugins = newPlugins(availablePluginsJSONPath)
err := loadedPlugins.readFromFile()
if err != nil {
fs.Errorf(nil, "error reading available plugins: %v", err)
}
initSuccess = true
}
return nil
}
func (p *Plugins) readFromFile() (err error) {
err = CreatePathIfNotExist(pluginsConfigPath)
if err != nil {
return err
}
availablePluginsJSON := filepath.Join(pluginsConfigPath, p.fileName)
_, err = os.Stat(availablePluginsJSON)
if err == nil {
data, err := os.ReadFile(availablePluginsJSON)
if err != nil {
return err
}
err = json.Unmarshal(data, &p)
if err != nil {
fs.Logf(nil, "%s", err)
}
return nil
} else if os.IsNotExist(err) {
// path does not exist
err = p.writeToFile()
if err != nil {
return err
}
}
return nil
}
func (p *Plugins) addPlugin(pluginName string, packageJSONPath string) (err error) {
p.mutex.Lock()
defer p.mutex.Unlock()
data, err := os.ReadFile(packageJSONPath)
if err != nil {
return err
}
var pkgJSON = PackageJSON{}
err = json.Unmarshal(data, &pkgJSON)
if err != nil {
return err
}
p.LoadedPlugins[pluginName] = pkgJSON
err = p.writeToFile()
if err != nil {
return err
}
return nil
}
func (p *Plugins) writeToFile() (err error) {
availablePluginsJSON := filepath.Join(pluginsConfigPath, p.fileName)
file, err := json.MarshalIndent(p, "", " ")
if err != nil {
fs.Logf(nil, "%s", err)
}
err = os.WriteFile(availablePluginsJSON, file, 0755)
if err != nil {
fs.Logf(nil, "%s", err)
}
return nil
}
func (p *Plugins) removePlugin(name string) (err error) {
p.mutex.Lock()
defer p.mutex.Unlock()
err = p.readFromFile()
if err != nil {
return err
}
_, ok := p.LoadedPlugins[name]
if !ok {
return fmt.Errorf("plugin %s not loaded", name)
}
delete(p.LoadedPlugins, name)
err = p.writeToFile()
if err != nil {
return err
}
return nil
}
// GetPluginByName returns the plugin object for the key (author/plugin-name)
func (p *Plugins) GetPluginByName(name string) (out *PackageJSON, err error) {
p.mutex.Lock()
defer p.mutex.Unlock()
po, ok := p.LoadedPlugins[name]
if !ok {
return nil, fmt.Errorf("plugin %s not loaded", name)
}
return &po, nil
}
// getAuthorRepoBranchGitHub gives author, repoName and branch from a github.com url
//
// url examples:
// https://github.com/rclone/rclone-webui-react/
// http://github.com/rclone/rclone-webui-react
// https://github.com/rclone/rclone-webui-react/tree/caman-js
// github.com/rclone/rclone-webui-react
func getAuthorRepoBranchGitHub(url string) (author string, repoName string, branch string, err error) {
repoURL := url
repoURL = strings.Replace(repoURL, "https://", "", 1)
repoURL = strings.Replace(repoURL, "http://", "", 1)
urlSplits := strings.Split(repoURL, "/")
if len(urlSplits) < 3 || len(urlSplits) > 5 || urlSplits[0] != "github.com" {
return "", "", "", fmt.Errorf("invalid github url: %s", url)
}
// get branch name
if len(urlSplits) == 5 && urlSplits[3] == "tree" {
return urlSplits[1], urlSplits[2], urlSplits[4], nil
}
return urlSplits[1], urlSplits[2], "master", nil
}
func filterPlugins(plugins *Plugins, compare func(packageJSON *PackageJSON) bool) map[string]PackageJSON {
output := map[string]PackageJSON{}
for key, val := range plugins.LoadedPlugins {
if compare(&val) {
output[key] = val
}
}
return output
}
// getDirectorForProxy is a helper function for reverse proxy of test plugins
func getDirectorForProxy(origin *url.URL) func(req *http.Request) {
return func(req *http.Request) {
req.Header.Add("X-Forwarded-Host", req.Host)
req.Header.Add("X-Origin-Host", origin.Host)
req.URL.Scheme = "http"
req.URL.Host = origin.Host
req.URL.Path = origin.Path
}
}
// ServePluginOK checks the plugin url and uses reverse proxy to allow redirection for content not being served by rclone
func ServePluginOK(w http.ResponseWriter, r *http.Request, pluginsMatchResult []string) (ok bool) {
testPlugin, err := loadedPlugins.GetPluginByName(fmt.Sprintf("%s/%s", pluginsMatchResult[1], pluginsMatchResult[2]))
if err != nil {
return false
}
if !testPlugin.Rclone.Test {
return false
}
origin, _ := url.Parse(fmt.Sprintf("%s/%s", testPlugin.TestURL, pluginsMatchResult[3]))
director := getDirectorForProxy(origin)
pluginsProxy.Director = director
pluginsProxy.ServeHTTP(w, r)
return true
}
var referrerPathReg = regexp.MustCompile(`^(https?):\/\/(.+):([0-9]+)?\/(.*)\/?\?(.*)$`)
// ServePluginWithReferrerOK check if redirectReferrer is set for the referred a plugin, if yes,
// sends a redirect to actual url. This function is useful for plugins to refer to absolute paths when
// the referrer in http.Request is set
func ServePluginWithReferrerOK(w http.ResponseWriter, r *http.Request, path string) (ok bool) {
err := initPluginsOrError()
if err != nil {
return false
}
referrer := r.Referer()
referrerPathMatch := referrerPathReg.FindStringSubmatch(referrer)
if len(referrerPathMatch) > 3 {
referrerPluginMatch := PluginsMatch.FindStringSubmatch(referrerPathMatch[4])
if len(referrerPluginMatch) > 2 {
pluginKey := fmt.Sprintf("%s/%s", referrerPluginMatch[1], referrerPluginMatch[2])
currentPlugin, err := loadedPlugins.GetPluginByName(pluginKey)
if err != nil {
return false
}
if currentPlugin.Rclone.RedirectReferrer {
path = fmt.Sprintf("/plugins/%s/%s/%s", referrerPluginMatch[1], referrerPluginMatch[2], path)
http.Redirect(w, r, path, http.StatusMovedPermanently)
return true
}
}
}
return false
}
-328
View File
@@ -1,328 +0,0 @@
package webgui
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/rc"
)
func init() {
rc.Add(rc.Call{
Path: "pluginsctl/listTestPlugins",
Fn: rcListTestPlugins,
Title: "Show currently loaded test plugins",
Help: `Allows listing of test plugins with the rclone.test set to true in package.json of the plugin.
This takes no parameters and returns:
- loadedTestPlugins - list of currently available test plugins.
E.g.
rclone rc pluginsctl/listTestPlugins
`,
})
}
func rcListTestPlugins(_ context.Context, _ rc.Params) (out rc.Params, err error) {
err = initPluginsOrError()
if err != nil {
return nil, err
}
return rc.Params{
"loadedTestPlugins": filterPlugins(loadedPlugins, func(json *PackageJSON) bool { return json.isTesting() }),
}, nil
}
func init() {
rc.Add(rc.Call{
Path: "pluginsctl/removeTestPlugin",
Fn: rcRemoveTestPlugin,
Title: "Remove a test plugin",
Help: `This allows you to remove a plugin using it's name.
This takes the following parameters:
- name - name of the plugin in the format ` + "`author`/`plugin_name`" + `.
Example:
rclone rc pluginsctl/removeTestPlugin name=rclone/rclone-webui-react
`,
})
}
func rcRemoveTestPlugin(_ context.Context, in rc.Params) (out rc.Params, err error) {
err = initPluginsOrError()
if err != nil {
return nil, err
}
name, err := in.GetString("name")
if err != nil {
return nil, err
}
err = loadedPlugins.removePlugin(name)
if err != nil {
return nil, err
}
return nil, nil
}
func init() {
rc.Add(rc.Call{
Path: "pluginsctl/addPlugin",
Fn: rcAddPlugin,
Title: "Add a plugin using url",
Help: `Used for adding a plugin to the webgui.
This takes the following parameters:
- url - http url of the github repo where the plugin is hosted (http://github.com/rclone/rclone-webui-react).
Example:
rclone rc pluginsctl/addPlugin
`,
})
}
func rcAddPlugin(_ context.Context, in rc.Params) (out rc.Params, err error) {
err = initPluginsOrError()
if err != nil {
return nil, err
}
pluginURL, err := in.GetString("url")
if err != nil {
return nil, err
}
author, repoName, repoBranch, err := getAuthorRepoBranchGitHub(pluginURL)
if err != nil {
return nil, err
}
branch, err := in.GetString("branch")
if err != nil || branch == "" {
branch = repoBranch
}
version, err := in.GetString("version")
if err != nil || version == "" {
version = "latest"
}
err = CreatePathIfNotExist(PluginsPath)
if err != nil {
return nil, err
}
// fetch and package.json
// https://raw.githubusercontent.com/rclone/rclone-webui-react/master/package.json
pluginID := fmt.Sprintf("%s/%s", author, repoName)
currentPluginPath := filepath.Join(PluginsPath, pluginID)
err = CreatePathIfNotExist(currentPluginPath)
if err != nil {
return nil, err
}
packageJSONUrl := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/package.json", author, repoName, branch)
packageJSONFilePath := filepath.Join(currentPluginPath, "package.json")
err = DownloadFile(packageJSONFilePath, packageJSONUrl)
if err != nil {
return nil, err
}
// register in plugins
// download release and save in plugins/<author>/repo-name/app
// https://api.github.com/repos/rclone/rclone-webui-react/releases/latest
releaseURL, tag, _, err := GetLatestReleaseURL(fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/%s", author, repoName, version))
if err != nil {
return nil, err
}
zipName := tag + ".zip"
zipPath := filepath.Join(currentPluginPath, zipName)
err = DownloadFile(zipPath, releaseURL)
if err != nil {
return nil, err
}
extractPath := filepath.Join(currentPluginPath, "app")
err = CreatePathIfNotExist(extractPath)
if err != nil {
return nil, err
}
err = os.RemoveAll(extractPath)
if err != nil {
fs.Logf(nil, "No previous downloads to remove")
}
fs.Logf(nil, "Unzipping plugin binary")
err = Unzip(zipPath, extractPath)
if err != nil {
return nil, err
}
err = loadedPlugins.addPlugin(pluginID, packageJSONFilePath)
if err != nil {
return nil, err
}
return nil, nil
}
func init() {
rc.Add(rc.Call{
Path: "pluginsctl/listPlugins",
Fn: rcGetPlugins,
Title: "Get the list of currently loaded plugins",
Help: `This allows you to get the currently enabled plugins and their details.
This takes no parameters and returns:
- loadedPlugins - list of current production plugins.
- testPlugins - list of temporarily loaded development plugins, usually running on a different server.
E.g.
rclone rc pluginsctl/listPlugins
`,
})
}
func rcGetPlugins(_ context.Context, _ rc.Params) (out rc.Params, err error) {
err = initPluginsOrError()
if err != nil {
return nil, err
}
err = loadedPlugins.readFromFile()
if err != nil {
return nil, err
}
return rc.Params{
"loadedPlugins": filterPlugins(loadedPlugins, func(packageJSON *PackageJSON) bool { return !packageJSON.isTesting() }),
"loadedTestPlugins": filterPlugins(loadedPlugins, func(packageJSON *PackageJSON) bool { return packageJSON.isTesting() }),
}, nil
}
func init() {
rc.Add(rc.Call{
Path: "pluginsctl/removePlugin",
Fn: rcRemovePlugin,
Title: "Remove a loaded plugin",
Help: `This allows you to remove a plugin using it's name.
This takes parameters:
- name - name of the plugin in the format ` + "`author`/`plugin_name`" + `.
E.g.
rclone rc pluginsctl/removePlugin name=rclone/video-plugin
`,
})
}
func rcRemovePlugin(_ context.Context, in rc.Params) (out rc.Params, err error) {
err = initPluginsOrError()
if err != nil {
return nil, err
}
name, err := in.GetString("name")
if err != nil {
return nil, err
}
err = loadedPlugins.removePlugin(name)
if err != nil {
return nil, err
}
return nil, nil
}
func init() {
rc.Add(rc.Call{
Path: "pluginsctl/getPluginsForType",
Fn: rcGetPluginsForType,
Title: "Get plugins with type criteria",
Help: `This shows all possible plugins by a mime type.
This takes the following parameters:
- type - supported mime type by a loaded plugin e.g. (video/mp4, audio/mp3).
- pluginType - filter plugins based on their type e.g. (DASHBOARD, FILE_HANDLER, TERMINAL).
Returns:
- loadedPlugins - list of current production plugins.
- testPlugins - list of temporarily loaded development plugins, usually running on a different server.
Example:
rclone rc pluginsctl/getPluginsForType type=video/mp4
`,
})
}
func rcGetPluginsForType(_ context.Context, in rc.Params) (out rc.Params, err error) {
err = initPluginsOrError()
if err != nil {
return nil, err
}
handlesType, err := in.GetString("type")
if err != nil {
handlesType = ""
}
pluginType, err := in.GetString("pluginType")
if err != nil {
pluginType = ""
}
var loadedPluginsResult map[string]PackageJSON
var loadedTestPluginsResult map[string]PackageJSON
if pluginType == "" || pluginType == "FileHandler" {
loadedPluginsResult = filterPlugins(loadedPlugins, func(packageJSON *PackageJSON) bool {
for i := range packageJSON.Rclone.HandlesType {
if packageJSON.Rclone.HandlesType[i] == handlesType && !packageJSON.Rclone.Test {
return true
}
}
return false
})
loadedTestPluginsResult = filterPlugins(loadedPlugins, func(packageJSON *PackageJSON) bool {
for i := range packageJSON.Rclone.HandlesType {
if packageJSON.Rclone.HandlesType[i] == handlesType && packageJSON.Rclone.Test {
return true
}
}
return false
})
} else {
loadedPluginsResult = filterPlugins(loadedPlugins, func(packageJSON *PackageJSON) bool {
return packageJSON.Rclone.PluginType == pluginType && !packageJSON.isTesting()
})
loadedTestPluginsResult = filterPlugins(loadedPlugins, func(packageJSON *PackageJSON) bool {
return packageJSON.Rclone.PluginType == pluginType && packageJSON.isTesting()
})
}
return rc.Params{
"loadedPlugins": loadedPluginsResult,
"loadedTestPlugins": loadedTestPluginsResult,
}, nil
}
-146
View File
@@ -1,146 +0,0 @@
package webgui
import (
"context"
"path/filepath"
"strings"
"testing"
"github.com/rclone/rclone/fs/rc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testPluginName = "rclone-test-plugin"
const testPluginAuthor = "rclone"
const testPluginKey = testPluginAuthor + "/" + testPluginName
const testPluginURL = "https://github.com/" + testPluginAuthor + "/" + testPluginName + "/"
func init() {
rc.Opt.WebUI = true
}
func setCacheDir(t *testing.T) {
cacheDir := t.TempDir()
PluginsPath = filepath.Join(cacheDir, "plugins")
pluginsConfigPath = filepath.Join(cacheDir, "config")
loadedPlugins = newPlugins(availablePluginsJSONPath)
err := loadedPlugins.readFromFile()
assert.Nil(t, err)
}
func addPlugin(t *testing.T) {
addPlugin := rc.Calls.Get("pluginsctl/addPlugin")
assert.NotNil(t, addPlugin)
in := rc.Params{
"url": testPluginURL,
}
out, err := addPlugin.Fn(context.Background(), in)
if err != nil && strings.Contains(err.Error(), "bad HTTP status") {
t.Skipf("skipping test as plugin download failed: %v", err)
}
require.Nil(t, err)
assert.Nil(t, out)
}
func removePlugin(t *testing.T) {
addPlugin := rc.Calls.Get("pluginsctl/removePlugin")
assert.NotNil(t, addPlugin)
in := rc.Params{
"name": testPluginKey,
}
out, err := addPlugin.Fn(context.Background(), in)
assert.NotNil(t, err)
assert.Nil(t, out)
}
//func TestListTestPlugins(t *testing.T) {
// addPlugin := rc.Calls.Get("pluginsctl/listTestPlugins")
// assert.NotNil(t, addPlugin)
// in := rc.Params{}
// out, err := addPlugin.Fn(context.Background(), in)
// assert.Nil(t, err)
// expected := rc.Params{
// "loadedTestPlugins": map[string]PackageJSON{},
// }
// assert.Equal(t, expected, out)
//}
//func TestRemoveTestPlugin(t *testing.T) {
// addPlugin := rc.Calls.Get("pluginsctl/removeTestPlugin")
// assert.NotNil(t, addPlugin)
// in := rc.Params{
// "name": "",
// }
// out, err := addPlugin.Fn(context.Background(), in)
// assert.NotNil(t, err)
// assert.Nil(t, out)
//}
func TestAddPlugin(t *testing.T) {
setCacheDir(t)
addPlugin(t)
_, ok := loadedPlugins.LoadedPlugins[testPluginKey]
assert.True(t, ok)
//removePlugin(t)
//_, ok = loadedPlugins.LoadedPlugins[testPluginKey]
//assert.False(t, ok)
}
func TestListPlugins(t *testing.T) {
setCacheDir(t)
addPlugin := rc.Calls.Get("pluginsctl/listPlugins")
assert.NotNil(t, addPlugin)
in := rc.Params{}
out, err := addPlugin.Fn(context.Background(), in)
assert.Nil(t, err)
expected := rc.Params{
"loadedPlugins": map[string]PackageJSON{},
"loadedTestPlugins": map[string]PackageJSON{},
}
assert.Equal(t, expected, out)
}
func TestRemovePlugin(t *testing.T) {
setCacheDir(t)
addPlugin(t)
removePluginCall := rc.Calls.Get("pluginsctl/removePlugin")
assert.NotNil(t, removePlugin)
in := rc.Params{
"name": testPluginKey,
}
out, err := removePluginCall.Fn(context.Background(), in)
assert.Nil(t, err)
assert.Nil(t, out)
removePlugin(t)
assert.Equal(t, len(loadedPlugins.LoadedPlugins), 0)
}
func TestPluginsForType(t *testing.T) {
addPlugin := rc.Calls.Get("pluginsctl/getPluginsForType")
assert.NotNil(t, addPlugin)
in := rc.Params{
"type": "",
"pluginType": "FileHandler",
}
out, err := addPlugin.Fn(context.Background(), in)
assert.Nil(t, err)
assert.NotNil(t, out)
in = rc.Params{
"type": "video/mp4",
"pluginType": "",
}
_, err = addPlugin.Fn(context.Background(), in)
assert.Nil(t, err)
assert.NotNil(t, out)
}
-278
View File
@@ -1,278 +0,0 @@
// Package webgui defines the Web GUI helpers.
package webgui
import (
"archive/zip"
"encoding/json"
"errors"
"fmt"
"io"
"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 := os.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 = os.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"`
}