diff --git a/README.md b/README.md index eed32331..15f16295 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ We also need to enable some services in order to run Sylve, you can drop these i sysrc ntpd_enable="YES" # Optional sysrc ntpd_sync_on_start="YES" # Optional sysrc zfs_enable="YES" -sysrc linux_enable="YES" # Optional sysrc libvirtd_enable="YES" sysrc dnsmasq_enable="YES" sysrc rpcbind_enable="YES" diff --git a/internal/config/config.go b/internal/config/config.go index e8b9956c..3b5dea73 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -92,6 +92,7 @@ func SetupDataPath() error { filepath.Join(dataPath, "downloads"), filepath.Join(dataPath, "downloads", "torrents"), filepath.Join(dataPath, "downloads", "http"), + filepath.Join(dataPath, "downloads", "path"), filepath.Join(dataPath, "downloads", "extracted"), } @@ -122,6 +123,8 @@ func GetDownloadsPath(dType string) string { return filepath.Join(ParsedConfig.DataPath, "downloads", "torrents", "torrent.db") case "http": return filepath.Join(ParsedConfig.DataPath, "downloads", "http") + case "path": + return filepath.Join(ParsedConfig.DataPath, "downloads", "path") case "extracted": return filepath.Join(ParsedConfig.DataPath, "downloads", "extracted") } diff --git a/internal/db/models/utilities/downloads.go b/internal/db/models/utilities/downloads.go index 69bb6f28..0b4ed8ca 100644 --- a/internal/db/models/utilities/downloads.go +++ b/internal/db/models/utilities/downloads.go @@ -19,6 +19,14 @@ const ( DownloadStatusFailed DownloadStatus = "failed" ) +type DownloadType string + +const ( + DownloadTypeHTTP DownloadType = "http" + DownloadTypeTorrent DownloadType = "torrent" + DownloadTypePath DownloadType = "path" +) + type DownloadedFile struct { ID int `json:"id" gorm:"primaryKey"` DownloadID int `json:"downloadId" gorm:"not null"` @@ -32,7 +40,7 @@ type Downloads struct { UUID string `json:"uuid" gorm:"unique;not null"` Path string `json:"path" gorm:"unique;not null"` Name string `json:"name" gorm:"not null"` - Type string `json:"type" gorm:"not null"` + Type DownloadType `json:"type" gorm:"not null"` URL string `json:"url" gorm:"unique;not null"` Progress int `json:"progress" gorm:"not null"` Size int64 `json:"size" gorm:"not null"` diff --git a/internal/handlers/utilities/downloads.go b/internal/handlers/utilities/downloads.go index 77b3f053..9598f15d 100644 --- a/internal/handlers/utilities/downloads.go +++ b/internal/handlers/utilities/downloads.go @@ -25,9 +25,10 @@ import ( ) type DownloadFileRequest struct { - URL string `json:"url" binding:"required"` - Filename *string `json:"filename"` - IgnoreTLS *bool `json:"ignoreTLS"` + URL string `json:"url" binding:"required"` + Filename *string `json:"filename"` + IgnoreTLS *bool `json:"ignoreTLS"` + AutomaticExtraction *bool `json:"automaticExtraction"` } type BulkDeleteDownloadRequest struct { @@ -108,7 +109,14 @@ func DownloadFile(utilitiesService *utilities.Service) gin.HandlerFunc { insecureOkay = false } - if err := utilitiesService.DownloadFile(request.URL, fileName, insecureOkay); err != nil { + var automaticExtraction bool + if request.AutomaticExtraction != nil && *request.AutomaticExtraction { + automaticExtraction = true + } else { + automaticExtraction = false + } + + if err := utilitiesService.DownloadFile(request.URL, fileName, insecureOkay, automaticExtraction); err != nil { c.JSON(http.StatusInternalServerError, internal.APIResponse[any]{ Status: "error", Message: "failed_to_download_file", diff --git a/internal/interfaces/services/utilities/utilities.go b/internal/interfaces/services/utilities/utilities.go index ee704af8..345b88f3 100644 --- a/internal/interfaces/services/utilities/utilities.go +++ b/internal/interfaces/services/utilities/utilities.go @@ -11,7 +11,7 @@ package utilitiesServiceInterfaces import utilitiesModels "github.com/alchemillahq/sylve/internal/db/models/utilities" type UtilitiesServiceInterface interface { - DownloadFile(url string, optFilename string, insecureOkay bool) error + DownloadFile(url string, optFilename string, insecureOkay bool, automaticExtraction bool) error ListDownloads() ([]utilitiesModels.Downloads, error) GetMagnetDownloadAndFile(uuid, name string) (*utilitiesModels.Downloads, *utilitiesModels.DownloadedFile, error) SyncDownloadProgress() error diff --git a/internal/services/utilities/downloads.go b/internal/services/utilities/downloads.go index e1e7fffe..9f6794bb 100644 --- a/internal/services/utilities/downloads.go +++ b/internal/services/utilities/downloads.go @@ -98,7 +98,7 @@ func (s *Service) GetFilePathById(uuid string, id int) (string, error) { return "", fmt.Errorf("unsupported_download_type") } -func (s *Service) DownloadFile(url string, optFilename string, insecureOkay bool) error { +func (s *Service) DownloadFile(url string, optFilename string, insecureOkay bool, automaticExtraction bool) error { var existing utilitiesModels.Downloads if s.DB.Where("url = ?", url).First(&existing).RowsAffected > 0 { @@ -120,15 +120,16 @@ func (s *Service) DownloadFile(url string, optFilename string, insecureOkay bool } download := utilitiesModels.Downloads{ - URL: url, - UUID: t.ID(), - Path: t.Dir(), - Type: "torrent", - Name: t.Name(), - Size: 0, - Progress: 0, - Files: []utilitiesModels.DownloadedFile{}, - Status: utilitiesModels.DownloadStatusPending, + URL: url, + UUID: t.ID(), + Path: t.Dir(), + Type: utilitiesModels.DownloadTypeTorrent, + Name: t.Name(), + Size: 0, + Progress: 0, + Files: []utilitiesModels.DownloadedFile{}, + Status: utilitiesModels.DownloadStatusPending, + AutomaticExtraction: false, } if err := s.DB.Create(&download).Error; err != nil { @@ -180,7 +181,7 @@ func (s *Service) DownloadFile(url string, optFilename string, insecureOkay bool URL: url, UUID: uuid, Path: filePath, - Type: "http", + Type: utilitiesModels.DownloadTypeHTTP, Name: filename, Size: size, Progress: 100, @@ -199,15 +200,16 @@ func (s *Service) DownloadFile(url string, optFilename string, insecureOkay bool } download := utilitiesModels.Downloads{ - URL: url, - UUID: uuid, - Path: filePath, - Type: "http", - Name: filename, - Size: 0, - Progress: 0, - Files: []utilitiesModels.DownloadedFile{}, - Status: utilitiesModels.DownloadStatusPending, + URL: url, + UUID: uuid, + Path: filePath, + Type: utilitiesModels.DownloadTypeHTTP, + Name: filename, + Size: 0, + Progress: 0, + Files: []utilitiesModels.DownloadedFile{}, + Status: utilitiesModels.DownloadStatusPending, + AutomaticExtraction: automaticExtraction, } if err := s.DB.Create(&download).Error; err != nil { @@ -268,15 +270,16 @@ func (s *Service) DownloadFile(url string, optFilename string, insecureOkay bool logger.L.Info().Msgf("Copied file %s to %s (%d bytes)", url, destPath, size) download := utilitiesModels.Downloads{ - URL: url, - UUID: utils.GenerateDeterministicUUID(url), - Path: destPath, - Type: "http", - Name: filename, - Size: size, - Progress: 100, - Files: []utilitiesModels.DownloadedFile{}, - Status: utilitiesModels.DownloadStatusDone, + URL: url, + UUID: utils.GenerateDeterministicUUID(url), + Path: destPath, + Type: utilitiesModels.DownloadTypePath, + Name: filename, + Size: size, + Progress: 100, + Files: []utilitiesModels.DownloadedFile{}, + Status: utilitiesModels.DownloadStatusDone, + AutomaticExtraction: false, } if err := s.DB.Create(&download).Error; err != nil { @@ -352,6 +355,10 @@ func (s *Service) postProcessOne(id uint) error { return nil } + if !d.AutomaticExtraction { + return s.finishDownload(&d, "", "") + } + // Prepare extract dir extractsPath := filepath.Join(config.GetDownloadsPath("extracted"), d.UUID) if err := utils.ResetDir(extractsPath); err != nil { @@ -459,9 +466,9 @@ func (s *Service) SyncDownloadProgress() error { for _, d := range downloads { switch d.Type { - case "torrent": + case utilitiesModels.DownloadTypeTorrent: s.syncTorrent(&d) - case "http": + case utilitiesModels.DownloadTypeHTTP: s.syncHTTP(&d) default: logger.L.Warn().Msgf("Unknown download type: %s", d.Type) @@ -592,7 +599,16 @@ func (s *Service) DeleteDownload(id int) error { } } - err := utils.DeleteFile(path.Join(config.GetDownloadsPath(download.Type), download.Name)) + var dType string + if download.Type == utilitiesModels.DownloadTypeHTTP { + dType = "http" + } else if download.Type == utilitiesModels.DownloadTypePath { + dType = "path" + } else if download.Type == utilitiesModels.DownloadTypeTorrent { + dType = "torrent" + } + + err := utils.DeleteFile(path.Join(config.GetDownloadsPath(dType), download.Name)) if err != nil { logger.L.Debug().Msgf("Failed to delete HTTP download file: %v", err) return err diff --git a/web/src/lib/api/utilities/downloader.ts b/web/src/lib/api/utilities/downloader.ts index 4ad6aeb2..ca20db52 100644 --- a/web/src/lib/api/utilities/downloader.ts +++ b/web/src/lib/api/utilities/downloader.ts @@ -9,12 +9,14 @@ export async function getDownloads(): Promise { export async function startDownload( url: string, filename?: string, - ignoreTLS?: boolean + ignoreTLS?: boolean, + automaticExtraction?: boolean ): Promise { return await apiRequest('/utilities/downloads', APIResponseSchema, 'POST', { url, filename, - ignoreTLS + ignoreTLS, + automaticExtraction }); } diff --git a/web/src/routes/[node]/utilities/downloader/+page.svelte b/web/src/routes/[node]/utilities/downloader/+page.svelte index 8bfa034b..45a3c598 100644 --- a/web/src/routes/[node]/utilities/downloader/+page.svelte +++ b/web/src/routes/[node]/utilities/downloader/+page.svelte @@ -24,7 +24,7 @@ isValidFileName } from '$lib/utils/string'; import { generateTableData } from '$lib/utils/utilities/downloader'; - import { useQueries } from '@sveltestack/svelte-query'; + import { useQueries, useQueryClient } from '@sveltestack/svelte-query'; import { toast } from 'svelte-sonner'; import isMagnet from 'validator/lib/isMagnetURI'; @@ -33,13 +33,15 @@ } let { data }: { data: Data } = $props(); + + const queryClient = useQueryClient(); const results = useQueries([ { - queryKey: ['downloads'], + queryKey: 'downloads', queryFn: async () => { return await getDownloads(); }, - refetchInterval: 1000, + refetchInterval: false, keepPreviousData: true, initialData: data.downloads, onSuccess: (data: Download[]) => { @@ -48,15 +50,28 @@ } ]); - let modalState = $state({ + let reload = $state(false); + + $effect(() => { + if (reload) { + queryClient.invalidateQueries('downloads'); + reload = false; + } + }); + + let options = { isOpen: false, isDelete: false, isBulkDelete: false, title: '', url: '', name: '', - ignoreTLS: false - }); + ignoreTLS: false, + automaticExtraction: false, + loading: false + }; + + let modalState = $state(options); let downloads = $derived($results[0].data as Download[]); let tableData = $derived(generateTableData(downloads)); @@ -132,14 +147,18 @@ return; } + modalState.loading = true; + const result = await startDownload( modalState.url, modalState.name || undefined, - modalState.ignoreTLS + modalState.ignoreTLS, + modalState.automaticExtraction ); + if (result) { - modalState.isOpen = false; - modalState.url = ''; + modalState = options; + reload = true; toast.success('Download started', { position: 'bottom-center' }); } else { toast.error('Download failed', { position: 'bottom-center' }); @@ -280,9 +299,11 @@ {#if modalState.url && isDownloadURL(modalState.url)} @@ -294,17 +315,30 @@ classes="flex-1 space-y-1 mt-2" /> - +
+ + +
{/if}
- +
@@ -325,9 +359,9 @@ onConfirm: async () => { const id = activeRows ? activeRows[0]?.id : null; const result = await deleteDownload(id as number); + reload = true; if (isAPIResponse(result) && result.status === 'success') { - modalState.isDelete = false; - modalState.title = ''; + modalState = options; activeRows = null; } else { handleAPIError(result as APIResponse); @@ -335,7 +369,7 @@ } }, onCancel: () => { - modalState.isDelete = false; + modalState = options; modalState.title = ''; } }} @@ -348,9 +382,9 @@ onConfirm: async () => { const ids = activeRows ? activeRows.map((row) => row.id) : []; const result = await bulkDeleteDownloads(ids as number[]); + reload = true; if (isAPIResponse(result) && result.status === 'success') { - modalState.isBulkDelete = false; - modalState.title = ''; + modalState = options; activeRows = null; } else { handleAPIError(result as APIResponse); @@ -358,7 +392,7 @@ } }, onCancel: () => { - modalState.isBulkDelete = false; + modalState = options; modalState.title = ''; } }}