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:
+15
-10
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user