I want to make a private directory using Laravel 6 - php

I want to make a private directory using Laravel 6.
Only users who have already logged in can access the directory.
So, I implemented below:
routes/web.php
Route::group(['middleware' => ['private_web']], function() { // 'private_web' includes auth
Route::get('/private/{path?}', 'PrivateController#index')->name('private')->where('path', '.*');
});
PrivateController.php
public function index(Request $request, $path = null) {
$storage_path = 'private/' . $path;
$mime_type = Storage::mimeType($storage_path);
$headers = [ 'Content-Type' => $mime_type, ];
return Storage::response($storage_path, null, $headers);
}
It is working.
But, when I got a html from the directory using Chrome, a css linked from the html wasn't applied (the css is in private directory and just downloaded successfully).
The cause is already known and it is Storage::mimeType returns 'text/plain' against css.
I can fix it by making a branch:
if (ends_with($path, '.css')) {
$mime_type = 'text/css';
} else {
$mime_type = Storage::mimeType($storage_path);
}
Question:
Is there more better solution?
I'm afraid of increasing such branch at each file type...
thanks.

Related

Theme system for laravel application

I am working on a control panel for a niche game server. I want a basic theme system for my app and the goal is to keep the theme assets (js/css/images) together with the views. I don't want the views in resources dir and the assets in public dir separately.
With that in mind; here's what I came up with.
Theme (views and assets) organised like this - i.e. default views dir removed:
new config/site.php
<?php
return [
'theme' => 'default',
];
modified config/views.php
<?php
return [
'paths' => [
resource_path('themes/' . config('site.theme')),
],
...
new route routes/web.php
Route::get('theme/{file?}', 'ThemeController#serve')
->where('file', '[a-zA-Z0-9\.\-\/]+');
new controller app/Http/Controllers/ThemeController.php
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use Illuminate\Support\Facades\File;
class ThemeController extends Controller
{
/**
* #param $file
* #return \Symfony\Component\HttpFoundation\BinaryFileResponse
*/
public function serve($file)
{
$siteConfig = config('site');
$filePath = resource_path("themes/{$siteConfig['theme']}/{$file}");
if (!file_exists($filePath)) {
header("HTTP/1.0 404 Not Found");
exit;
}
$fileLastModified = Carbon::createFromTimestamp(filemtime($filePath))->format('D, j M Y H:i:s');
$fileEtag = md5_file($filePath);
$requestFileEtag = request()->getETags()[0] ?? null;
if (!empty($requestFileEtag) && strcmp($fileEtag, $requestFileEtag) === 0) {
header("Last-Modified: {$fileLastModified}");
header("Etag: {$fileEtag}");
header("HTTP/1.1 304 Not Modified");
exit;
}
return response()->file($filePath, [
'Cache-Control' => 'public, max-age=' . ($siteConfig['themeFilesCacheForMinutes'] * 60),
'Etag' => $fileEtag,
'Last-Modified' => $fileLastModified,
'Content-Type' => $this->guessMimeType($filePath)
]);
}
/**
* #param $filePath
* #return false|string
*/
private function guessMimeType($filePath) {
$ext = pathinfo($filePath, PATHINFO_EXTENSION);
switch ($ext) {
case 'css':
return 'text/css; charset=UTF-8';
case 'js':
return 'text/javascript; charset=UTF-8';
default:
return File::mimeType($filePath);
}
}
}
With that setup; if I want to include an asset from my theme, e.g. css/sb-admin-2.min.css in my master layout in <head>...</head>, this is what I do:
<link href="{{ url('theme/css/sb-admin-2.min.css') }}" rel="stylesheet">
So, using this technique I can keep the views and assets together and php to serve the static asset with caching capability (via headers + etag).
I've tested this locally and it works, initial load takes approx 900ms and once the cache is warmed up, it loads the page under 500ms.
My question; is this a bad approach? i.e. serving static files using php? is there a better way to do this?
If you want to package blades and static assets as an individual replaceable theme, just create a package for each theme and select desired theme using dependency injection. Inside each theme's ServiceProvider publish your assets to public directory.

Laravel: Generating and downloading a CSV file using StreamedResponse produces ERR_INVALID_RESPONSE

I have a controller method like so:
use Symfony\Component\HttpFoundation\StreamedResponse;
public function downloadTransactions($type)
{
$this->authorize('download', Invoice::class);
$filename = 'invoices-' . strtolower($type) . '.csv';
$response = new StreamedResponse(function() {
Invoice::generateTransactionsCsv($type);
}, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
return $response;
}
and then in my Invoice model I am populating the file:
public static function generateTransactionsCsv($type = null)
{
// Open output stream
$handle = fopen('php://output', 'w');
// Add CSV headers
fputcsv($handle, [
'ID',
'REF',
'DESCRIPTION',
'DATE',
'AMOUNT',
]);
// Close the output stream
fclose($handle);
}
but I get an ERR_INVALID_RESPONSE in Chrome, although I would assume the browser doesn't matter. I've checked similar questions which suggest installing the zip extension but I already have it installed. Using PHP 7.1 locally. Also tried looking at the logs but there doesn't seem to be anything there (using Valet locally).
If I move the logic from the model to the controller then it works fine but my example above is simplified to just the header row of the csv, in reality, there's a bit more to it so I'd like to keep the logic in the model if possible.
I've also tried opening and closing the file handle in the controller and passing it the model but that didn't work either.
Looks like it was because the $type variable wasn't being passed correctly:
$response = new StreamedResponse(function() use ($type) {
....
}
Was able to figure it out thanks to Safari, it downloaded a csv with the Laravel error trace in it which was weird, I didn't think it would be any different from Chrome.

Laravel Zipstream add file from stream

I'm trying to get ZipStream working in Laravel 5.4. I want to stream files from s3 to create a ZipStream which is returned to the browser.
Currently my code produces corrupt ZIP files the same size as valid ZIP files.
This is my action method:
public function mediaZip(User $user)
{
return response()->stream(function () use ($user) {
$zip = new ZipStream($user->name . ".zip", [
'content_type' => 'application/octet-stream'
]);
foreach ($user->media as $medium) {
$stream = Storage::disk("s3")->readStream($medium->location);
$zip->addFileFromStream($medium->name, $stream);
}
$zip->finish();
});
}
If I read each file into memory using the code below, the ZIP files work fine. This is however undesirable as I will be working with files that can be several gigabytes large.
public function mediaZip(User $user)
{
return response()->stream(function () use ($user) {
$zip = new ZipStream($user->name . ".zip", [
'content_type' => 'application/octet-stream'
]);
foreach ($user->media as $medium) {
$file = Storage::disk("s3")->read($medium->location);
$zip->addFile($medium->name, $file);
}
$zip->finish();
});
}
Can anyone help me figure out what's going wrong with the ZIP files?
Thanks!
Got it to work by specifying "store" for the storage_method param.
$zip->addFile($medium->name, $file, [], "store");

Laravel 5.4 Response File -> Error in File

I am working with Laravel 5.4 and save some JPEG-Files to Storage with
`Storage::disk('local')->put('upload/pictures/full-size/'.$filename ,$picture);`
And now i try to get this pictures again which i tryed like
...
Routes:
Route::get('pictures/full/{filename}', ['as' => 'picture_full', 'uses' => 'ImageController#getFull']);
...
Image Controller:
public function getFull(Image $image)
{
$path = storage_path('app/'.Config::get('pictures.icon_size').$image->filename);
$handler = new \Symfony\Component\HttpFoundation\File\File($path);
$header_content_type = $handler->getMimeType();
$header_content_length = $handler->getSize();
$headers = array(
'Content-Type' => $header_content_type,
'Content-Length' => $header_content_length
);
return response()->file($path, $headers);
}
So now my Problem is, that the file can't be shown.
The Browser says the File contains an Error.
Tryed a lot, but just don't see what I am making wrong.
Anyone has an idea?

Laravel league/flysystem getting file URL with AWS S3

I am trying to build a file management system in Laravel based on league/flysystem: https://github.com/thephpleague/flysystem
I am using the S3 adapter and I have it working to save the uploaded files using:
$filesystem->write('filename.txt', 'contents');
Now I am stuck on generating the download file URL when using the S3 adapter.
The files are saved correctly in the S3 bucket, I have permissions to access them, I just don't know how to get to the S3 getObjectUrl method through the league/flysystem package.
I have tried:
$contents = $filesystem->read('filename.txt');
but that returns the content of the file.
$contents = $filemanager->listContents();
or
$paths = $filemanager->listPaths();
but they give me the relative paths to my files.
What I need is something like "ht...//[s3-region].amazonaws.com/[bucket]/[dir]/[file]..."
I am using Laravel 5.2 and the code below seemed to work fine.
Storage::cloud()->url('filename');
I'm not sure what the correct way of doing this is with Flysystem, but the underlying S3Client object has a method for doing that. You could do $filesystem->getAdapter()->getClient()->getObjectUrl($bucket, $key);. Of course, building the URL is as trivial as you described, so you don't really need a special method to do it.
When updating to Laravel 5.1 this method no longer supported by the adapter. No in your config you must have the S3_REGION set or you will get a invalid hostname error and secondly I had to use the command as input to create the presignedRequest.
public function getFilePathAttribute($value)
{
$disk = Storage::disk('s3');
if ($disk->exists($value)) {
$command = $disk->getDriver()->getAdapter()->getClient()->getCommand('GetObject', [
'Bucket' => Config::get('filesystems.disks.s3.bucket'),
'Key' => $value,
'ResponseContentDisposition' => 'attachment;'
]);
$request = $disk->getDriver()->getAdapter()->getClient()->createPresignedRequest($command, '+5 minutes');
return (string) $request->getUri();
}
return $value;
}
Maybe I'm a little late to this question, but here's a way to use Laravel 5's built-in Filesystem.
I created a Manager class that extends Laravel's FilesystemManager to handle the public url retrieval:
class FilesystemPublicUrlManager extends FilesystemManager
{
public function publicUrl($name = null, $object_path = '')
{
$name = $name ?: $this->getDefaultDriver();
$config = $this->getConfig($name);
return $this->{'get' . ucfirst($config['driver']) . 'PublicUrl'}($config, $object_path);
}
public function getLocalPublicUrl($config, $object_path = '')
{
return URL::to('/public') . $object_path;
}
public function getS3PublicUrl($config, $object_path = '')
{
$config += ['version' => 'latest'];
if ($config['key'] && $config['secret']) {
$config['credentials'] = Arr::only($config, ['key', 'secret']);
}
return (new S3Client($config))->getObjectUrl($config['bucket'], $object_path);
}
}
Then, I added this class to the AppServiceProvider under the register method so it has access to the current app instance:
$this->app->singleton('filesystemPublicUrl', function () {
return new FilesystemPublicUrlManager($this->app);
});
Finally, for easy static access, I created a Facade class:
use Illuminate\Support\Facades\Facade;
class StorageUrl extends Facade
{
/**
* Get the registered name of the component.
*
* #return string
*/
protected static function getFacadeAccessor()
{
return 'filesystemPublicUrl';
}
}
Now, I can easily get the public url for my public objects on my local and s3 filesystems (note that I didn't add anything for ftp or rackspace in the FilesystemPublicUrlManager):
$s3Url = StorageUrl::publicUrl('s3') //using the s3 driver
$localUrl = StorageUrl::publicUrl('local') //using the local driver
$defaultUrl = StorageUrl::publicUrl() //default driver
$objectUrl = StorageUrl::publicUrl('s3', '/path/to/object');
Another form of Storage::cloud():
/** #var FilesystemAdapter $disk */
$s3 = Storage::disk('s3');
return $s3->url($path);
Using presigned request S3:
public function getFileUrl($key) {
$s3 = Storage::disk('s3');
$client = $s3->getDriver()->getAdapter()->getClient();
$bucket = env('AWS_BUCKET');
$command = $client->getCommand('GetObject', [
'Bucket' => $bucket,
'Key' => $key
]);
$request = $client->createPresignedRequest($command, '+20 minutes');
return (string) $request->getUri();
}
For private cloud use this
Storage::disk('s3')->temporaryUrl($path);

Categories