i.t's avatar
Level 10

Upload and display documents to logged in users

I have a Laravel 5.3 application and I am trying to display a document (PDF, Word...) in the browser that I just uploaded. I am uploading the document this way:

$file = $request->file->store('documents/'. $user->id);

and then storing the path in the database. I need to display the document in the browser only to logged in users. The problem I am facing is that I cannot get the document in any case. When I try to access the document's path in the browser (http://localhost:8000/storage/app/docs/document.pdf) I get NotFoundHttpException in RouteCollection.php line 161:

I tried setting a route for that, but I am not sure what I am doing wrong. I was hoping I can access the file like that? Do I need to configure something or set permissions and how do I restrict the document to only be visible to logged in users? (I don't want the document to be public and someone to access it by typing the URL)

Any help is really appreciated. Thanks

0 likes
20 replies
cent040's avatar

Hi, Follow these steps

1: Run this command in your composer

         php artisan storage:link

2: you will see a shortcut of storage in your public folder

Now you can access your images and files easily

example.com/public/storage/etc/etc.png

Regards Arfan

1 like
v.lozickas's avatar
Level 9

The above answer would work, but it would also make a file public.

What you need here is a route that accepts an identifier of the file you want to access (something like example.com/files/xyz123 where that xyz1231 is an identifier (or a hash name) of the file you want to access. Then you protect this route with authentication (so it's not public) and when signed-in user tries to access it (and has permissions to do so (optional)) - you read the file from the storage and return it with the response.

Of course that means you need your saving/uploading mechanism of this file to be able to create that identifier, store it and map it to a file path. Or just rename the file itself (using generated identifier/hash) and use that to find that file (that is if you don't care about the original name).

1 like
i.t's avatar
Level 10

Thanks for your answers, guys. @cent040 your answer will make my documents public, that's not what I want. @v.lozickas Thanks for the helpful answer. I created another route and I use the response like this:

return Response::make(file_get_contents($path), 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'inline; filename="' . $filename.'"' ]);

and I can display the file now and also protect the route, so everything seems to work fine. I am using a hardcoded file name, so I need to write some function like you suggested to generate a hash name (I don't care about the original name), but hopefully, there will be thousands of files and I need to make sure the name is unique.

Thanks again

1 like
v.lozickas's avatar

You can also use return response()->download($file, 'file.pdf', $headers); (Or through the facade Response::download(...);) - where $file is a path to your file. That gives you an ability to rename the file ('file.pdf') and also drops a need of that (a bit ugly) file_get_contents function call :)

1 like
i.t's avatar
Level 10

Great, thanks, I will try this. I have one question, though. Now the logged in user can access the document directly by typing the URL. For example: http://localhost:8000/documents/57db263b31a6aa19b406.pdf

I want to restrict this so they won't be able to download the document, but only see it in my view. I am using html object to display it in the view, which also allows download. Do you have any idea about this?

v.lozickas's avatar

As far as I'm concerned if you're accessing a file directly through the URL, the request will never even reach Laravel, as server iteself (through .htaccess file or nginx configuration) will forward this to just respond with the file... I may be wrong here, but I think that might not be possible...

And TBH I don't really see a point for this, because there should not be any problems when accessing it through the route :)

i.t's avatar
Level 10

Yeah, I understand, but some of these files are confidential and I am trying my best to protect them, at least not to allow people to download them.

v.lozickas's avatar

If you did php artisan storage:link then just remove the symlink from public folder and these files will not be accessible publicly with direct links. From your controller you can read the file from the storage dir directly.

1 like
i.t's avatar
Level 10

@v.lozickas do you think generating a unique name like this is good enough?

$user->id . '_doc_' . md5(time())

I want to prefix them with the user id and doc in case some documents get mixed up (that shouldn't happen though, because I upload them to separate folders for each user, so maybe this is not necessary)

v.lozickas's avatar

What do you mean by "direct link"? Where is you file storage located? In the public dir or in the storage dir? Your file should be accessible only through the route when a user is logged in.

If you want your users to have "own" files, I suggest creating a file reference table, where you can save references (paths/hashes) of your files with a reference to a user (through user id). Then in your route you query that table, get the file that was requested and check if the user requesting it owns it. If not - you throw 404 or something...

For the file names itself (if you're grouping them by user folder), I think it's really enough to use md5 with time (unless you plan of having billions of files...).

i.t's avatar
Level 10

By direct link I mean the route, for example my route is this:

Route::get('documents/{user}/{file}'

and if a logged in user types something like this:

http://localhost:8000/documents/2_doc_e63bb69dfbd28a123.pdf

The browser will open the pdf and allow to be downloaded. On another page in the application I am using iframe with this exact link to embed this same PDF, but I don't want another user to be able to go to the link and download the file. I think I can achieve something like that with .htaccess, need to try.

I already did that and I am storing the paths and user ids in a database and each user owns files, but there will be a type of user that will be able to access all of the documents from all users and I don't want this user to be able to download the documents, only view them.

Thanks for the file name suggestion, I will remove the id.

v.lozickas's avatar

If you have admin user that can access all of the files, then maybe check out Laravel's authorization (https://laravel.com/docs/5.3/authorization) - with it you can implement a simple ACL.

As for the "just view but not download" I think that would be tricky... As if you can see the file it means that you have "downloaded" it already (even if you just show it through an iframe or something). I mean if you see it - the browser will allow you to save it...

i.t's avatar
Level 10

yeah, I get your point. I have no idea how to handle that in another way. Some of the documents might be confidential, so that is the reason I am trying to restrict that. The user role that will be able to access all documents is not admin, but a different role, a person who can login and view the documents displayed in iframe and probably I can put some watermark over them in case they screenshot or something. Or I can try to generate images from the uploaded document and display that, but if they can check the source and download it, it's not helpful as well. :)

v.lozickas's avatar

Generating image with a watermark from the backend should work, as you will only get a generated image if a user can only view the document, and you will get a full document file if user is allowed to download it :)

1 like
nikocraft's avatar

Hope this helps you. Here is answer I posted in another thread: https://laracasts.com/discuss/channels/laravel/private-file-access

I have a solution that uses public folder. When you upload files you can save them in whatever folder you want, you then save that folder in database together with filename. Then you generate a hash as well and use that in your route to access a particular file or you can use filename instead of hash, up to you. When you check the hash and find the coresponding file in the database you then serve that file from public folder directly from router.

Here is an example for images:

        $image = Cache::rememberForever('image_'.$hash, function() use($hash){
            return Image::where('hash', $hash)->first();
        });

        if($image == null)
            abort(404, 'The image you are looking for could not be found.');

        $imagefile = file_get_contents($image->path.'/'.$image->hash . '.' . $image->extension);

        header('Content-type: image/jpeg;');
        header("Content-Length: " . strlen($imagefile));
        echo $imagefile;

user can't access any files in the public folder since they do not know where in what folder you save the files and since you render the file directly from controller function they will think if they type

www.yourwebsite.com/files/Yq4VgtC.jpg

that jpg image they just tried to access is actually stored directly under files folder when neither files folder exists nor that image directly under it.

Image could be somewhere like this

public/uploads/2016/12/28/15

so year/month/day/hour 

etc that's just an example, you can store it where you want and noone will figure it out.

and before they can access any files you can use middleware to check if they are logged in if they try to access

www.website.com/files route

here is how a route can look for example image in my case:

Route::get('{hash}.{extension}', 'ImageController@image')->where('hash', '[a-zA-Z0-9-]+')->where('extension', '[jpg, jpeg, gif, png, mp4, bmp]+');

kieranst 3 weeks ago (19,010 XP)

The issue with that is that the files are still exposed publicly as they are in the public area/folder. Yea it would be a lot harder to find (as your not giving away it's exact name or location), but still possible.

@kieranst

it's pretty much impossible to find the files using my system, I'd love you to prove me wrong :) You can block the folder listing in .htaccess so noone would be able to browse the folders by guessing folder names and you have no chance of finding a file if you do not know what kind of folder structure programmer used to store the files.

for example you could randomize the upload folder so that not even you know where it's uploaded

'public/uploads/2016/12/06/15/' . str_random(7) .'/filename.mp4';

No one no matter what software they used to scan your website would figure it out, and you could ofcourse block anyone using robots to probe your folder structure to find out files. No normal users would do this so it's pretty much safest way to do it if you indeed decide that you must use public folder to store files that should only be visible to certain users.

v.lozickas's avatar

@maxnb I'm pretty sure OP does not require files to be stored in the public folder :)

nikocraft's avatar

well if he wants to do this:

but I don't want another user to be able to go to the link and download the file. I think I can achieve something like that with .htaccess, need to try.

then my solution will work for him. He should never serve the files directly but through the controller so he can check if the user is logged in or not and if the user has the neccessary rights to access the files.

User will never see where the file is actually stored and will not be able to send the link to any other user who is not signed in to download the files.

jeremykes's avatar

I just came across this and maybe it can help someone too. I used this to "protect" my PDF documents to only logged in users.

I put my files outside the storage/public folder (so non of it was accessable to the public). The Controller was protected by the 'auth' middleware in the constructor.

public function grab_file($hash)
    {
        $file = Publication::where('hash', $hash)->first();

        if ($file) {
            return Response::download(storage_path('app/publications/' . $file->filename), null, [
                'Cache-Control' => 'no-cache, no-store, must-revalidate',
                'Pragma' => 'no-cache',
                'Expires' => '0',
            ], null);
        } else {
            return abort(404);
        }
}

and route is;

Route::get('/user/file/{hash}', 'UserController@grab_file');

Where Publication stored the hash reference to the file. This way there was no way for the user to "see" where the actual file was.

The "no caching" headers made sure the browser didn't cache the PDF. I did that because after logging out, you can still access the file.

Please or to participate in this conversation.