231 lines
5.2 KiB
Go
231 lines
5.2 KiB
Go
package download
|
|
|
|
import (
|
|
"NanoKVM-Server/proto"
|
|
"fmt"
|
|
"github.com/gin-gonic/gin"
|
|
log "github.com/sirupsen/logrus"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Service struct{}
|
|
|
|
var sentinelPath = "/tmp/.download_in_progress"
|
|
|
|
func NewService() *Service {
|
|
// Clear sentinel
|
|
// If we are starting from scratch, we need to remove the sentinel file as any downloads at this point are done or broken
|
|
_ = os.Remove(sentinelPath)
|
|
return &Service{}
|
|
}
|
|
|
|
func (s *Service) ImageEnabled(c *gin.Context) {
|
|
var rsp proto.Response
|
|
|
|
// Check if /data mount is RO/RW
|
|
testFile := "/data/.testfile"
|
|
file, err := os.Create(testFile)
|
|
defer file.Close()
|
|
defer os.Remove(testFile)
|
|
if err != nil {
|
|
if os.IsPermission(err) {
|
|
rsp.OkRspWithData(c, &proto.ImageEnabledRsp{
|
|
Enabled: false,
|
|
})
|
|
return
|
|
}
|
|
rsp.OkRspWithData(c, &proto.ImageEnabledRsp{
|
|
Enabled: false,
|
|
})
|
|
return
|
|
}
|
|
|
|
rsp.OkRspWithData(c, &proto.ImageEnabledRsp{
|
|
Enabled: true,
|
|
})
|
|
}
|
|
|
|
func (s *Service) StatusImage(c *gin.Context) {
|
|
var rsp proto.Response
|
|
|
|
// Check if the sentinel file exists
|
|
log.Debug("StatusImage")
|
|
if _, err := os.Stat(sentinelPath); err == nil {
|
|
content, err := os.ReadFile(sentinelPath)
|
|
if err != nil {
|
|
log.Error("Failed to read sentinel file")
|
|
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
|
Status: "in_progress",
|
|
File: "",
|
|
Percentage: "",
|
|
})
|
|
return
|
|
}
|
|
splitted := strings.Split(string(content), ";")
|
|
if len(splitted) == 1 {
|
|
// No percentage, just the URL
|
|
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
|
Status: "in_progress",
|
|
File: splitted[0],
|
|
Percentage: "",
|
|
})
|
|
} else {
|
|
// Percentage is available
|
|
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
|
Status: "in_progress",
|
|
File: splitted[0],
|
|
Percentage: splitted[1],
|
|
})
|
|
}
|
|
|
|
return
|
|
}
|
|
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
|
Status: "idle",
|
|
File: "",
|
|
Percentage: "",
|
|
})
|
|
}
|
|
|
|
func (s *Service) DownloadImage(c *gin.Context) {
|
|
var req proto.MountImageReq
|
|
var rsp proto.Response
|
|
|
|
log.Debug("DownloadImage")
|
|
|
|
if err := proto.ParseFormRequest(c, &req); err != nil {
|
|
rsp.ErrRsp(c, -1, "invalid arguments")
|
|
return
|
|
}
|
|
|
|
if req.File == "" {
|
|
rsp.ErrRsp(c, -1, "invalid arguments")
|
|
return
|
|
}
|
|
// Parse the URI to see if its valid http/s
|
|
u, err := url.Parse(req.File)
|
|
if err != nil || u.Scheme == "" || u.Host == "" {
|
|
rsp.ErrRsp(c, -1, "invalid url")
|
|
return
|
|
}
|
|
|
|
// Set a sentinel file to mark that there is a download in progress
|
|
// This is to prevent multiple downloads at the same time
|
|
if _, err := os.Stat(sentinelPath); err == nil {
|
|
log.Debug("Download in progress")
|
|
rsp.ErrRsp(c, -1, "download in progress")
|
|
return
|
|
}
|
|
// Create the sentinel file
|
|
err = os.WriteFile(sentinelPath, []byte(req.File), 0644)
|
|
if err != nil {
|
|
log.Error("Failed to create sentinel file")
|
|
rsp.ErrRsp(c, -1, "failed to create sentinel file")
|
|
return
|
|
}
|
|
|
|
// Check if it actually exists and fail if it doesn't
|
|
resp, err := http.Head(req.File)
|
|
if resp.StatusCode != http.StatusOK || err != nil {
|
|
rsp.ErrRsp(c, resp.StatusCode, "failed when checking the url")
|
|
log.Error("Failed to check the URL")
|
|
defer os.Remove(sentinelPath)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Download the image in a goroutine to not block the request
|
|
go func() {
|
|
defer os.Remove(sentinelPath)
|
|
resp, err = http.Get(req.File)
|
|
if err != nil {
|
|
log.Error("Failed to download the file")
|
|
rsp.ErrRsp(c, -1, "failed to download the file")
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
// Create the destination file
|
|
destPath := filepath.Join("/data", filepath.Base(u.Path))
|
|
out, err := os.Create(destPath)
|
|
if err != nil {
|
|
log.Error("Failed to create destination file")
|
|
rsp.ErrRsp(c, -1, "failed to create destination file")
|
|
return
|
|
}
|
|
defer out.Close()
|
|
|
|
lw := &loggingWriter{writer: out, totalSize: resp.ContentLength}
|
|
lw.startTicker()
|
|
_, err = io.Copy(lw, resp.Body)
|
|
if err != nil {
|
|
log.Error("Failed to save the file")
|
|
rsp.ErrRsp(c, -1, "failed to save the file")
|
|
lw.stopTicker()
|
|
return
|
|
}
|
|
lw.stopTicker()
|
|
}()
|
|
rsp.OkRspWithData(c, &proto.StatusImageRsp{
|
|
Status: "in_progress",
|
|
File: req.File,
|
|
Percentage: "",
|
|
})
|
|
}
|
|
|
|
type loggingWriter struct {
|
|
writer io.Writer
|
|
total int64
|
|
totalSize int64
|
|
ticker *time.Ticker
|
|
done chan bool
|
|
}
|
|
|
|
func (lw *loggingWriter) startTicker() {
|
|
lw.ticker = time.NewTicker(2500 * time.Millisecond)
|
|
lw.done = make(chan bool)
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-lw.done:
|
|
return
|
|
case <-lw.ticker.C:
|
|
lw.updateSentinel()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (lw *loggingWriter) stopTicker() {
|
|
lw.ticker.Stop()
|
|
lw.done <- true
|
|
}
|
|
|
|
func (lw *loggingWriter) updateSentinel() {
|
|
percentage := float64(lw.total) / float64(lw.totalSize) * 100
|
|
content, err := os.ReadFile(sentinelPath)
|
|
if err != nil {
|
|
log.Error("Failed to read sentinel file")
|
|
return
|
|
}
|
|
splitted := strings.Split(string(content), ";")
|
|
if len(splitted) == 0 {
|
|
return
|
|
}
|
|
err = os.WriteFile(sentinelPath, []byte(fmt.Sprintf("%s;%.2f%%", splitted[0], percentage)), 0644)
|
|
if err != nil {
|
|
log.Error("Failed to update sentinel file")
|
|
}
|
|
}
|
|
|
|
func (lw *loggingWriter) Write(p []byte) (int, error) {
|
|
n, err := lw.writer.Write(p)
|
|
lw.total += int64(n)
|
|
return n, err
|
|
}
|