From 3737e86de329cece900583a8e852ff9b69da722d Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Wed, 17 May 2023 18:32:46 +0200 Subject: [PATCH 1/6] backend functions for list, download, and delete local files --- go.mod | 1 + go.sum | 2 + server/rest/handlers.go | 106 ++++++++++++++++++++++++++++++++++++++++ server/server.go | 5 ++ 4 files changed, 114 insertions(+) create mode 100644 server/rest/handlers.go diff --git a/go.mod b/go.mod index 35f9947..c08ce79 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gofiber/websocket/v2 v2.1.5 github.com/google/uuid v1.3.0 github.com/marcopeocchi/fazzoletti v0.0.0-20230308161120-c545580f79fa + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc golang.org/x/sys v0.7.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 74e695e..fef2991 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= diff --git a/server/rest/handlers.go b/server/rest/handlers.go new file mode 100644 index 0000000..5506382 --- /dev/null +++ b/server/rest/handlers.go @@ -0,0 +1,106 @@ +package rest + +import ( + "crypto/sha256" + "encoding/hex" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/marcopeocchi/yt-dlp-web-ui/server/config" + "golang.org/x/exp/slices" +) + +type DirectoryEntry struct { + Name string `json:"name"` + Path string `json:"path"` + SHASum string `json:"shaSum"` +} + +func walkDir(root string) (*[]DirectoryEntry, error) { + files := []DirectoryEntry{} + + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() && !strings.HasPrefix(d.Name(), ".") { + h := sha256.New() + h.Write([]byte(path)) + + files = append(files, DirectoryEntry{ + Path: path, + Name: d.Name(), + SHASum: hex.EncodeToString(h.Sum(nil)), + }) + } + return nil + }) + + return &files, err +} + +func ListDownloaded(ctx *fiber.Ctx) error { + root := config.Instance().GetConfig().DownloadPath + + files, err := walkDir(root) + if err != nil { + return err + } + + return ctx.JSON(files) +} + +type DeleteRequest = DirectoryEntry + +func DeleteFile(ctx *fiber.Ctx) error { + req := new(DeleteRequest) + + err := ctx.BodyParser(req) + if err != nil { + return err + } + + root := config.Instance().GetConfig().DownloadPath + + files, err := walkDir(root) + if err != nil { + return err + } + + index := slices.IndexFunc(*files, func(e DirectoryEntry) bool { + return e.Path == req.Path && e.SHASum == req.SHASum + }) + + if index >= 0 { + err := os.Remove(req.Path) + if err != nil { + return err + } + } + + return ctx.JSON(index) +} + +type PlayRequest struct { + Path string +} + +func PlayFile(ctx *fiber.Ctx) error { + req := new(PlayRequest) + + err := ctx.BodyParser(req) + if err != nil { + return err + } + + root := config.Instance().GetConfig().DownloadPath + + if strings.Contains(filepath.Dir(req.Path), root) { + return ctx.SendFile(req.Path) + } + + return ctx.SendStatus(fiber.StatusUnauthorized) +} diff --git a/server/server.go b/server/server.go index 12b6897..047d509 100644 --- a/server/server.go +++ b/server/server.go @@ -12,6 +12,7 @@ import ( "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/filesystem" "github.com/gofiber/websocket/v2" + "github.com/marcopeocchi/yt-dlp-web-ui/server/rest" ) var db MemoryDB @@ -31,6 +32,10 @@ func RunBlocking(port int, frontend fs.FS) { return c.Redirect("/") }) + app.Get("/downloaded", rest.ListDownloaded) + app.Post("/delete", rest.DeleteFile) + app.Post("/play", rest.PlayFile) + // RPC handlers // websocket app.Get("/ws-rpc", websocket.New(func(c *websocket.Conn) { From 908f4c66366c936a836cc10e940ed6bdae20b198 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Wed, 24 May 2023 13:19:04 +0200 Subject: [PATCH 2/6] first implementation of downloaded files viewer --- frontend/src/App.tsx | 26 +++-- frontend/src/Downloaded.tsx | 164 +++++++++++++++++++++++++++++++ frontend/src/hooks/observable.ts | 44 +++++++++ frontend/src/types/index.d.ts | 43 +++++--- frontend/src/utils.ts | 4 + server/rest/handlers.go | 24 ++++- server/server.go | 2 +- 7 files changed, 276 insertions(+), 31 deletions(-) create mode 100644 frontend/src/Downloaded.tsx create mode 100644 frontend/src/hooks/observable.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 81e58fe..181acb8 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,21 +19,22 @@ import ListItemIcon from '@mui/material/ListItemIcon' import ListItemText from '@mui/material/ListItemText' import Toolbar from '@mui/material/Toolbar' import Typography from '@mui/material/Typography' - import ListItemButton from '@mui/material/ListItemButton' + import { grey } from '@mui/material/colors' + import { Suspense, lazy, useMemo, useState } from 'react' import { Provider, useDispatch, useSelector } from 'react-redux' -import { - Link, Route, - BrowserRouter as Router, - Routes -} from 'react-router-dom' -import AppBar from './components/AppBar' -import Drawer from './components/Drawer' + +import { Link, Route, BrowserRouter, Routes } from 'react-router-dom' import { toggleListView } from './features/settings/settingsSlice' import { RootState, store } from './stores/store' + +import AppBar from './components/AppBar' +import Drawer from './components/Drawer' + import { formatGiB } from './utils' +import Downloaded from './Downloaded' function AppContent() { const [open, setOpen] = useState(false) @@ -63,7 +64,7 @@ function AppContent() { return ( - + @@ -180,10 +181,15 @@ function AppContent() { } /> + }> + + + } /> - + ) } diff --git a/frontend/src/Downloaded.tsx b/frontend/src/Downloaded.tsx new file mode 100644 index 0000000..84d8819 --- /dev/null +++ b/frontend/src/Downloaded.tsx @@ -0,0 +1,164 @@ +import { + Backdrop, + Button, + Checkbox, + CircularProgress, + Container, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Paper, + SpeedDial, + SpeedDialAction, + SpeedDialIcon +} from '@mui/material' + +import VideoFileIcon from '@mui/icons-material/VideoFile' +import { Buffer } from 'buffer' +import { useEffect, useMemo, useState } from 'react' +import { useSelector } from 'react-redux' +import { BehaviorSubject, combineLatestWith, map, share } from 'rxjs' +import { useObservable } from './hooks/observable' +import { RootState } from './stores/store' +import { DeleteRequest, DirectoryEntry } from './types' +import DeleteForeverIcon from '@mui/icons-material/DeleteForever' + +export default function Downloaded() { + const settings = useSelector((state: RootState) => state.settings) + + const [openDialog, setOpenDialog] = useState(false) + + const serverAddr = + `${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}` + + const files$ = useMemo(() => new BehaviorSubject([]), []) + const selected$ = useMemo(() => new BehaviorSubject([]), []) + + const fetcher = () => fetch(`${serverAddr}/downloaded`) + .then(res => res.json()) + .then(data => files$.next(data)) + + const selectable$ = useMemo(() => files$.pipe( + combineLatestWith(selected$), + map(([data, selected]) => data.map(x => ({ + ...x, + selected: selected.includes(x.name) + }))), + share() + ), []) + + const selectable = useObservable(selectable$, []) + + const addSelected = (name: string) => { + selected$.value.includes(name) + ? selected$.next(selected$.value.filter(val => val !== name)) + : selected$.next([...selected$.value, name]) + } + + const deleteSelected = () => { + Promise.all(selectable + .filter(entry => entry.selected) + .map(entry => fetch(`${serverAddr}/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + path: entry.path, + shaSum: entry.shaSum, + } as DeleteRequest) + })) + ).then(fetcher) + } + + useEffect(() => { + fetcher() + }, [settings.serverAddr, settings.serverPort]) + + return ( + + theme.zIndex.drawer + 1 }} + open={selectable.length === 0} + > + + + + + {selectable.map((file) => ( + addSelected(file.name)} + /> + } + disablePadding + > + + + + + window.open( + `${serverAddr}/play?path=${Buffer.from(file.path).toString('hex')}` + )} /> + + + ))} + + + } + > + } + tooltipTitle={`Delete selected`} + tooltipOpen + onClick={() => setOpenDialog(true)} + /> + + setOpenDialog(false)} + > + + Are you sure? + + + + You're deleting: + +
    + {selected$.value.map((entry, idx) => ( +
  • {entry}
  • + ))} +
+
+ + + + +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/hooks/observable.ts b/frontend/src/hooks/observable.ts new file mode 100644 index 0000000..174f202 --- /dev/null +++ b/frontend/src/hooks/observable.ts @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react' +import { Observable } from 'rxjs' + +/** + * Handles the subscription and unsubscription from an observable. + * Automatically disposes the subscription. + * @param source$ source observable + * @param nextHandler subscriber function + * @param errHandler error catching callback + */ +export function useSubscription( + source$: Observable, + nextHandler: (value: T) => void, + errHandler?: (err: any) => void, +) { + useEffect(() => { + if (source$) { + const sub = source$.subscribe({ + next: nextHandler, + error: errHandler, + }) + return () => sub.unsubscribe() + } + }, [source$]) +} + +/** + * Use an observable as state + * @param source$ source observable + * @param initialState the initial state prior to the emission + * @param errHandler error catching callback + * @returns value emitted to the observable + */ +export function useObservable( + source$: Observable, + initialState: T, + errHandler?: (err: any) => void, +): T { + const [value, setValue] = useState(initialState) + + useSubscription(source$, setValue, errHandler) + + return value +} \ No newline at end of file diff --git a/frontend/src/types/index.d.ts b/frontend/src/types/index.d.ts index f807ad6..041012a 100644 --- a/frontend/src/types/index.d.ts +++ b/frontend/src/types/index.d.ts @@ -10,13 +10,13 @@ export type RPCMethods = | "Service.UpdateExecutable" export type RPCRequest = { - method: RPCMethods, - params?: any[], + method: RPCMethods + params?: any[] id?: string } export type RPCResponse = { - result: T, + result: T error: number | null id?: string } @@ -46,18 +46,29 @@ export type RPCParams = { } export interface DLMetadata { - formats: Array, - best: DLFormat, - thumbnail: string, - title: string, + formats: Array + best: DLFormat + thumbnail: string + title: string } -export interface DLFormat { - format_id: string, - format_note: string, - fps: number, - resolution: string, - vcodec: string, - acodec: string, - filesize_approx: number, -} \ No newline at end of file +export type DLFormat = { + format_id: string + format_note: string + fps: number + resolution: string + vcodec: string + acodec: string + filesize_approx: number +} + +export type DirectoryEntry = { + name: string + path: string + shaSum: string +} + +export type DeleteRequest = Omit + +export type PlayRequest = Omit + diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index e85a106..b9d7ba3 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -82,6 +82,10 @@ export function getHttpRPCEndpoint() { return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/http-rpc` } +export function getHttpEndpoint() { + return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}` +} + export function formatGiB(bytes: number) { return `${(bytes / 1_000_000_000).toFixed(0)}GiB` } diff --git a/server/rest/handlers.go b/server/rest/handlers.go index 5506382..5e98ba4 100644 --- a/server/rest/handlers.go +++ b/server/rest/handlers.go @@ -3,7 +3,9 @@ package rest import ( "crypto/sha256" "encoding/hex" + "errors" "io/fs" + "net/http" "os" "path/filepath" "strings" @@ -50,6 +52,7 @@ func ListDownloaded(ctx *fiber.Ctx) error { return err } + ctx.Status(http.StatusOK) return ctx.JSON(files) } @@ -74,6 +77,10 @@ func DeleteFile(ctx *fiber.Ctx) error { return e.Path == req.Path && e.SHASum == req.SHASum }) + if index == -1 { + ctx.SendString("shasum doesn't match") + } + if index >= 0 { err := os.Remove(req.Path) if err != nil { @@ -81,6 +88,7 @@ func DeleteFile(ctx *fiber.Ctx) error { } } + ctx.Status(fiber.StatusOK) return ctx.JSON(index) } @@ -89,18 +97,26 @@ type PlayRequest struct { } func PlayFile(ctx *fiber.Ctx) error { - req := new(PlayRequest) + path := ctx.Query("path") - err := ctx.BodyParser(req) + if path == "" { + return errors.New("inexistent path") + } + + decoded, err := hex.DecodeString(path) if err != nil { return err } root := config.Instance().GetConfig().DownloadPath - if strings.Contains(filepath.Dir(req.Path), root) { - return ctx.SendFile(req.Path) + //TODO: further path / file validations + + if strings.Contains(filepath.Dir(string(decoded)), root) { + ctx.SendStatus(fiber.StatusPartialContent) + return ctx.SendFile(string(decoded)) } + ctx.Status(fiber.StatusOK) return ctx.SendStatus(fiber.StatusUnauthorized) } diff --git a/server/server.go b/server/server.go index 047d509..2926111 100644 --- a/server/server.go +++ b/server/server.go @@ -34,7 +34,7 @@ func RunBlocking(port int, frontend fs.FS) { app.Get("/downloaded", rest.ListDownloaded) app.Post("/delete", rest.DeleteFile) - app.Post("/play", rest.PlayFile) + app.Get("/play", rest.PlayFile) // RPC handlers // websocket From ac6fe98dc8f076701f7ed18c4a1c9f3f5532ae7d Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Wed, 24 May 2023 13:29:54 +0200 Subject: [PATCH 3/6] ui refactor, downloaded files view enabled --- frontend/src/App.tsx | 29 +++++++++++++++++------------ frontend/src/Downloaded.tsx | 9 +++++---- frontend/src/Home.tsx | 17 +++++++++++++++++ 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 181acb8..843d774 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,6 @@ import { ThemeProvider } from '@emotion/react' import ChevronLeft from '@mui/icons-material/ChevronLeft' import Dashboard from '@mui/icons-material/Dashboard' -import FormatListBulleted from '@mui/icons-material/FormatListBulleted' import Menu from '@mui/icons-material/Menu' import SettingsIcon from '@mui/icons-material/Settings' import SettingsEthernet from '@mui/icons-material/SettingsEthernet' @@ -15,33 +14,32 @@ import CssBaseline from '@mui/material/CssBaseline' import Divider from '@mui/material/Divider' import IconButton from '@mui/material/IconButton' import List from '@mui/material/List' +import ListItemButton from '@mui/material/ListItemButton' import ListItemIcon from '@mui/material/ListItemIcon' import ListItemText from '@mui/material/ListItemText' import Toolbar from '@mui/material/Toolbar' import Typography from '@mui/material/Typography' -import ListItemButton from '@mui/material/ListItemButton' +import DownloadIcon from '@mui/icons-material/Download'; import { grey } from '@mui/material/colors' import { Suspense, lazy, useMemo, useState } from 'react' import { Provider, useDispatch, useSelector } from 'react-redux' -import { Link, Route, BrowserRouter, Routes } from 'react-router-dom' -import { toggleListView } from './features/settings/settingsSlice' +import { BrowserRouter, Link, Route, Routes } from 'react-router-dom' import { RootState, store } from './stores/store' import AppBar from './components/AppBar' import Drawer from './components/Drawer' -import { formatGiB } from './utils' import Downloaded from './Downloaded' +import { formatGiB } from './utils' function AppContent() { const [open, setOpen] = useState(false) const settings = useSelector((state: RootState) => state.settings) const status = useSelector((state: RootState) => state.status) - const dispatch = useDispatch() const mode = settings.theme const theme = useMemo(() => @@ -140,12 +138,19 @@ function AppContent() { - dispatch(toggleListView())}> - - - - - + + + + + + + + state.settings) @@ -38,7 +38,7 @@ export default function Downloaded() { const serverAddr = `${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}` - const files$ = useMemo(() => new BehaviorSubject([]), []) + const files$ = useMemo(() => new Subject(), []) const selected$ = useMemo(() => new BehaviorSubject([]), []) const fetcher = () => fetch(`${serverAddr}/downloaded`) @@ -86,7 +86,7 @@ export default function Downloaded() { theme.zIndex.drawer + 1 }} - open={selectable.length === 0} + open={!(files$.observed)} > @@ -96,6 +96,7 @@ export default function Downloaded() { flexDirection: 'column', }}> + {selectable.length === 0 && 'No files found'} {selectable.map((file) => ( + } + > + } + tooltipTitle={`Table view`} + tooltipOpen + onClick={() => dispatch(toggleListView())} + /> + ); } From b1c6f7248cee4c63ef0efce0be47d0dc3de4a11c Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Wed, 24 May 2023 13:31:05 +0200 Subject: [PATCH 4/6] code refactoring --- frontend/src/App.tsx | 2 +- server/server.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 843d774..122e1a2 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -138,7 +138,7 @@ function AppContent() { - Date: Wed, 24 May 2023 13:31:48 +0200 Subject: [PATCH 5/6] code refactoring --- frontend/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 122e1a2..261835d 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -186,7 +186,7 @@ function AppContent() { } /> - }> From fd0b40ac4612e435fd0d1ab75a95b14482831006 Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Thu, 25 May 2023 11:13:46 +0200 Subject: [PATCH 6/6] code refactoring, enabled memory db persist to fs. --- .gitignore | 1 + frontend/src/App.tsx | 6 ++-- frontend/src/{Downloaded.tsx => Archive.tsx} | 6 +++- frontend/src/components/DownloadsCardView.tsx | 2 +- frontend/src/components/StackableResult.tsx | 11 +++--- server/memory_db.go | 10 ++++++ server/process.go | 15 ++++---- server/rest/handlers.go | 9 ++++- server/server.go | 36 +++++++++++++++++++ server/service.go | 3 ++ 10 files changed, 82 insertions(+), 17 deletions(-) rename frontend/src/{Downloaded.tsx => Archive.tsx} (97%) diff --git a/.gitignore b/.gitignore index 0f5b409..3e94f74 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ downloads .DS_Store build/ yt-dlp-webui +session.dat diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 261835d..ea32cdb 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,7 +32,7 @@ import { RootState, store } from './stores/store' import AppBar from './components/AppBar' import Drawer from './components/Drawer' -import Downloaded from './Downloaded' +import Archive from './Archive' import { formatGiB } from './utils' function AppContent() { @@ -148,7 +148,7 @@ function AppContent() { - + }> - + } /> diff --git a/frontend/src/Downloaded.tsx b/frontend/src/Archive.tsx similarity index 97% rename from frontend/src/Downloaded.tsx rename to frontend/src/Archive.tsx index de47021..6d601f8 100644 --- a/frontend/src/Downloaded.tsx +++ b/frontend/src/Archive.tsx @@ -130,7 +130,11 @@ export default function Downloaded() { icon={} tooltipTitle={`Delete selected`} tooltipOpen - onClick={() => setOpenDialog(true)} + onClick={() => { + if (selected$.value.length > 0) { + setOpenDialog(true) + } + }} /> void } export function DownloadsCardView({ downloads, abortFunction }: Props) { diff --git a/frontend/src/components/StackableResult.tsx b/frontend/src/components/StackableResult.tsx index 24a6211..382e6d1 100644 --- a/frontend/src/components/StackableResult.tsx +++ b/frontend/src/components/StackableResult.tsx @@ -1,4 +1,4 @@ -import { EightK, FourK, Hd, Sd } from "@mui/icons-material"; +import { EightK, FourK, Hd, Sd } from '@mui/icons-material' import { Button, Card, @@ -11,9 +11,9 @@ import { Skeleton, Stack, Typography -} from "@mui/material"; -import { useEffect, useState } from "react"; -import { ellipsis, formatSpeedMiB, roundMiB } from "../utils"; +} from '@mui/material' +import { useEffect, useState } from 'react' +import { ellipsis, formatSpeedMiB, roundMiB } from '../utils' type Props = { title: string, @@ -97,7 +97,8 @@ export function StackableResult({ variant="contained" size="small" color="primary" - onClick={stopCallback}> + onClick={stopCallback} + > {isCompleted ? "Clear" : "Stop"} diff --git a/server/memory_db.go b/server/memory_db.go index 28f8aaa..2562709 100644 --- a/server/memory_db.go +++ b/server/memory_db.go @@ -108,4 +108,14 @@ func (m *MemoryDB) Restore() { feed, _ := os.ReadFile("session.dat") session := Session{} json.Unmarshal(feed, &session) + + for _, proc := range session.Processes { + db.table.Store(proc.Id, &Process{ + id: proc.Id, + url: proc.Info.URL, + Info: proc.Info, + Progress: proc.Progress, + mem: m, + }) + } } diff --git a/server/process.go b/server/process.go index 8f0f3c8..d348319 100644 --- a/server/process.go +++ b/server/process.go @@ -171,14 +171,17 @@ func (p *Process) Kill() error { // has been spawned with setPgid = true. To properly kill // all subprocesses a SIGTERM need to be sent to the correct // process group - pgid, err := syscall.Getpgid(p.proc.Pid) - if err != nil { + if p.proc != nil { + pgid, err := syscall.Getpgid(p.proc.Pid) + if err != nil { + return err + } + err = syscall.Kill(-pgid, syscall.SIGTERM) + + log.Println("Killed process", p.id) return err } - err = syscall.Kill(-pgid, syscall.SIGTERM) - - log.Println("Killed process", p.id) - return err + return nil } // Returns the available format for this URL diff --git a/server/rest/handlers.go b/server/rest/handlers.go index 5e98ba4..7a39aad 100644 --- a/server/rest/handlers.go +++ b/server/rest/handlers.go @@ -21,6 +21,13 @@ type DirectoryEntry struct { SHASum string `json:"shaSum"` } +func isValidEntry(d fs.DirEntry) bool { + return !d.IsDir() && + !strings.HasPrefix(d.Name(), ".") && + !strings.HasSuffix(d.Name(), ".part") && + !strings.HasSuffix(d.Name(), ".ytdl") +} + func walkDir(root string) (*[]DirectoryEntry, error) { files := []DirectoryEntry{} @@ -28,7 +35,7 @@ func walkDir(root string) (*[]DirectoryEntry, error) { if err != nil { return err } - if !d.IsDir() && !strings.HasPrefix(d.Name(), ".") { + if isValidEntry(d) { h := sha256.New() h.Write([]byte(path)) diff --git a/server/server.go b/server/server.go index 54b9ae8..b430457 100644 --- a/server/server.go +++ b/server/server.go @@ -1,12 +1,17 @@ package server import ( + "context" "fmt" "io" "io/fs" "log" "net/http" "net/rpc" + "os" + "os/signal" + "syscall" + "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -18,6 +23,8 @@ import ( var db MemoryDB func RunBlocking(port int, frontend fs.FS) { + db.Restore() + service := new(Service) rpc.Register(service) @@ -73,5 +80,34 @@ func RunBlocking(port int, frontend fs.FS) { app.Server().StreamRequestBody = true + go periodicallyPersist() + go gracefulShutdown(app) + log.Fatal(app.Listen(fmt.Sprintf(":%d", port))) } + +func gracefulShutdown(app *fiber.App) { + ctx, stop := signal.NotifyContext(context.Background(), + os.Interrupt, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + + go func() { + <-ctx.Done() + log.Println("shutdown signal received") + + defer func() { + db.Persist() + stop() + app.ShutdownWithTimeout(time.Second * 5) + }() + }() +} + +func periodicallyPersist() { + for { + db.Persist() + time.Sleep(time.Minute * 5) + } +} diff --git a/server/service.go b/server/service.go index 6d2de79..7d92407 100644 --- a/server/service.go +++ b/server/service.go @@ -72,12 +72,15 @@ func (t *Service) Running(args NoArgs, running *Running) error { func (t *Service) Kill(args string, killed *string) error { log.Println("Trying killing process with id", args) proc, err := db.Get(args) + if err != nil { return err } if proc != nil { err = proc.Kill() } + + db.Delete(proc.id) return err }