You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

255 lines
6.7 KiB

/*
* GodoOS - A lightweight cloud desktop
* Copyright (C) 2024 https://godoos.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package store
import (
"encoding/json"
"fmt"
"godo/files"
"godo/libs"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"time"
"github.com/cavaliergopher/grab/v3"
)
const (
concurrency = 6 // 并发下载数
)
var downloads = make(map[string]*grab.Response)
var downloadsMutex sync.Mutex
type FileProgress struct {
Progress int `json:"progress"` // 将进度改为浮点数,以百分比表示
IsFinished bool `json:"is_finished"`
Total int64 `json:"total"`
Current int64 `json:"completed"`
Status string `json:"status"`
}
type ReqBody struct {
DownloadUrl string `json:"url"`
PkgUrl string `json:"pkg"`
Name string `json:"name"`
}
func DownloadHandler(w http.ResponseWriter, r *http.Request) {
var reqBody ReqBody
err := json.NewDecoder(r.Body).Decode(&reqBody)
if err != nil {
libs.ErrorMsg(w, "first Decode request body error")
return
}
downloadDir := libs.GetCacheDir()
if downloadDir == "" {
downloadDir = "./downloads"
}
if reqBody.Name == "" {
libs.ErrorMsg(w, "the name is empty")
return
}
if reqBody.DownloadUrl == "" && reqBody.PkgUrl == "" {
libs.ErrorMsg(w, "the url is empty")
return
}
paths := []string{}
for _, url := range []string{reqBody.DownloadUrl, reqBody.PkgUrl} {
if url == "" {
continue
}
if rsp, ok := downloads[url]; ok {
// 如果URL正在下载,跳过创建新的下载器实例
go trackProgress(w, rsp, url)
continue
}
// 创建新的下载器实例
client := grab.NewClient()
client.HTTPClient = &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: concurrency, // 可选,设置并发连接数
},
}
filePath := filepath.Join(downloadDir, filepath.Base(url))
paths = append(paths, filePath)
log.Printf("filePath is %s", filePath)
// 创建下载请求
req, err := grab.NewRequest(filePath, url)
if err != nil {
libs.ErrorMsg(w, "Invalid download URL")
return
}
resp := client.Do(req)
if fileInfo, err := os.Stat(filePath); err == nil {
// 文件存在,检查文件大小是否与远程资源匹配
if fileInfo.Size() == resp.Size() { // 这里的req.Size需要从下载请求中获取,或通过其他方式预知
log.Printf("File %s already exists and is up-to-date.", filePath)
continue
}
}
downloads[url] = resp
//log.Printf("Download urls: %v\n", reqBody.DownloadUrl)
// // 跟踪进度
go trackProgress(w, resp, url)
// 等待下载完成并检查错误
if err := resp.Err(); err != nil {
libs.ErrorMsg(w, "Download failed")
return
}
}
if len(paths) > 0 {
// 解压文件
err := HandlerZipFiles(paths, reqBody.Name)
if err != nil {
libs.ErrorMsg(w, "Decompress failed:"+err.Error())
return
}
}
installInfo, err := Installation(reqBody.Name)
if err != nil {
libs.ErrorData(w, installInfo, "the install.json is error:"+err.Error())
return
}
libs.SuccessMsg(w, installInfo, "install the app success!")
}
func HandlerZipFiles(paths []string, name string) error {
runDir := libs.GetRunDir()
targetDir := filepath.Join(runDir, name)
downloadDir := libs.GetCacheDir()
for _, filePath := range paths {
zipPath, err := files.Decompress(filePath, downloadDir)
if err != nil {
return fmt.Errorf("decompress failed: %v", err)
}
err = files.CopyResource(zipPath, targetDir)
if err != nil {
return fmt.Errorf("copyResource failed")
}
err = os.RemoveAll(zipPath)
if err != nil {
return fmt.Errorf("removeAll failed")
}
}
return nil
}
func trackProgress(w http.ResponseWriter, resp *grab.Response, md5url string) {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered panic in trackProgress: %v", r)
}
downloadsMutex.Lock()
defer downloadsMutex.Unlock()
delete(downloads, md5url)
}()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
flusher, ok := w.(http.Flusher)
if !ok {
log.Printf("Streaming unsupported")
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
for {
select {
case <-ticker.C:
fp := FileProgress{
Progress: int(resp.Progress() * 100),
IsFinished: resp.IsComplete(),
Total: resp.Size(),
Current: resp.BytesComplete(),
Status: "loading",
}
//log.Printf("Progress: %v", fp)
if resp.IsComplete() && fp.Current == fp.Total {
fp.Status = "loaded"
}
jsonBytes, err := json.Marshal(fp)
if err != nil {
log.Printf("Error marshaling FileProgress to JSON: %v", err)
continue
}
if w != nil {
io.WriteString(w, string(jsonBytes))
w.Write([]byte("\n"))
flusher.Flush()
} else {
log.Println("ResponseWriter is nil, cannot send progress")
}
if fp.Status == "loaded" {
return
}
}
}
}
// Predefined filename for the icon
const iconFilename = "icon.png"
// DownloadIcon downloads the icon from the given URL to the target path with a predefined filename.
func DownloadIcon(url, iconTargetPath string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
out, err := os.Create(iconTargetPath)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, resp.Body); err != nil {
return err
}
return nil
}
// HandlerIcon handles the icon by downloading it and updating the installInfo.
func HandlerIcon(installInfo InstallInfo, targetPath string) (string, error) {
var iconUrl string
if url, err := url.Parse(installInfo.Icon); err == nil && url.Scheme != "" {
// Download the icon using the predefined filename
iconTargetPath := filepath.Join(targetPath, installInfo.Name, "_icon.png")
if err := DownloadIcon(installInfo.Icon, iconTargetPath); err != nil {
return "", fmt.Errorf("error downloading icon: %v", err)
}
iconUrl = "http://localhost:56780/static/" + installInfo.Name + "/" + installInfo.Name + "_icon.png"
} else {
iconUrl = "http://localhost:56780/static/" + installInfo.Name + "/" + installInfo.Icon
}
return iconUrl, nil
}