Skip to content

Commit 9868de2

Browse files
committed
different method for downloading files with API middleware
1 parent 2fd7051 commit 9868de2

File tree

11 files changed

+118
-78
lines changed

11 files changed

+118
-78
lines changed

package-lock.json

Lines changed: 0 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
"vue": "^3.2.47",
3434
"vue-loader": "^17.0.1",
3535
"vue-router": "^4.1.6",
36-
"vue-template-compiler": "^2.7.14",
37-
"streamsaver": "^2.0.6"
36+
"vue-template-compiler": "^2.7.14"
3837
}
3938
}

public/app.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/mix-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"/app.js": "/app.js?id=3aebfa606492fb5d84145443e7a458cd",
2+
"/app.js": "/app.js?id=957d67186ab2055921321370e7dc21a5",
33
"/app.css": "/app.css?id=46e730db84da71f61a3f42fe6b08d8eb",
44
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
55
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",

resources/js/components/DownloadLink.vue

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,34 @@
11
<script setup>
22
import { CloudArrowDownIcon } from '@heroicons/vue/24/outline';
3-
import streamSaver from 'streamsaver';
43
import axios from 'axios';
54
65
const props = defineProps(['url']);
76
8-
const downloadFile = async () => {
9-
const response = await axios.get(props.url, {
10-
responseType: 'blob'
11-
});
12-
13-
if (response.status !== 200) {
14-
throw new Error(`HTTP error! status: ${response.status}`);
15-
}
16-
17-
const disposition = response.headers['content-disposition'];
18-
const filename = disposition ? disposition.split('filename=')[1].replace(/"/g, '') : 'download.txt';
19-
20-
const fileStream = streamSaver.createWriteStream(filename);
21-
const readableStream = response.data.stream();
22-
23-
if (window.WritableStream && readableStream.pipeTo) {
24-
return readableStream.pipeTo(fileStream)
25-
.then(() => console.log('done writing'));
26-
}
27-
28-
window.writer = fileStream.getWriter();
29-
const reader = readableStream.getReader();
30-
const pump = () => reader.read()
31-
.then(res => res.done
32-
? writer.close()
33-
: writer.write(res.value).then(pump));
34-
35-
pump();
36-
};</script>
7+
const requestFileDownload = () => {
8+
axios.get(`${props.url}/request`)
9+
.then((response) => {
10+
downloadFromUrl(response.data.url);
11+
}).catch((error) => {
12+
console.log(error);
13+
14+
if (error.response && error.response.data) {
15+
alert(`${error.message}: ${error.response.data.message}. Check developer console for more info.`);
16+
}
17+
});
18+
};
19+
20+
const downloadFromUrl = (url) => {
21+
const link = document.createElement('a');
22+
link.href = url;
23+
link.setAttribute('download', '');
24+
document.body.appendChild(link);
25+
link.click();
26+
document.body.removeChild(link);
27+
}
28+
</script>
3729

3830
<template>
39-
<button @click="downloadFile">
31+
<button @click="requestFileDownload">
4032
<slot>
4133
<CloudArrowDownIcon class="w-4 h-4 mr-2" />
4234
Download

routes/api.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use Illuminate\Routing\Middleware\ValidateSignature;
34
use Illuminate\Support\Facades\Route;
45
use Opcodes\LogViewer\Http\Middleware\ForwardRequestToHostMiddleware;
56
use Opcodes\LogViewer\Http\Middleware\JsonResourceWithoutWrappingMiddleware;
@@ -11,12 +12,12 @@
1112
JsonResourceWithoutWrappingMiddleware::class,
1213
])->group(function () {
1314
Route::get('folders', 'FoldersController@index')->name('log-viewer.folders');
14-
Route::get('folders/{folderIdentifier}/download', 'FoldersController@download')->name('log-viewer.folders.download');
15+
Route::get('folders/{folderIdentifier}/download/request', 'FoldersController@requestDownload')->name('log-viewer.folders.request-download');
1516
Route::post('folders/{folderIdentifier}/clear-cache', 'FoldersController@clearCache')->name('log-viewer.folders.clear-cache');
1617
Route::delete('folders/{folderIdentifier}', 'FoldersController@delete')->name('log-viewer.folders.delete');
1718

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

@@ -25,3 +26,11 @@
2526

2627
Route::get('logs', 'LogsController@index')->name('log-viewer.logs');
2728
});
29+
30+
Route::get('folders/{folderIdentifier}/download', 'FoldersController@download')
31+
->middleware(ValidateSignature::class)
32+
->name('log-viewer.folders.download');
33+
34+
Route::get('files/{fileIdentifier}/download', 'FilesController@download')
35+
->middleware(ValidateSignature::class)
36+
->name('log-viewer.files.download');

src/Http/Controllers/FilesController.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Http\Request;
66
use Illuminate\Support\Facades\Gate;
7+
use Illuminate\Support\Facades\URL;
78
use Opcodes\LogViewer\Facades\LogViewer;
89
use Opcodes\LogViewer\Http\Resources\LogFileResource;
910

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

