github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/fs/rc/webgui.go (about)

     1  // Define the Web GUI helpers
     2  
     3  package rc
     4  
     5  import (
     6  	"archive/zip"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/rclone/rclone/fs"
    19  	"github.com/rclone/rclone/lib/errors"
    20  )
    21  
    22  // getLatestReleaseURL returns the latest release details of the rclone-webui-react
    23  func getLatestReleaseURL(fetchURL string) (string, string, int, error) {
    24  	resp, err := http.Get(fetchURL)
    25  	if err != nil {
    26  		return "", "", 0, errors.New("Error getting latest release of rclone-webui")
    27  	}
    28  	results := gitHubRequest{}
    29  	if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
    30  		return "", "", 0, errors.New("Could not decode results from http request")
    31  	}
    32  
    33  	res := results.Assets[0].BrowserDownloadURL
    34  	tag := results.TagName
    35  	size := results.Assets[0].Size
    36  
    37  	return res, tag, size, nil
    38  }
    39  
    40  // CheckAndDownloadWebGUIRelease is a helper function to download and setup latest release of rclone-webui-react
    41  func CheckAndDownloadWebGUIRelease(checkUpdate bool, forceUpdate bool, fetchURL string, cacheDir string) (err error) {
    42  	cachePath := filepath.Join(cacheDir, "webgui")
    43  	tagPath := filepath.Join(cachePath, "tag")
    44  	extractPath := filepath.Join(cachePath, "current")
    45  
    46  	extractPathExist, extractPathStat, err := exists(extractPath)
    47  
    48  	if extractPathExist && !extractPathStat.IsDir() {
    49  		return errors.New("Web GUI path exists, but is a file instead of folder. Please check the path " + extractPath)
    50  	}
    51  
    52  	// if the old file exists does not exist or forced update is enforced.
    53  	// TODO: Add hashing to check integrity of the previous update.
    54  	if !extractPathExist || checkUpdate || forceUpdate {
    55  		// Get the latest release details
    56  		WebUIURL, tag, size, err := getLatestReleaseURL(fetchURL)
    57  		if err != nil {
    58  			return err
    59  		}
    60  
    61  		dat, err := ioutil.ReadFile(tagPath)
    62  		if err == nil && string(dat) == tag {
    63  			fs.Logf(nil, "No update to Web GUI available.")
    64  			if !forceUpdate {
    65  				return nil
    66  			}
    67  			fs.Logf(nil, "Force update the Web GUI binary.")
    68  		}
    69  
    70  		zipName := tag + ".zip"
    71  		zipPath := filepath.Join(cachePath, zipName)
    72  
    73  		cachePathExist, cachePathStat, _ := exists(cachePath)
    74  		if !cachePathExist {
    75  			if err := os.MkdirAll(cachePath, 0755); err != nil {
    76  				return errors.New("Error creating cache directory: " + cachePath)
    77  			}
    78  		}
    79  
    80  		if cachePathExist && !cachePathStat.IsDir() {
    81  			return errors.New("Web GUI path is a file instead of folder. Please check it " + extractPath)
    82  		}
    83  
    84  		fs.Logf(nil, "A new release for gui is present at "+WebUIURL)
    85  		fs.Logf(nil, "Downloading webgui binary. Please wait. [Size: %s, Path :  %s]\n", strconv.Itoa(size), zipPath)
    86  
    87  		// download the zip from latest url
    88  		err = downloadFile(zipPath, WebUIURL)
    89  		if err != nil {
    90  			return err
    91  		}
    92  
    93  		err = os.RemoveAll(extractPath)
    94  		if err != nil {
    95  			fs.Logf(nil, "No previous downloads to remove")
    96  		}
    97  		fs.Logf(nil, "Unzipping webgui binary")
    98  
    99  		err = unzip(zipPath, extractPath)
   100  		if err != nil {
   101  			return err
   102  		}
   103  
   104  		err = os.RemoveAll(zipPath)
   105  		if err != nil {
   106  			fs.Logf(nil, "Downloaded ZIP cannot be deleted")
   107  		}
   108  
   109  		err = ioutil.WriteFile(tagPath, []byte(tag), 0644)
   110  		if err != nil {
   111  			fs.Infof(nil, "Cannot write tag file. You may be required to redownload the binary next time.")
   112  		}
   113  	} else {
   114  		fs.Logf(nil, "Web GUI exists. Update skipped.")
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  // downloadFile is a helper function to download a file from url to the filepath
   121  func downloadFile(filepath string, url string) error {
   122  
   123  	// Get the data
   124  	resp, err := http.Get(url)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	defer fs.CheckClose(resp.Body, &err)
   129  
   130  	// Create the file
   131  	out, err := os.Create(filepath)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	defer fs.CheckClose(out, &err)
   136  
   137  	// Write the body to file
   138  	_, err = io.Copy(out, resp.Body)
   139  	return err
   140  }
   141  
   142  // unzip is a helper function to unzip a file specified in src to path dest
   143  func unzip(src, dest string) (err error) {
   144  	dest = filepath.Clean(dest) + string(os.PathSeparator)
   145  
   146  	r, err := zip.OpenReader(src)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	defer fs.CheckClose(r, &err)
   151  
   152  	if err := os.MkdirAll(dest, 0755); err != nil {
   153  		return err
   154  	}
   155  
   156  	// Closure to address file descriptors issue with all the deferred .Close() methods
   157  	extractAndWriteFile := func(f *zip.File) error {
   158  		path := filepath.Join(dest, f.Name)
   159  		// Check for Zip Slip: https://github.com/rclone/rclone/issues/3529
   160  		if !strings.HasPrefix(path, dest) {
   161  			return fmt.Errorf("%s: illegal file path", path)
   162  		}
   163  
   164  		rc, err := f.Open()
   165  		if err != nil {
   166  			return err
   167  		}
   168  		defer fs.CheckClose(rc, &err)
   169  
   170  		if f.FileInfo().IsDir() {
   171  			if err := os.MkdirAll(path, 0755); err != nil {
   172  				return err
   173  			}
   174  		} else {
   175  			if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
   176  				return err
   177  			}
   178  			f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
   179  			if err != nil {
   180  				return err
   181  			}
   182  			defer fs.CheckClose(f, &err)
   183  
   184  			_, err = io.Copy(f, rc)
   185  			if err != nil {
   186  				return err
   187  			}
   188  		}
   189  		return nil
   190  	}
   191  
   192  	for _, f := range r.File {
   193  		err := extractAndWriteFile(f)
   194  		if err != nil {
   195  			return err
   196  		}
   197  	}
   198  
   199  	return nil
   200  }
   201  
   202  func exists(path string) (existance bool, stat os.FileInfo, err error) {
   203  	stat, err = os.Stat(path)
   204  	if err == nil {
   205  		return true, stat, nil
   206  	}
   207  	if os.IsNotExist(err) {
   208  		return false, nil, nil
   209  	}
   210  	return false, stat, err
   211  }
   212  
   213  // gitHubRequest Maps the GitHub API request to structure
   214  type gitHubRequest struct {
   215  	URL string `json:"url"`
   216  
   217  	Prerelease  bool      `json:"prerelease"`
   218  	CreatedAt   time.Time `json:"created_at"`
   219  	PublishedAt time.Time `json:"published_at"`
   220  	TagName     string    `json:"tag_name"`
   221  	Assets      []struct {
   222  		URL                string    `json:"url"`
   223  		ID                 int       `json:"id"`
   224  		NodeID             string    `json:"node_id"`
   225  		Name               string    `json:"name"`
   226  		Label              string    `json:"label"`
   227  		ContentType        string    `json:"content_type"`
   228  		State              string    `json:"state"`
   229  		Size               int       `json:"size"`
   230  		DownloadCount      int       `json:"download_count"`
   231  		CreatedAt          time.Time `json:"created_at"`
   232  		UpdatedAt          time.Time `json:"updated_at"`
   233  		BrowserDownloadURL string    `json:"browser_download_url"`
   234  	} `json:"assets"`
   235  	TarballURL string `json:"tarball_url"`
   236  	ZipballURL string `json:"zipball_url"`
   237  	Body       string `json:"body"`
   238  }