253 lines
5.4 KiB
Go
253 lines
5.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/config"
|
|
"github.com/marcopeocchi/yt-dlp-web-ui/v3/server/internal"
|
|
)
|
|
|
|
/*
|
|
File based operation handlers (should be moved to rest/handlers.go) or in
|
|
a entirely self-contained package
|
|
*/
|
|
|
|
var (
|
|
videoRe = regexp.MustCompile(`(?i)/\.mov|\.mp4|\.webm|\.mvk|/gmi`)
|
|
)
|
|
|
|
func isVideo(d fs.DirEntry) bool {
|
|
return videoRe.MatchString(d.Name())
|
|
}
|
|
|
|
func isValidEntry(d fs.DirEntry) bool {
|
|
return !strings.HasPrefix(d.Name(), ".") &&
|
|
!strings.HasSuffix(d.Name(), ".part") &&
|
|
!strings.HasSuffix(d.Name(), ".ytdl")
|
|
}
|
|
|
|
type DirectoryEntry struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
ModTime time.Time `json:"modTime"`
|
|
IsVideo bool `json:"isVideo"`
|
|
IsDirectory bool `json:"isDirectory"`
|
|
}
|
|
|
|
func walkDir(root string) (*[]DirectoryEntry, error) {
|
|
dirs, err := os.ReadDir(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var files []DirectoryEntry
|
|
|
|
for _, d := range dirs {
|
|
if !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(),
|
|
IsVideo: 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().DownloadPath
|
|
req := new(ListRequest)
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); 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)
|
|
|
|
if err := json.NewEncoder(w).Encode(files); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
type DeleteRequest = DirectoryEntry
|
|
|
|
func DeleteFile(w http.ResponseWriter, r *http.Request) {
|
|
req := new(DeleteRequest)
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := os.Remove(req.Path); err != nil {
|
|
http.Error(w, err.Error(), 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
|
|
}
|
|
|
|
path, err := url.QueryUnescape(path)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(path)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filename := string(decoded)
|
|
|
|
root := config.Instance().DownloadPath
|
|
|
|
if strings.Contains(filepath.Dir(filepath.Clean(filename)), filepath.Clean(root)) {
|
|
http.ServeFile(w, r, filename)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}
|
|
|
|
func DownloadFile(w http.ResponseWriter, r *http.Request) {
|
|
path := chi.URLParam(r, "id")
|
|
|
|
if path == "" {
|
|
http.Error(w, "inexistent path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
path, err := url.QueryUnescape(path)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(path)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filename := string(decoded)
|
|
|
|
root := config.Instance().DownloadPath
|
|
|
|
if strings.Contains(filepath.Dir(filepath.Clean(filename)), filepath.Clean(root)) {
|
|
w.Header().Add("Content-Disposition", "inline; filename=\""+filepath.Base(filename)+"\"")
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
fd, err := os.Open(filename)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
io.Copy(w, fd)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}
|
|
|
|
func BulkDownload(mdb *internal.MemoryDB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ps := slices.DeleteFunc(*mdb.All(), func(e internal.ProcessResponse) bool {
|
|
return e.Progress.Status != internal.StatusCompleted
|
|
})
|
|
|
|
if len(ps) == 0 {
|
|
return
|
|
}
|
|
|
|
zipWriter := zip.NewWriter(w)
|
|
|
|
w.Header().Add(
|
|
"Content-Disposition",
|
|
"inline; filename=download-archive-"+time.Now().Format(time.RFC3339)+".zip",
|
|
)
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
|
|
for _, p := range ps {
|
|
wr, err := zipWriter.Create(filepath.Base(p.Output.SavedFilePath))
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
fd, err := os.Open(p.Output.SavedFilePath)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if _, err := io.Copy(wr, fd); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := zipWriter.Close(); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|