Skip to content

Commit

Permalink
Merge pull request #302 from opcodesio/improvement/downloading-files-…
Browse files Browse the repository at this point in the history
…folders

Improvement / downloading files folders
  • Loading branch information
arukompas authored Nov 24, 2023
2 parents 2fd7051 + 435251b commit f5ac2df
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 78 deletions.
13 changes: 0 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"vue": "^3.2.47",
"vue-loader": "^17.0.1",
"vue-router": "^4.1.6",
"vue-template-compiler": "^2.7.14",
"streamsaver": "^2.0.6"
"vue-template-compiler": "^2.7.14"
}
}
2 changes: 1 addition & 1 deletion public/app.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"/app.js": "/app.js?id=3aebfa606492fb5d84145443e7a458cd",
"/app.js": "/app.js?id=957d67186ab2055921321370e7dc21a5",
"/app.css": "/app.css?id=46e730db84da71f61a3f42fe6b08d8eb",
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",
Expand Down
54 changes: 23 additions & 31 deletions resources/js/components/DownloadLink.vue
Original file line number Diff line number Diff line change
@@ -1,42 +1,34 @@
<script setup>
import { CloudArrowDownIcon } from '@heroicons/vue/24/outline';
import streamSaver from 'streamsaver';
import axios from 'axios';
const props = defineProps(['url']);
const downloadFile = async () => {
const response = await axios.get(props.url, {
responseType: 'blob'
});
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const disposition = response.headers['content-disposition'];
const filename = disposition ? disposition.split('filename=')[1].replace(/"/g, '') : 'download.txt';
const fileStream = streamSaver.createWriteStream(filename);
const readableStream = response.data.stream();
if (window.WritableStream && readableStream.pipeTo) {
return readableStream.pipeTo(fileStream)
.then(() => console.log('done writing'));
}
window.writer = fileStream.getWriter();
const reader = readableStream.getReader();
const pump = () => reader.read()
.then(res => res.done
? writer.close()
: writer.write(res.value).then(pump));
pump();
};</script>
const requestFileDownload = () => {
axios.get(`${props.url}/request`)
.then((response) => {
downloadFromUrl(response.data.url);
}).catch((error) => {
console.log(error);
if (error.response && error.response.data) {
alert(`${error.message}: ${error.response.data.message}. Check developer console for more info.`);
}
});
};
const downloadFromUrl = (url) => {
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', '');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>

<template>
<button @click="downloadFile">
<button @click="requestFileDownload">
<slot>
<CloudArrowDownIcon class="w-4 h-4 mr-2" />
Download
Expand Down
13 changes: 11 additions & 2 deletions routes/api.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Illuminate\Routing\Middleware\ValidateSignature;
use Illuminate\Support\Facades\Route;
use Opcodes\LogViewer\Http\Middleware\ForwardRequestToHostMiddleware;
use Opcodes\LogViewer\Http\Middleware\JsonResourceWithoutWrappingMiddleware;
Expand All @@ -11,12 +12,12 @@
JsonResourceWithoutWrappingMiddleware::class,
])->group(function () {
Route::get('folders', 'FoldersController@index')->name('log-viewer.folders');
Route::get('folders/{folderIdentifier}/download', 'FoldersController@download')->name('log-viewer.folders.download');
Route::get('folders/{folderIdentifier}/download/request', 'FoldersController@requestDownload')->name('log-viewer.folders.request-download');
Route::post('folders/{folderIdentifier}/clear-cache', 'FoldersController@clearCache')->name('log-viewer.folders.clear-cache');
Route::delete('folders/{folderIdentifier}', 'FoldersController@delete')->name('log-viewer.folders.delete');

Route::get('files', 'FilesController@index')->name('log-viewer.files');
Route::get('files/{fileIdentifier}/download', 'FilesController@download')->name('log-viewer.files.download');
Route::get('files/{fileIdentifier}/download/request', 'FilesController@requestDownload')->name('log-viewer.files.request-download');
Route::post('files/{fileIdentifier}/clear-cache', 'FilesController@clearCache')->name('log-viewer.files.clear-cache');
Route::delete('files/{fileIdentifier}', 'FilesController@delete')->name('log-viewer.files.delete');

Expand All @@ -25,3 +26,11 @@

Route::get('logs', 'LogsController@index')->name('log-viewer.logs');
});

