diff --git a/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php b/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php
new file mode 100644
index 000000000..22c7ea6fb
--- /dev/null
+++ b/app/Http/Controllers/Api/Client/Servers/DownloadBackupController.php
@@ -0,0 +1,80 @@
+daemonBackupRepository = $daemonBackupRepository;
+ $this->responseFactory = $responseFactory;
+ }
+
+ /**
+ * Download the backup for a given server instance. For daemon local files, the file
+ * will be streamed back through the Panel. For AWS S3 files, a signed URL will be generated
+ * which the user is redirected to.
+ *
+ * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Backups\DownloadBackupRequest $request
+ * @param \Pterodactyl\Models\Server $server
+ * @param \Pterodactyl\Models\Backup $backup
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function __invoke(DownloadBackupRequest $request, Server $server, Backup $backup)
+ {
+ $signer = new Sha256;
+ $now = CarbonImmutable::now();
+
+ $token = (new Builder)->issuedBy(config('app.url'))
+ ->permittedFor($server->node->getConnectionAddress())
+ ->identifiedBy(hash('sha256', $request->user()->id . $server->uuid), true)
+ ->issuedAt($now->getTimestamp())
+ ->canOnlyBeUsedAfter($now->subMinutes(5)->getTimestamp())
+ ->expiresAt($now->addMinutes(15)->getTimestamp())
+ ->withClaim('unique_id', Str::random(16))
+ ->withClaim('backup_uuid', $backup->uuid)
+ ->withClaim('server_uuid', $server->uuid)
+ ->getToken($signer, new Key($server->node->daemonSecret));
+
+ $location = sprintf(
+ '%s/download/backup?token=%s',
+ $server->node->getConnectionAddress(),
+ $token->__toString()
+ );
+
+ return RedirectResponse::create($location);
+ }
+}
diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php
index 40c26c538..81ed6b401 100644
--- a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php
+++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php
@@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Middleware\Api\Client;
use Closure;
+use Pterodactyl\Models\Backup;
use Illuminate\Container\Container;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings;
@@ -55,6 +56,10 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings
}
});
+ $this->router->model('backup', Backup::class, function ($value) {
+ return Backup::query()->where('uuid', $value)->firstOrFail();
+ });
+
return parent::handle($request, $next);
}
}
diff --git a/app/Http/Requests/Api/Client/Servers/Backups/DownloadBackupRequest.php b/app/Http/Requests/Api/Client/Servers/Backups/DownloadBackupRequest.php
new file mode 100644
index 000000000..cfa38e688
--- /dev/null
+++ b/app/Http/Requests/Api/Client/Servers/Backups/DownloadBackupRequest.php
@@ -0,0 +1,41 @@
+route()->parameter('server');
+ /** @var \Pterodactyl\Models\Backup|mixed $backup */
+ $backup = $this->route()->parameter('backup');
+
+ if ($server instanceof Server && $backup instanceof Backup) {
+ if ($server->exists && $backup->exists && $server->id === $backup->server_id) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/app/Models/Backup.php b/app/Models/Backup.php
index 56be90f87..6259f11c6 100644
--- a/app/Models/Backup.php
+++ b/app/Models/Backup.php
@@ -26,6 +26,9 @@ class Backup extends Model
const RESOURCE_NAME = 'backup';
+ const DISK_LOCAL = 'local';
+ const DISK_AWS_S3 = 's3';
+
/**
* @var string
*/
diff --git a/app/Repositories/Wings/DaemonBackupRepository.php b/app/Repositories/Wings/DaemonBackupRepository.php
new file mode 100644
index 000000000..d95573d1f
--- /dev/null
+++ b/app/Repositories/Wings/DaemonBackupRepository.php
@@ -0,0 +1,35 @@
+server, Server::class);
+
+ try {
+ return $this->getHttpClient()->get(
+ sprintf('/api/servers/%s/backup/%s', $this->server->uuid, $backup),
+ ['stream' => true]
+ );
+ } catch (TransferException $exception) {
+ throw new DaemonConnectionException($exception);
+ }
+ }
+}
diff --git a/resources/scripts/components/server/backups/BackupRow.tsx b/resources/scripts/components/server/backups/BackupRow.tsx
index ec98cc885..406372e12 100644
--- a/resources/scripts/components/server/backups/BackupRow.tsx
+++ b/resources/scripts/components/server/backups/BackupRow.tsx
@@ -1,28 +1,67 @@
-import React from 'react';
+import React, { useState } from 'react';
import { ServerBackup } from '@/api/server/backups/getServerBackups';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArchive } from '@fortawesome/free-solid-svg-icons/faArchive';
import format from 'date-fns/format';
-import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'
+import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
import Spinner from '@/components/elements/Spinner';
import { faCloudDownloadAlt } from '@fortawesome/free-solid-svg-icons/faCloudDownloadAlt';
+import Modal, { RequiredModalProps } from '@/components/elements/Modal';
+import { bytesToHuman } from '@/helpers';
+import Can from '@/components/elements/Can';
+import { join } from "path";
+import useServer from '@/plugins/useServer';
interface Props {
backup: ServerBackup;
className?: string;
}
+const DownloadModal = ({ checksum, ...props }: RequiredModalProps & { checksum: string }) => (
+
+ The SHA256 checksum of this file is:
+ Verify file checksum
+
+
+ {checksum}
+
{backup.name}
-{backup.uuid}
++ {backup.name} + {backup.completedAt && + {bytesToHuman(backup.bytes)} + } +
++ {backup.uuid} +
{
Created