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 }