views.py 3.45 KB
Newer Older
1
2
import os

3
from PIL import Image, ImageOps
Thom Wiggers's avatar
Thom Wiggers committed
4
from django.core.exceptions import SuspiciousFileOperation
5
from django.conf import settings
6
from django.contrib.auth.decorators import login_required
Joost Rijneveld's avatar
Joost Rijneveld committed
7
from django.http import Http404
8
9
10
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlunquote
11
12
13
from sendfile import sendfile


Thom Wiggers's avatar
Thom Wiggers committed
14
15
16
def _private_thumbnails_unauthed(request, size_fit, original_path):
    """
    Serve thumbnails from the filesystem
17

Thom Wiggers's avatar
Thom Wiggers committed
18
    This layer of indirection makes it possible to make exceptions
19
    to the authentication requirements for thumbnails, e.g. when sharing
Thom Wiggers's avatar
Thom Wiggers committed
20
21
22
23
24
25
26
27
    photo albums with external parties using access tokens.
    """
    thumbpath = os.path.join(settings.MEDIA_ROOT, 'thumbnails', size_fit)
    path = os.path.normpath(os.path.join(thumbpath, original_path))
    if not os.path.commonprefix([thumbpath, path]).startswith(thumbpath):
        raise SuspiciousFileOperation(
            "Path traversal detected: someone tried to download "
            "{}, input: {}".format(path, original_path))
Joost Rijneveld's avatar
Joost Rijneveld committed
28
29
    if not os.path.isfile(path):
        raise Http404("Thumbnail not found.")
30
    return sendfile(request, path)
31
32
33
34
35


@login_required
def private_thumbnails(request, size_fit, path):
    return _private_thumbnails_unauthed(request, size_fit, path)
36
37
38


def generate_thumbnail(request, size_fit, path, thumbpath):
39
40
41
42
    """
    Generate thumbnail and redirect user to new location

    The thumbnails are generated with this route. Because the
43
44
45
46
47
48
    thumbnails will be generated in parallel, it will not block
    page load when many thumbnails need to be generated.
    After it is done, the user is redirected to the new location
    of the thumbnail."""
    thumbpath = urlunquote(thumbpath)
    path = urlunquote(path)
49
50
51
    full_thumbpath = os.path.normpath(
        os.path.join(settings.MEDIA_ROOT, thumbpath))
    full_path = os.path.normpath(os.path.join(settings.MEDIA_ROOT, path))
52
53
    size, fit = size_fit.split('_')

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    public_media = os.path.join(settings.MEDIA_ROOT, 'public')
    thumb_root = os.path.join(settings.MEDIA_ROOT, 'thumbnails', size_fit)

    public_img = False
    if (os.path.commonprefix([full_thumbpath, full_path])
            .startswith(public_media)):
        public_img = True
    elif not (os.path.commonprefix([full_thumbpath, thumb_root])
              .startswith(thumb_root) and
              os.path.commonprefix([full_path, settings.MEDIA_ROOT])
              .startswith(settings.MEDIA_ROOT)):
        raise SuspiciousFileOperation(
            "Path traversal detected: someone tried to generate a thumb from "
            "{} to {}".format(full_path, full_thumbpath))

69
70
71
72
73
74
75
76
77
78
79
    os.makedirs(os.path.dirname(full_thumbpath), exist_ok=True)
    if (not os.path.isfile(full_thumbpath) or
            os.path.getmtime(full_path) > os.path.getmtime(full_thumbpath)):
        image = Image.open(full_path)
        size = tuple(int(dim) for dim in size.split('x'))
        if not fit:
            ratio = min([a / b for a, b in zip(size, image.size)])
            size = tuple(int(ratio * x) for x in image.size)
        thumb = ImageOps.fit(image, size, Image.ANTIALIAS)
        thumb.save(full_thumbpath)

80
81
    # We can instantly redirect to the new correct image url if public
    if public_img:
82
83
84
85
86
        return redirect(settings.MEDIA_URL + thumbpath, permanent=True)

    # Otherwise redirect to the route with an auth check
    return redirect(
        reverse('private-thumbnails', args=[size_fit, path]),
87
        permanent=True)