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