Route::get('folders/{folderIdentifier}/download', 'FoldersController@download')
->middleware(ValidateSignature::class)
->name('log-viewer.folders.download');

Route::get('files/{fileIdentifier}/download', 'FilesController@download')
->middleware(ValidateSignature::class)
->name('log-viewer.files.download');
16 changes: 15 additions & 1 deletion src/Http/Controllers/FilesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Opcodes\LogViewer\Facades\LogViewer;
use Opcodes\LogViewer\Http\Resources\LogFileResource;

Expand All @@ -22,14 +23,27 @@ public function index(Request $request)
return LogFileResource::collection($files);
}

public function download(string $fileIdentifier)
public function requestDownload(Request $request, string $fileIdentifier)
{
$file = LogViewer::getFile($fileIdentifier);

abort_if(is_null($file), 404);

Gate::authorize('downloadLogFile', $file);

return response()->json([
'url' => URL::temporarySignedRoute(
'log-viewer.files.download',
now()->addMinute(),
['fileIdentifier' => $fileIdentifier]
),
]);
}

public function download(string $fileIdentifier)
{
$file = LogViewer::getFile($fileIdentifier);

return $file->download();
}

Expand Down
16 changes: 15 additions & 1 deletion src/Http/Controllers/FoldersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Opcodes\LogViewer\Facades\LogViewer;
use Opcodes\LogViewer\Http\Resources\LogFolderResource;
use Opcodes\LogViewer\LogFile;
Expand All @@ -23,14 +24,27 @@ public function index(Request $request)
return LogFolderResource::collection($folders->values());
}

public function download(string $folderIdentifier)
public function requestDownload(Request $request, string $folderIdentifier)
{
$folder = LogViewer::getFolder($folderIdentifier);

abort_if(is_null($folder), 404);

Gate::authorize('downloadLogFolder', $folder);

return response()->json([
'url' => URL::temporarySignedRoute(
'log-viewer.folders.download',
now()->addMinutes(30), // longer time to allow for processing of the ZIP file
['folderIdentifier' => $folderIdentifier]
),
]);
}

public function download(string $folderIdentifier)
{
$folder = LogViewer::getFolder($folderIdentifier);

return $folder->download();
}

Expand Down
36 changes: 24 additions & 12 deletions tests/Feature/Authorization/CanDownloadFoldersTest.php
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
<?php

use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Opcodes\LogViewer\Facades\LogViewer;
use Opcodes\LogViewer\LogFolder;

use function Pest\Laravel\get;

function assertCanDownloadFolder(string $folderName, string $expectedFileName): void
{
$response = get(route('log-viewer.folders.request-download', $folderName));

$response->assertOk();
expect(URL::isValidUrl($response->json('url')))->toBeTrue();

get($response->json('url'))
->assertOk()
->assertDownload($expectedFileName);
}

function assertCannotDownloadFolder(string $folderName): void
{
get(route('log-viewer.folders.request-download', $folderName))
->assertForbidden();
}

test('can download every folder by default', function () {
generateLogFiles([$fileName = 'laravel.log']);
$folder = LogViewer::getFolder('');

get(route('log-viewer.folders.download', $folder->identifier))
->assertOk()
->assertDownload('root.zip');
assertCanDownloadFolder($folder->identifier, 'root.zip');
});

test('cannot download a folder that\'s not found', function () {
get(route('log-viewer.folders.download', 'notfound'))
get(route('log-viewer.folders.request-download', 'notfound'))
->assertNotFound();
});

Expand All @@ -25,15 +42,12 @@
$folder = LogViewer::getFolder('');
Gate::define('downloadLogFolder', fn (mixed $user) => false);

get(route('log-viewer.folders.download', $folder->identifier))
->assertForbidden();
assertCannotDownloadFolder($folder->identifier);

