github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/rc/webgui/webgui.go (about)

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