Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django/Nginx - Error 403 Forbidden when serving media files over some size

When user uploads image, it is stored in media folder inside project directory. The problem is that when they want to see it on the website, nginx return 403 Forbidden error for images over approximately 3 Mb.

I set nginx.conf client_max_body_size to 8M

http {

        ##
        # Basic Settings
        ##
        client_max_body_size 8M;
     ...

And already changed memory size in settings.py:

FILE_UPLOAD_MAX_MEMORY_SIZE = 8388608

When I upload an image under 3 MB, there are no problems, if I upload image over 3 MB, I can see it inside media folder but the error is raised instead of serving image:

GET https://example.com/media/images/dom.jpg 403 (Forbidden)

I noticed that files under 3 MB have different permissions:

-rw-r--r-- 1 django www-data    4962 Jul 19 19:51 61682_3995232_IMG_01_0000.jpg.150x84_q85_crop.jpg
-rw-r--r-- 1 django www-data 1358541 Jul 20 09:32 byt.jpg
-rw------- 1 django www-data 3352841 Jul 20 09:32 dom.jpg
-rw-r--r-- 1 django www-data    5478 Jul 19 20:10 downloasd.jpeg.150x84_q85_crop.jpg
-rw-r--r-- 1 django www-data    3225 Jul  9 22:53 images.jpeg.100x56_q85_crop.jpg
-rw-r--r-- 1 django www-data    6132 Jul 19 20:00 NorthYorkHouse2.JPG.150x84_q85_crop.jpg

Do you know where is the problem?

EDIT:

VIEW

class NehnutelnostUploadImagesView(LoginRequiredMixin, ExclusiveMaklerDetailView, DetailView):
    template_name = "nehnutelnosti/nehnutelnost_image_upload.html"
    model = Nehnutelnost

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        form = ImageUploadForm(self.request.POST, self.request.FILES, nehnutelnost=self.object)
        if form.is_valid():
            nehnutelnost_image = form.save()
            images_count = self.object.images.count()

            data = {'is_valid': True, 'row_html': image_row_renderer(nehnutelnost_image, self.request),
                    'name': nehnutelnost_image.image.name, 'url': nehnutelnost_image.image.url,}
        else:
            images_count = self.object.images.count()

            data = {'is_valid': False, 'errors': form.errors, 'images_count': images_count}
        return JsonResponse(data)

    def get_context_data(self, **kwargs):
        context = super(NehnutelnostUploadImagesView, self).get_context_data(**kwargs)
        context['images'] = self.object.images.all()
        context['podorys'] = self.object.podorys
        return context

We use https://github.com/blueimp/jQuery-File-Upload plugin to upload images.

$(function () {

            $(".js-upload-photos").click(function () {
                $("#fileupload").click();
            });

            $("#fileupload").fileupload({
                dataType: 'json',
                sequentialUploads: true, /* 1. SEND THE FILES ONE BY ONE */
                start: function (e) {  /* 2. WHEN THE UPLOADING PROCESS STARTS, SHOW THE MODAL */
                    $(".modal").modal().show();
                },
                stop: function (e) {  /* 3. WHEN THE UPLOADING PROCESS FINALIZE, HIDE THE MODAL */
                    $(".modal").modal().hide();
                    $(".modal-backdrop").hide();

                },
                {#                TODO Chrome bug?#}
                progressall: function (e, data) {  /* 4. UPDATE THE PROGRESS BAR */
                    var progress = parseInt(data.loaded / data.total * 100, 10);
                    var strProgress = progress + "%";
                    $(".progress-bar").css({"width": strProgress});
                    $(".progress-bar").text(strProgress);
                },
                done: function (e, data) {
                    if (data.result.is_valid) {

                        $(".gridly").prepend(
                            data.result.row_html
                        )


                    }
                    var message = data.result.message;
                    addMessage('success', message);
                    var errors = data.result.errors;
                    if (errors) {
                        $.each(errors, function (fieldname, error_messages) {
                            $.each(error_messages, function (_, message) {
                                addMessage('danger', message);
                            })
                        })
                    }
                    var images_count_span = $('#images_count');
                    var images_count = data.result.images_count;
                    images_count_span.text(' - ' + images_count);
                    makegrid();

                }

            });
like image 925
Milano Avatar asked Jul 20 '18 09:07

Milano


1 Answers

From the documentation:

By default, if an uploaded file is smaller than 2.5 megabytes, Django will hold the entire contents of the upload in memory.

In more concrete terms, it means smaller files use the MemoryFileUploadHandler while larger files use the TemporaryFileUploadHandler. The latter uses tempfile to create a temporary file with user-only access.

After going through all form and model validation and everything, the actual saving is performed by FileSystemStorage._save method. At this point, the file is still either a TemporaryUploadedFile or a InMemoryUploadedFile depending on its size.

Now, a TemporaryUploadedFile is an actual file, created by tempfile, with user-only permissions.

The save method does the smart thing: if given a temporary file (namely, if hasattr(content, 'temporary_file_path')), it moves it instead of copying it. This means it keeps its user-only permissions and remains unreadable by www-data.

The problem doesn't show up with InMemoryUploadedFile, which will simply use whatever default permissions the process has (in your case, read/write for both user and group).

How to fix?

The storage object can set the permissions if so requested. For the default storage object, you can set this using FILE_UPLOAD_PERMISSIONS. Here,

FILE_UPLOAD_PERMISSIONS=0o640

…should do the trick.

(that's R/W for django user and read-only for nginx)

like image 155
spectras Avatar answered Oct 26 '22 02:10

spectras