25-
public function download(string $fileIdentifier)
26+
public function requestDownload(Request $request, string $fileIdentifier)
2627
{
2728
$file = LogViewer::getFile($fileIdentifier);
2829

2930
abort_if(is_null($file), 404);
3031

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

34+
return response()->json([
35+
'url' => URL::temporarySignedRoute(
36+
'log-viewer.files.download',
37+
now()->addMinute(),
38+
['fileIdentifier' => $fileIdentifier]
39+
)
40+
]);
41+
}
42+
43+
public function download(string $fileIdentifier)
44+
{
45+
$file = LogViewer::getFile($fileIdentifier);
46+
3347
return $file->download();
3448
}
3549

src/Http/Controllers/FoldersController.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Http\Request;
66
use Illuminate\Support\Facades\Gate;
7+
use Illuminate\Support\Facades\URL;
78
use Opcodes\LogViewer\Facades\LogViewer;
89
use Opcodes\LogViewer\Http\Resources\LogFolderResource;
910
use Opcodes\LogViewer\LogFile;
@@ -23,14 +24,27 @@ public function index(Request $request)
2324
return LogFolderResource::collection($folders->values());
2425
}
2526

26-
public function download(string $folderIdentifier)
27+
public function requestDownload(Request $request, string $folderIdentifier)
2728
{
2829
$folder = LogViewer::getFolder($folderIdentifier);
2930

3031
abort_if(is_null($folder), 404);
3132

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

35+
return response()->json([
36+
'url' => URL::temporarySignedRoute(
37+
'log-viewer.folders.download',
38+
now()->addMinutes(30), // longer time to allow for processing of the ZIP file
39+
['folderIdentifier' => $folderIdentifier]
40+
)
41+
]);
42+
}
43+
44+
public function download(string $folderIdentifier)
45+
{
46+
$folder = LogViewer::getFolder($folderIdentifier);
47+
3448
return $folder->download();
3549
}
3650

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,39 @@
11
<?php
22

33
use Illuminate\Support\Facades\Gate;
4+
use Illuminate\Support\Facades\URL;
45
use Opcodes\LogViewer\Facades\LogViewer;
56
use Opcodes\LogViewer\LogFolder;
67

78
use function Pest\Laravel\get;
89

10+
function assertCanDownloadFolder(string $folderName, string $expectedFileName): void
11+
{
12+
$response = get(route('log-viewer.folders.request-download', $folderName));
13+
14+
$response->assertOk();
15+
expect(URL::isValidUrl($response->json('url')))->toBeTrue();
16+
17+
get($response->json('url'))
18+
->assertOk()
19+
->assertDownload($expectedFileName);
20+
}
21+
22+
function assertCannotDownloadFolder(string $folderName): void
23+
{
24+
get(route('log-viewer.folders.request-download', $folderName))
25+
->assertForbidden();
26+
}
27+
928
test('can download every folder by default', function () {
1029
generateLogFiles([$fileName = 'laravel.log']);
1130
$folder = LogViewer::getFolder('');
1231

13-
get(route('log-viewer.folders.download', $folder->identifier))
14-
->assertOk()
15-
->assertDownload('root.zip');
32+
assertCanDownloadFolder($folder->identifier, 'root.zip');
1633
});
1734

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

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

28-
get(route('log-viewer.folders.download', $folder->identifier))
29-
->assertForbidden();
45+
assertCannotDownloadFolder($folder->identifier);
3046

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

34-
get(route('log-viewer.folders.download', $folder->identifier))
35-
->assertOk()
36-
->assertDownload('root.zip');
50+
assertCanDownloadFolder($folder->identifier, 'root.zip');
3751
});
3852

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

52-
get(route('log-viewer.folders.download', $expectedFolder->identifier))
53-
->assertOk()
54-
->assertDownload('root.zip');
66+
assertCanDownloadFolder($expectedFolder->identifier, 'root.zip');
5567

5668
expect($gateChecked)->toBeTrue();
5769
});
Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,50 @@
11
<?php
22

33
use Illuminate\Support\Facades\Gate;
4+
use Illuminate\Support\Facades\URL;
45
use Opcodes\LogViewer\LogFile;
56

67
use function Pest\Laravel\get;
78

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

11-
get(route('log-viewer.files.download', $fileName))
13+
$response->assertOk();
14+
expect(URL::isValidUrl($response->json('url')))->toBeTrue();
15+
16+
get($response->json('url'))
1217
->assertOk()
1318
->assertDownload($fileName);
19+
}
20+
21+
function assertCannotDownloadFile(string $fileName): void
22+
{
23+
get(route('log-viewer.files.request-download', $fileName))
24+
->assertForbidden();
25+
}
26+
27+
test('can download every file by default', function () {
28+
generateLogFiles([$fileName = 'laravel.log']);
29+
30+
assertCanDownloadFile($fileName);
1431
});
1532

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

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

25-
get(route('log-viewer.files.download', $fileName))
26-
->assertForbidden();
42+
assertCannotDownloadFile($fileName);
2743

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

31-
get(route('log-viewer.files.download', $fileName))
32-
->assertOk()
33-
->assertDownload($fileName);
47+
assertCanDownloadFile($fileName);
3448
});
3549

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

48-
get(route('log-viewer.files.download', $fileName))
49-
->assertOk()
50-
->assertDownload($fileName);
62+
assertCanDownloadFile($fileName);
5163

5264
expect($gateChecked)->toBeTrue();
5365
});

0 commit comments

Comments
 (0)