diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index 5ad07ecee..54638f2e1 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -2,15 +2,12 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; -use Carbon\Carbon; -use Ramsey\Uuid\Uuid; use Illuminate\Http\Response; use Pterodactyl\Models\Server; -use Illuminate\Http\JsonResponse; use GuzzleHttp\Exception\TransferException; +use Illuminate\Contracts\Routing\ResponseFactory; use Pterodactyl\Repositories\Wings\DaemonFileRepository; use Pterodactyl\Transformers\Daemon\FileObjectTransformer; -use Illuminate\Contracts\Cache\Repository as CacheRepository; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest; @@ -18,34 +15,35 @@ use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest; -use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DownloadFileRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest; class FileController extends ClientApiController { - /** - * @var \Illuminate\Contracts\Cache\Factory - */ - private $cache; - /** * @var \Pterodactyl\Repositories\Wings\DaemonFileRepository */ private $fileRepository; + /** + * @var \Illuminate\Contracts\Routing\ResponseFactory + */ + private $responseFactory; + /** * FileController constructor. * + * @param \Illuminate\Contracts\Routing\ResponseFactory $responseFactory * @param \Pterodactyl\Repositories\Wings\DaemonFileRepository $fileRepository - * @param \Illuminate\Contracts\Cache\Repository $cache */ - public function __construct(DaemonFileRepository $fileRepository, CacheRepository $cache) - { + public function __construct( + ResponseFactory $responseFactory, + DaemonFileRepository $fileRepository + ) { parent::__construct(); - $this->cache = $cache; $this->fileRepository = $fileRepository; + $this->responseFactory = $responseFactory; } /** @@ -91,6 +89,39 @@ class FileController extends ClientApiController ); } + /** + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Symfony\Component\HttpFoundation\StreamedResponse + * + * @throws \Exception + */ + public function download(GetFileContentsRequest $request, Server $server) + { + set_time_limit(0); + + $request = $this->fileRepository->setServer($server)->streamContent( + $request->get('file') + ); + + $body = $request->getBody(); + + preg_match('/filename=(?.*)$/', $request->getHeaderLine('Content-Disposition'), $matches); + + return $this->responseFactory->streamDownload( + function () use ($body) { + while (! $body->eof()) { + echo $body->read(128); + } + }, + $matches['name'] ?? 'download', + [ + 'Content-Type' => $request->getHeaderLine('Content-Type'), + 'Content-Length' => $request->getHeaderLine('Content-Length'), + ] + ); + } + /** * Writes the contents of the specified file to the server. * @@ -171,27 +202,4 @@ class FileController extends ClientApiController return Response::create('', Response::HTTP_NO_CONTENT); } - - /** - * Configure a reference to a file to download in the cache so that when the - * user hits the Daemon and it verifies with the Panel they'll actually be able - * to download that file. - * - * Returns the token that needs to be used when downloading the file. - * - * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DownloadFileRequest $request - * @param \Pterodactyl\Models\Server $server - * @return \Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function download(DownloadFileRequest $request, Server $server): JsonResponse - { - $token = Uuid::uuid4()->toString(); - - $this->cache->put( - 'Server:Downloads:' . $token, ['server' => $server->uuid, 'path' => $request->route()->parameter('file')], Carbon::now()->addMinutes(5) - ); - - return JsonResponse::create(['token' => $token]); - } } diff --git a/app/Repositories/Wings/DaemonFileRepository.php b/app/Repositories/Wings/DaemonFileRepository.php index 9e1252933..23c1a23a8 100644 --- a/app/Repositories/Wings/DaemonFileRepository.php +++ b/app/Repositories/Wings/DaemonFileRepository.php @@ -57,6 +57,29 @@ class DaemonFileRepository extends DaemonRepository return $response->getBody()->__toString(); } + /** + * Returns a stream of a file's contents back to the calling function to allow + * proxying the request through the Panel rather than needing a direct call to + * the Daemon in order to work. + * + * @param string $path + * @return \Psr\Http\Message\ResponseInterface + */ + public function streamContent(string $path): ResponseInterface + { + Assert::isInstanceOf($this->server, Server::class); + + $response = $this->getHttpClient()->get( + sprintf('/api/servers/%s/files/contents', $this->server->uuid), + [ + 'query' => ['file' => $path, 'download' => true], + 'stream' => true, + ] + ); + + return $response; + } + /** * Save new contents to a given file. This works for both creating and updating * a file. diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx index db0ded5c9..07bec2912 100644 --- a/resources/scripts/components/server/files/FileDropdownMenu.tsx +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -13,7 +13,7 @@ import { join } from 'path'; import deleteFile from '@/api/server/files/deleteFile'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; import copyFile from '@/api/server/files/copyFile'; -import { httpErrorToHuman } from '@/api/http'; +import http, { httpErrorToHuman } from '@/api/http'; type ModalType = 'rename' | 'move'; @@ -69,6 +69,10 @@ export default ({ uuid }: { uuid: string }) => { }); }; + const doDownload = () => { + window.location = `/api/client/servers/${server.uuid}/files/download?file=${join(directory, file.name)}` as unknown as Location; + }; + useEffect(() => { menuVisible ? document.addEventListener('click', windowListener) @@ -138,7 +142,10 @@ export default ({ uuid }: { uuid: string }) => { Copy -
+
doDownload()} + > Download
diff --git a/routes/api-client.php b/routes/api-client.php index 560645e6d..b246bebcc 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -46,15 +46,12 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::group(['prefix' => '/files'], function () { Route::get('/list', 'Servers\FileController@listDirectory')->name('api.client.servers.files.list'); Route::get('/contents', 'Servers\FileController@getFileContents')->name('api.client.servers.files.contents'); + Route::get('/download', 'Servers\FileController@download'); Route::put('/rename', 'Servers\FileController@renameFile')->name('api.client.servers.files.rename'); Route::post('/copy', 'Servers\FileController@copyFile')->name('api.client.servers.files.copy'); Route::post('/write', 'Servers\FileController@writeFileContents')->name('api.client.servers.files.write'); Route::post('/delete', 'Servers\FileController@delete')->name('api.client.servers.files.delete'); Route::post('/create-folder', 'Servers\FileController@createFolder')->name('api.client.servers.files.create-folder'); - - Route::post('/download/{file}', 'Servers\FileController@download') - ->where('file', '.*') - ->name('api.client.servers.files.download'); }); Route::group(['prefix' => '/network'], function () {