diff --git a/go.mod b/go.mod
index 32c772ca..0fe91ce1 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
github.com/jackc/pgx/v5 v5.5.2
github.com/labstack/echo-contrib v0.15.0
github.com/labstack/echo/v4 v4.11.4
+ github.com/labstack/gommon v0.4.2
github.com/lib/pq v1.10.9
github.com/rosedblabs/rosedb/v2 v2.3.4
github.com/stretchr/testify v1.8.4
@@ -53,7 +54,6 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
- github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
diff --git a/handler/app/web.go b/handler/app/web.go
index 9f2e0d70..6ead6fb9 100644
--- a/handler/app/web.go
+++ b/handler/app/web.go
@@ -33,14 +33,15 @@ func EditDownloadGET(filename, filesize, demozoo any) template.HTML {
return ""
}
}
+ var zooID int64
if val, ok := demozoo.(null.Int64); ok {
if !val.Valid || val.Int64 == 0 {
return ""
}
+ zooID = val.Int64
}
- // zoo := demozoo.(null.Int64).Int64
- // fmt.Sprintf("%d", zoo)
- return template.HTML(`GET a remote download`)
+ s := fmt.Sprintf("GET a remote download", zooID)
+ return template.HTML(s)
}
// Web is the configuration and status of the web app.
diff --git a/handler/router.go b/handler/router.go
index 00c94576..b14be504 100644
--- a/handler/router.go
+++ b/handler/router.go
@@ -7,14 +7,20 @@ import (
"embed"
"fmt"
"net/http"
+ "os"
+ "path/filepath"
+ "strconv"
"strings"
"github.com/Defacto2/releaser"
"github.com/Defacto2/server/handler/app"
"github.com/Defacto2/server/internal/config"
+ "github.com/Defacto2/server/internal/helper"
+ "github.com/Defacto2/server/internal/zoo"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
+ "github.com/labstack/gommon/log"
"go.uber.org/zap"
)
@@ -271,9 +277,47 @@ func (c Configuration) Routes(z *zap.SugaredLogger, e *echo.Echo, public embed.F
// Editor pages to update the database records.
editor := e.Group("/editor")
editor.Use(c.ReadOnlyLock, c.SessionLock)
+
editor.GET("/new-for-approval", func(x echo.Context) error {
return app.FilesWaiting(z, x, "1")
})
+ editor.GET("/new-for-approval/:id", func(x echo.Context) error {
+ // todo, placeholder
+ sid := x.Param("id")
+ id, err := strconv.Atoi(sid)
+ if err != nil {
+ return app.StatusErr(z, x, http.StatusNotFound, sid)
+ }
+ dz := zoo.Demozoo{}
+ if err = dz.Get(id); err != nil {
+ return app.StatusErr(z, x, http.StatusNotFound, sid)
+ }
+ var file string
+ for _, link := range dz.DownloadLinks {
+ if link.URL != "" {
+ file, err = helper.DownloadFile(link.URL)
+ if err != nil {
+ log.Debug(err)
+ continue
+ }
+ if file != "" {
+ base := filepath.Base(link.URL)
+ home, _ := os.UserHomeDir()
+ dst := filepath.Join(home, base)
+ if err = helper.RenameFile(file, dst); err != nil {
+ return app.StatusErr(z, x, http.StatusInternalServerError, sid)
+ }
+ return x.String(http.StatusOK,
+ fmt.Sprintf("File downloaded to %s", dst))
+ }
+ }
+ }
+ if file == "" {
+ return app.StatusErr(z, x, http.StatusNotFound, sid)
+ }
+ return x.JSON(http.StatusOK, dz)
+ })
+
online := editor.Group("/online")
online.POST("/true", func(x echo.Context) error {
return app.RecordToggle(z, x, true)
diff --git a/internal/helper/get.go b/internal/helper/get.go
new file mode 100644
index 00000000..ff0b1187
--- /dev/null
+++ b/internal/helper/get.go
@@ -0,0 +1,106 @@
+package helper
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "time"
+)
+
+const (
+ // Timeout is the HTTP client timeout.
+ Timeout = 5 * time.Second
+ // User-Agent to send with the HTTP request.
+ UserAgent = "Defacto2 2024 app under construction (thanks!)"
+)
+
+// DownloadFile downloads a file from a remote URL and saves it to the default temp directory.
+// It returns the path to the downloaded file.
+func DownloadFile(url string) (string, error) {
+ // Get the remote file
+ client := http.Client{
+ Timeout: Timeout,
+ }
+ ctx := context.Background()
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("User-Agent", UserAgent)
+ res, err := client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer res.Body.Close()
+
+ // Create the file in the default temp directory
+ tmpFile, err := os.CreateTemp("", "downloadfile-*")
+ if err != nil {
+ return "", err
+ }
+ defer tmpFile.Close()
+
+ // Write the body to file
+ if _, err := io.Copy(tmpFile, res.Body); err != nil {
+ defer os.Remove(tmpFile.Name())
+ return "", err
+ }
+ return tmpFile.Name(), nil
+}
+
+// RenameFile renames a file from oldpath to newpath.
+// It returns an error if the oldpath does not exist or is a directory,
+// newpath already exists, or the rename fails.
+func RenameFile(oldpath, newpath string) error {
+ st, err := os.Stat(oldpath)
+ if err != nil {
+ return err
+ }
+ if st.IsDir() {
+ return fmt.Errorf("oldpath %w: %s", ErrFilePath, oldpath)
+ }
+ if _, err = os.Stat(newpath); err == nil {
+ return fmt.Errorf("newpath %w: %s", ErrExistPath, newpath)
+ }
+ if err := os.Rename(oldpath, newpath); err != nil {
+ var linkErr *os.LinkError
+ if errors.As(err, &linkErr) && linkErr.Err.Error() == "invalid cross-device link" {
+ return RenameCrossDevice(oldpath, newpath)
+ }
+ return err
+ }
+ return nil
+}
+
+// RenameCrossDevice is a workaround for renaming files across different devices.
+// A cross device can also be a different file system such as a Docker volume.
+func RenameCrossDevice(oldpath, newpath string) error {
+ src, err := os.Open(oldpath)
+ if err != nil {
+ return err
+ }
+ defer src.Close()
+ dst, err := os.Create(newpath)
+ if err != nil {
+ return err
+ }
+ defer dst.Close()
+
+ if _, err = io.Copy(dst, src); err != nil {
+ return err
+ }
+ fi, err := os.Stat(oldpath)
+ if err != nil {
+ defer os.Remove(newpath)
+ return err
+ }
+ if err = os.Chmod(newpath, fi.Mode()); err != nil {
+ defer os.Remove(newpath)
+ return err
+ }
+ defer os.Remove(oldpath)
+ return nil
+}
diff --git a/internal/helper/helper.go b/internal/helper/helper.go
index 13351bdf..75db676d 100644
--- a/internal/helper/helper.go
+++ b/internal/helper/helper.go
@@ -5,6 +5,7 @@ import (
"crypto/sha512"
"embed"
"encoding/base64"
+ "errors"
"fmt"
"net"
"os"
@@ -19,6 +20,11 @@ const (
byteUnits = "kMGTPE"
)
+var (
+ ErrFilePath = errors.New("file path is a directory")
+ ErrExistPath = errors.New("path ready exists and will not overwrite")
+)
+
// GetLocalIPs returns a list of local IP addresses.
// credit: https://gosamples.dev/local-ip-address/
func GetLocalIPs() ([]net.IP, error) {
diff --git a/internal/pouet/pouet.go b/internal/pouet/pouet.go
index e0ad184d..ea22f5b2 100644
--- a/internal/pouet/pouet.go
+++ b/internal/pouet/pouet.go
@@ -12,6 +12,8 @@ import (
"strconv"
"strings"
"time"
+
+ "github.com/Defacto2/server/internal/helper"
)
const (
@@ -21,8 +23,6 @@ const (
Timeout = 5 * time.Second
// StarRounder is the rounding value for the stars rating.
StarRounder = 0.5
- // User-Agent to send with the HTTP request.
- UserAgent = "Defacto2 2024 app, under construction (thanks!)"
// firstID is the first production ID on Pouet.
firstID = 1
)
@@ -191,7 +191,7 @@ func (r *Response) Get(id int) error {
if err != nil {
return err
}
- req.Header.Set("User-Agent", UserAgent)
+ req.Header.Set("User-Agent", helper.UserAgent)
res, err := client.Do(req)
if err != nil {
return err
diff --git a/internal/zoo/zoo.go b/internal/zoo/zoo.go
index 9f16ca30..3e83f5ed 100644
--- a/internal/zoo/zoo.go
+++ b/internal/zoo/zoo.go
@@ -11,6 +11,8 @@ import (
"net/http"
"strconv"
"time"
+
+ "github.com/Defacto2/server/internal/helper"
)
const (
@@ -43,6 +45,11 @@ type Demozoo struct {
Name string `json:"name"`
ID int `json:"id"`
} `json:"types"`
+ // Download links to the remotely hosted files.
+ DownloadLinks []struct {
+ LinkClass string `json:"link_class"`
+ URL string `json:"url"`
+ } `json:"download_links"`
// ID is the production ID.
ID int `json:"id"`
}
@@ -67,8 +74,7 @@ func (d *Demozoo) Get(id int) error {
if err != nil {
return err
}
- req.Header.Set("User-Agent",
- "Defacto2 2023 app under construction (thanks!)")
+ req.Header.Set("User-Agent", helper.UserAgent)
res, err := client.Do(req)
if err != nil {
return err