diff --git a/fs/rc/rc.go b/fs/rc/rc.go index a418c4921..ad3d4f01f 100644 --- a/fs/rc/rc.go +++ b/fs/rc/rc.go @@ -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"` diff --git a/fs/rc/rcserver/rcserver.go b/fs/rc/rcserver/rcserver.go index ba7e957ba..e95aa3bc4 100644 --- a/fs/rc/rcserver/rcserver.go +++ b/fs/rc/rcserver/rcserver.go @@ -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) diff --git a/fs/rc/webgui/plugins.go b/fs/rc/webgui/plugins.go deleted file mode 100644 index 29bf8c1f7..000000000 --- a/fs/rc/webgui/plugins.go +++ /dev/null @@ -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 -} diff --git a/fs/rc/webgui/rc.go b/fs/rc/webgui/rc.go deleted file mode 100644 index 66600174a..000000000 --- a/fs/rc/webgui/rc.go +++ /dev/null @@ -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//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 - -} diff --git a/fs/rc/webgui/rc_test.go b/fs/rc/webgui/rc_test.go deleted file mode 100644 index 57c5c4a09..000000000 --- a/fs/rc/webgui/rc_test.go +++ /dev/null @@ -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) -} diff --git a/fs/rc/webgui/webgui.go b/fs/rc/webgui/webgui.go deleted file mode 100644 index bcc231d3a..000000000 --- a/fs/rc/webgui/webgui.go +++ /dev/null @@ -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"` -}