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 }