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 }