// now let's allow access again
Gate::define('downloadLogFolder', fn (mixed $user) => true);

get(route('log-viewer.folders.download', $folder->identifier))
->assertOk()
->assertDownload('root.zip');
assertCanDownloadFolder($folder->identifier, 'root.zip');
});

test('"downloadLogFolder" gate is supplied with a log folder object', function () {
Expand All @@ -49,9 +63,7 @@
return true;
});

get(route('log-viewer.folders.download', $expectedFolder->identifier))
->assertOk()
->assertDownload('root.zip');
assertCanDownloadFolder($expectedFolder->identifier, 'root.zip');

expect($gateChecked)->toBeTrue();
});
36 changes: 24 additions & 12 deletions tests/Feature/Authorization/CanDownloadLogFileTest.php
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
<?php

use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Opcodes\LogViewer\LogFile;

use function Pest\Laravel\get;

test('can download every file by default', function () {
generateLogFiles([$fileName = 'laravel.log']);
function assertCanDownloadFile(string $fileName): void
{
$response = get(route('log-viewer.files.request-download', $fileName));

get(route('log-viewer.files.download', $fileName))
$response->assertOk();
expect(URL::isValidUrl($response->json('url')))->toBeTrue();

get($response->json('url'))
->assertOk()
->assertDownload($fileName);
}

function assertCannotDownloadFile(string $fileName): void
{
get(route('log-viewer.files.request-download', $fileName))
->assertForbidden();
}

test('can download every file by default', function () {
generateLogFiles([$fileName = 'laravel.log']);

assertCanDownloadFile($fileName);
});

test('cannot download a file that\'s not found', function () {
get(route('log-viewer.files.download', 'notfound.log'))
get(route('log-viewer.files.request-download', 'notfound.log'))
->assertNotFound();
});

test('"downloadLogFile" gate can prevent file download', function () {
generateLogFiles([$fileName = 'laravel.log']);
Gate::define('downloadLogFile', fn (mixed $user) => false);

get(route('log-viewer.files.download', $fileName))
->assertForbidden();
assertCannotDownloadFile($fileName);

// now let's allow access again
Gate::define('downloadLogFile', fn (mixed $user) => true);

get(route('log-viewer.files.download', $fileName))
->assertOk()
->assertDownload($fileName);
assertCanDownloadFile($fileName);
});

test('"downloadLogFile" gate is supplied with a log file object', function () {
Expand All @@ -45,9 +59,7 @@
return true;
});

get(route('log-viewer.files.download', $fileName))
->assertOk()
->assertDownload($fileName);
assertCanDownloadFile($fileName);

expect($gateChecked)->toBeTrue();
});
5 changes: 3 additions & 2 deletions tests/Feature/ForwardRequestToHostTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Opcodes\LogViewer\Facades\LogViewer;

beforeEach(function () {
Expand Down Expand Up @@ -53,12 +54,12 @@ function expectedNewUrl($originalUrl, Opcodes\LogViewer\Host $host): string
});
})->with([
'folders index' => ['get', 'log-viewer.folders'],
'folder download' => ['get', 'log-viewer.folders.download', ['folderIdentifier' => 'folder']],
'folder download' => ['get', 'log-viewer.folders.request-download', ['folderIdentifier' => 'folder']],
'folder clear cache' => ['post', 'log-viewer.folders.clear-cache', ['folderIdentifier' => 'folder']],
'folder delete' => ['delete', 'log-viewer.folders.delete', ['folderIdentifier' => 'folder']],

'files index' => ['get', 'log-viewer.files'],
'file download' => ['get', 'log-viewer.files.download', ['fileIdentifier' => 'file']],
'file download' => ['get', 'log-viewer.files.request-download', ['fileIdentifier' => 'file']],
'file clear cache' => ['post', 'log-viewer.files.clear-cache', ['fileIdentifier' => 'file']],
'file delete' => ['delete', 'log-viewer.files.delete', ['fileIdentifier' => 'file']],

Expand Down

0 comments on commit f5ac2df

Please sign in to comment.