mirror of
https://github.com/AlchemillaHQ/Sylve.git
synced 2026-06-14 00:46:34 +03:00
utils: downloads: make extraction optional, i18n: init Hindi
Thanks Abitha V.K for the hindi translation
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,12 +9,14 @@ export async function getDownloads(): Promise<Download[]> {
|
||||
export async function startDownload(
|
||||
url: string,
|
||||
filename?: string,
|
||||
ignoreTLS?: boolean
|
||||
ignoreTLS?: boolean,
|
||||
automaticExtraction?: boolean
|
||||
): Promise<APIResponse> {
|
||||
return await apiRequest('/utilities/downloads', APIResponseSchema, 'POST', {
|
||||
url,
|
||||
filename,
|
||||
ignoreTLS
|
||||
ignoreTLS,
|
||||
automaticExtraction
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
<CustomValueInput
|
||||
label={'Magnet / HTTP URL / Path'}
|
||||
placeholder="magnet:?xt=urn:btih:7d5210a711291d7181d6e074ce5ebd56f3fedd60&dn=debian-12.10.0-amd64-netinst.iso&xl=663748608&tr=http%3A%2F%2Fbttracker.debian.org%3A6969%2Fannounce"
|
||||
placeholder="magnet:?xt=urn:btih:7d5210a711291d7181d6e074ce5ebd56f3fedd60"
|
||||
bind:value={modalState.url}
|
||||
classes="flex-1 space-y-1"
|
||||
type="textarea"
|
||||
textAreaClasses="h-24 w-full"
|
||||
/>
|
||||
|
||||
{#if modalState.url && isDownloadURL(modalState.url)}
|
||||
@@ -294,17 +315,30 @@
|
||||
classes="flex-1 space-y-1 mt-2"
|
||||
/>
|
||||
|
||||
<CustomCheckbox
|
||||
label="Ignore TLS Errors"
|
||||
bind:checked={modalState.ignoreTLS}
|
||||
classes="flex items-center gap-2"
|
||||
/>
|
||||
<div class="mt-2 flex flex-row gap-2">
|
||||
<CustomCheckbox
|
||||
label="Ignore TLS Errors"
|
||||
bind:checked={modalState.ignoreTLS}
|
||||
classes="flex items-center gap-2"
|
||||
/>
|
||||
<CustomCheckbox
|
||||
label="Extract Automatically"
|
||||
bind:checked={modalState.automaticExtraction}
|
||||
classes="flex items-center gap-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer class="flex justify-end">
|
||||
<div class="flex w-full items-center justify-end gap-2 py-2">
|
||||
<Button onclick={newDownload} type="submit" size="sm">Download</Button>
|
||||
<Button onclick={newDownload} type="submit" size="sm">
|
||||
{#if modalState.loading}
|
||||
<span class="icon-[mdi--loading] h-4 w-4 animate-spin"></span>
|
||||
{:else}
|
||||
<span>Download</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
@@ -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 = '';
|
||||
}
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user