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.
 
 
 
 
 
 

260 lines
7.1 KiB

// MIT License
//
// Copyright (c) 2024 godoos.com
// Email: xpbb@qq.com
// GitHub: github.com/phpk/godoos
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
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
}