Session serialization will use gob encoding instead of json. Binary size will likely be reduced. General backend code refactoring.
213 lines
4.4 KiB
Go
213 lines
4.4 KiB
Go
package rest
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/marcopeocchi/yt-dlp-web-ui/server/config"
|
|
"github.com/marcopeocchi/yt-dlp-web-ui/server/utils"
|
|
)
|
|
|
|
const (
|
|
TOKEN_COOKIE_NAME = "jwt"
|
|
)
|
|
|
|
type DirectoryEntry struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
SHASum string `json:"shaSum"`
|
|
ModTime time.Time `json:"modTime"`
|
|
IsVideo bool `json:"isVideo"`
|
|
IsDirectory bool `json:"isDirectory"`
|
|
}
|
|
|
|
func walkDir(root string) (*[]DirectoryEntry, error) {
|
|
files := []DirectoryEntry{}
|
|
|
|
dirs, err := os.ReadDir(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, d := range dirs {
|
|
if !utils.IsValidEntry(d) {
|
|
continue
|
|
}
|
|
|
|
path := filepath.Join(root, d.Name())
|
|
|
|
info, err := d.Info()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
files = append(files, DirectoryEntry{
|
|
Path: path,
|
|
Name: d.Name(),
|
|
Size: info.Size(),
|
|
SHASum: utils.ShaSumString(path),
|
|
IsVideo: utils.IsVideo(d),
|
|
IsDirectory: d.IsDir(),
|
|
ModTime: info.ModTime(),
|
|
})
|
|
}
|
|
|
|
return &files, err
|
|
}
|
|
|
|
type ListRequest struct {
|
|
SubDir string `json:"subdir"`
|
|
OrderBy string `json:"orderBy"`
|
|
}
|
|
|
|
func ListDownloaded(w http.ResponseWriter, r *http.Request) {
|
|
root := config.Instance().GetConfig().DownloadPath
|
|
req := new(ListRequest)
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
files, err := walkDir(filepath.Join(root, req.SubDir))
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.OrderBy == "modtime" {
|
|
sort.SliceStable(*files, func(i, j int) bool {
|
|
return (*files)[i].ModTime.After((*files)[j].ModTime)
|
|
})
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
err = json.NewEncoder(w).Encode(files)
|
|
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
type DeleteRequest = DirectoryEntry
|
|
|
|
func DeleteFile(w http.ResponseWriter, r *http.Request) {
|
|
req := new(DeleteRequest)
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
sum := utils.ShaSumString(req.Path)
|
|
if sum != req.SHASum {
|
|
http.Error(w, "shasum mismatch", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
err = os.Remove(req.Path)
|
|
if err != nil {
|
|
http.Error(w, "shasum mismatch", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode("ok")
|
|
}
|
|
|
|
func SendFile(w http.ResponseWriter, r *http.Request) {
|
|
path := chi.URLParam(r, "id")
|
|
|
|
if path == "" {
|
|
http.Error(w, "inexistent path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
decoded, err := hex.DecodeString(path)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
decodedStr := string(decoded)
|
|
|
|
root := config.Instance().GetConfig().DownloadPath
|
|
|
|
// TODO: further path / file validations
|
|
if strings.Contains(filepath.Dir(decodedStr), root) {
|
|
// ctx.Response().Header.Set(
|
|
// "Content-Disposition",
|
|
// "inline; filename="+filepath.Base(decodedStr),
|
|
// )
|
|
|
|
http.ServeFile(w, r, decodedStr)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}
|
|
|
|
type LoginRequest struct {
|
|
Secret string `json:"secret"`
|
|
}
|
|
|
|
func Login(w http.ResponseWriter, r *http.Request) {
|
|
req := new(LoginRequest)
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if config.Instance().GetConfig().RPCSecret != req.Secret {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
expiresAt := time.Now().Add(time.Hour * 24 * 30)
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"expiresAt": expiresAt,
|
|
})
|
|
|
|
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
cookie := &http.Cookie{
|
|
Name: TOKEN_COOKIE_NAME,
|
|
HttpOnly: true,
|
|
Secure: false,
|
|
Expires: expiresAt, // 30 days
|
|
Value: tokenString,
|
|
Path: "/",
|
|
}
|
|
|
|
http.SetCookie(w, cookie)
|
|
}
|
|
|
|
func Logout(w http.ResponseWriter, r *http.Request) {
|
|
cookie := &http.Cookie{
|
|
Name: TOKEN_COOKIE_NAME,
|
|
HttpOnly: true,
|
|
Secure: false,
|
|
Expires: time.Now(),
|
|
Value: "",
|
|
Path: "/",
|
|
}
|
|
|
|
http.SetCookie(w, cookie)
|
|
}
|