Tvorba internetových aplikací

Jakub Klinkovský

:: České vysoké učení technické v Praze
:: Fakulta jaderná a fyzikálně inženýrská
:: Katedra softwarového inženýrství

Akademický rok 2022-2023

Download a upload souborů

Jak funguje download

Tradiční způsob stahování souborů pomocí protokolu HTTP:

  1. URL určí umístění daného souboru na webu
  2. Klient vyšle požadavek metodou GET
  3. Server odešle odpověď s požadovanými daty
  4. Klient zpracuje odpověď (zobrazení multimediálního souboru v prohlížeči nebo uložení souboru na disk)

Tento postup je dostatečný pro malé soubory, které jsou typicky zobrazovány přímo v prohlížeči. Požadavky jsou vysílány a zpracovávány asynchronně, může jich být velké množství.

Efektivnější download

Pro zvýšení efektivity vznikla řada rozšíření protokolu HTTP:

  • pokračování přerušeného přenosu (hlavičky Accept-Range, Range, Content-Range a stavový kód 206 Partial Content)
  • kódování souboru po částech – umožňuje tzv. streaming (v protokolu HTTP/1.1 pomocí hlavičky Transfer-Encoding: chunked, HTTP/2 má vlastní mechanismy)
  • streamování dat s adaptivní změnou datového toku (vice na Wikipedii)

Pro specifické aplikace existují pokročilé protokoly (např. WebRTC pro real-time přenos obrazu a zvuku mezi klienty).

Jak funguje upload

  1. Klient má k dispozici dokument s formulářem, který obsahuje prvek (prvky) určené pro upload souboru (viz dále)
  2. Klient zakóduje data souboru do těla HTTP požadavku typu POST nebo PUT, který odešle na server
  3. Server zpracuje přijatá data (např. provede validaci, uloží do databáze nebo na disk)
  4. Server odešle odpověď obsahující výsledek zpracování uploadovaných dat

Samotný přenos dat protokolem HTTP probíhá analogicky jako v případě downloadu.

Upload a bezpečnost

Aplikace umožňující upload souborů je potenciálně nebezpečná:

  1. Maximální velikost uploadovaných souborů by měla být omezená.
  2. S uploadovanými soubory by se vždy mělo zacházet jako se statickými daty.
    Nikdy nespouštějte skripty uploadované z neověřeného zdroje!
  3. Typ dat by se měl vždy validovat (např. jestli PNG obrázek obsahuje opravdu obrazová data a ne třeba HTML).

Pro validaci dat neexistuje žádné univerzální řešení. Různé způsoby pro omezení bezpečnostních důsledků si vysvětlíme a ukážeme později.

Formuláře pro upload souborů

Pro upload souborů je nutné specifikovat typ kódování dat pomocí form-data. K tomu slouží atribut enctype značky <form>:

<form enctype="multipart/form-data" action="/upload/" method="post">
    ...
</form>

Prvky formuláře pro upload souborů

Existuje značka <input type="file">. Příklad:

<label for="avatar">Choose a profile picture:</label>

<input type="file"
       id="avatar" name="avatar"
       accept="image/png, image/jpeg">

Atributy:

  • accept – seznam akceptovaných typů dat
  • capture – umožňuje vytvořit nový soubor (obrázek, video, zvuk) pomocí kamery
    (v současnosti podporují jen prohlížeče mobilních zařízení)
  • multiple – umožňuje vybrat více souborů současně

Formuláře pro upload v Django

Prvky objektových formulářů: FileField a ImageField (třída odvozená od FileField).

Příklad:

from django import forms

class UploadFileForm(forms.Form):
    file = forms.FileField()

Pozor: atribut max_length pro FileField omezuje délku názvu souboru, ne obsahu!

Atribut allow_empty_file určuje, jestli formulář při validaci dat akceptuje soubory s prázdným obsahem.

Vytvoření prázdného formuláře

Pro zobrazení prázdného formuláře stačí jednoduchá funkce ve views.py:

def index(request):
    variables = {"form": forms.UploadFileForm()}
    return render(request, "pokus/index.html", variables)

Při formátování formuláře v šabloně je nutné uvést atribut enctype pro značku <form>, např.:

<form enctype="multipart/form-data" action="{% url 'upload' %}" method="post">
    {% csrf_token %}
    {{ form.as_div }}
    <input type="submit" value="Send">
</form>

Zpracování formuláře s uploadem

V souboru views.py vytvoříme funkci, která bude zpracovávat pouze upload:

def upload(request):
    # upload must be submitted via the POST method
    if request.method != 'POST':
        # return HTTP 405: Method Not Allowed
        return HttpResponseNotAllowed(["POST"])

    ...

Objekt HttpResponseNotAllowed představuje odpověď se stavovým kódem HTTP 405.

Zpracování formuláře s uploadem

Dále vytvoříme instanci formuláře navázanou na slovníky s parametry požadavku (request.POST) a uploadovanými soubory (request.FILES):

    ...

    # create a form with data from the request
    form = forms.UploadFileForm(request.POST, request.FILES)

    # check if the form is valid
    if not form.is_valid():
        # render the template showing errors in the form
        variables = {"form": form}
        return render(request, "pokus/index.html", variables)

    ...

Zpracování formuláře s uploadem

Po validaci formuláře zpracujeme samotná data uploadovaných souborů. V tomto příkladu jen soubor request.FILES["file"], kde "file" odpovídá atributu file v daném formuláři. Objekty ve slovníku FILES mají typ UploadedFile.

    ...

    # do something with the uploaded file
    save_uploaded_file(request.FILES["file"])

    # Always return an HttpResponseRedirect after successfully dealing
    # with POST data. This prevents data from being posted twice if the
    # user hits the <Back> or <Reload> button.
    return HttpResponseRedirect(reverse("index"))

Pohled do nitra – upload handlers

Jakmile Django začne zpracovávat požadavek (objekt request), data uploadovaných souborů ještě nemusí být k dispozici. V průběhu zpracování jsou různé atributy požadavku, včetně uploadovaných souborů, vytvářeny stylem on demand.

Přístup ke slovníku request.FILES vyvolá použití tzv. file handler objektů, které se postarají o samotný upload na dočasné místo a vytvoření objektu UploadedFile. Defaultní nastavení v settings.py je:

FILE_UPLOAD_HANDLERS = [
    'django.core.files.uploadhandler.MemoryFileUploadHandler',
    'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]

Do velikosti 2.5 MB se použije MemoryFile, jinak TemporaryFile na disku.

Lze implementovat vlastní file handler (např. ProgressBarUploadHandler pro AJAX).

Práce se soubory a adresáři v Pythonu

Modul pathlib poskytuje třídy pro reprezentaci cest v souborovém systému a příslušné operace. Základní použití:

from pathlib import Path

p = Path(".")
for x in p.iterdir():   # x je opět Path
    print(x)

# spojení několika cest
q = p / "Downloads" / "test.txt"
print(q)

Přehled atributů a metod najdete v dokumentaci. Nejdůležitější jsou:

  • metody: exists(), is_dir(), is_file(), iterdir(), mkdir()
  • atributy: name, parent, stem, suffix

Speciální proměnná __file__

V každém modulu existuje objekt __file__ – string obsahující cestu k souboru, ze kterého se modul importoval.

Příklad:

V souboru HelloWorld.py:

def hello():
    print("Hello, world!")

V jiném souboru:

import HelloWorld

HelloWorld.hello()
print(HelloWorld.__file__)
print(__file__)

Otevření souboru v Pythonu

Funkce open() slouží k otevření souboru na disku pro čtení nebo zápis. Typicky se používá s klíčovým slovem with:

with open("output.txt", "w", encoding="utf-8") as f:
    print("Hello, world!", file=f)

Druhý parametr určuje způsob otevření souboru. Možnosti:

  • r (čtení), w (zápis od začátku), a (zápis na konec)
  • x (exkluzivní otevření nového souboru, selže pokud soubor již existuje)
  • t (textový režim, defaultní), b (binární režim)
  • + (aktualizování, tj. čtení i zápis)

Tyto znaky lze různě kombinovat, např. "rwb" pro čtení i zápis v binárním režimu.

Více příkladů najdete v sekci Reading and Writing Files.

Další moduly pro práci se soubory

  • os.path – starší alternativa k modulu pathlib
  • shutil – vysokoúrovňové operace se soubory a stromovými strukturami na disku (kopírování, přesouvání, mazání, archivování)
  • tempfile – vytváření dočasných souborů a adresářů
  • filecmp – porovnávání souborů a adresářů
  • fileinput – pomocný modul pro čtení z několika souborů v jednom for-cyklu

A další nízkoúrovňové moduly, viz oficiální přehled.

Dokončení zpracování uploadu

Funkci save_uploaded_file můžeme implementovat takto:

def save_uploaded_file(uploaded_file):
    # choose upload directory
    upload_dir = Path(__file__).parent / "uploads"

    # create the directory
    upload_dir.mkdir(parents=True, exist_ok=True)

    # set upload file path
    upload_path = upload_dir / uploaded_file.name

    # save the uploaded data
    with open(upload_path, "wb+") as f:
        for chunk in uploaded_file.chunks():
            f.write(chunk)

Uploadování více souborů

Pro umožnění uploadu více souborů v jednom fieldu jsou potřeba tyto změny:

  1. Použít atribut multiple pro značku <input type="file">. V objektovém světě Django se to provede následovně:

    class UploadFileForm(forms.Form):
        file = forms.FileField(widget=forms.ClearableFileInput(
                                                 attrs={"multiple": True}))
    
  2. Zpracovat seznam souborů ve funkci ve views.py:

    def upload(request):
        ...
        # do something with the uploaded files
        for file in request.FILES.getlist("file"):
            save_uploaded_file(file)
        ...
    

Omezení typu souborů

  1. Pro značku <input type="file"> je vhodné použít atribut accept:

    class UploadFileForm(forms.Form):
        file = forms.FileField(
            widget=forms.ClearableFileInput(attrs={"accept": "application/pdf"})
        )
    
  2. Na straně aplikace by se měla provést validace, minimálně ověřit content_type pro UploadedFile ve funkci save_uploaded_file:

        ...
        if uploaded_file.content_type != "application/pdf":
            raise ValidationError("only PDF files are allowed")
        ...
    

    Obsah souboru by se měl také ověřit!

Objektové zpracování uploadu a spolupráce s databází

Django umožňuje zpracovat uploadované soubory ve spolupráci s modely:

  • třída FileField pro definici atributů modelu
  • propojení uploadovaných souborů s objektově-relačním mapováním
  • data se uloží na disku a v databázi se uloží jen cesta k souboru

Příklad: definice v models.py:

from django.db import models

class MyModel(models.Model):
    # file will be uploaded to MEDIA_ROOT/uploads
    upload = models.FileField(upload_to="uploads/")

Alternativně lze použít tokeny pro formátování funkcí strftime(), např.
upload_to="uploads/%Y/%m/%d/" uloží soubory do MEDIA_ROOT/uploads/2023/04/03

Volby pro umístění uploadovaných souborů

V souboru settings.py lze definovat:

  • MEDIA_ROOT – umístění souborů na disku
  • MEDIA_URL – část URL pro přístup k souborům ze strany klienta

Např.:

MEDIA_ROOT = "/media/mysite/"
MEDIA_URL = "media/"

V urls.py je potřeba nastavit mapování (funguje jen v debug režimu):

from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
    ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Zpracování uploadu pomocí modelu s atributem FileField

Funkce upload v souboru views.py bude vypadat stejně jako v předchozím příkladu, až na to, že místo použití funkce save_uploaded_file:

    # do something with the uploaded file
    save_uploaded_file(request.FILES["file"])

vytvoříme instanci modelu ("file" odpovídá atributu v definici formuláře a upload= odpovídá atributu v definici modelu):

    # save the uploaded file to a model
    instance = models.MyModel(upload=request.FILES["file"])
    instance.save()

Metoda save() se postará o správné uložení dat, tj. uložení souboru na disk a cesty k souboru do databáze.

Práce se soubory uloženými pomocí modelu

K souborům uloženým tímto způsobem se přistupuje pomocí třídy FieldFile. Např.:

f = MyModel.objects.first()     # instance třídy MyModel
f.upload                        # instance třídy FieldFile

Důležité atributy:

  • name – relativní cesta vzhledem k úložišti
  • path – cesta k souboru v souborovém systému
  • url – relativní URL pro přístup k danému souboru

Poznámka: Všimněte si, že v definici modelu se použila třída FileField.

Příklad: zobrazení seznamu uploadovaných souborů

Ve funkci index v souboru views.py přidáme dotaz na záznamy v databázi:

def index(request):
    ...

    variables["uploads"] = models.MyModel.objects.all()
    return render(request, "pokus/index.html", variables)

V šabloně naformátujeme seznam souborů s odkazy:

<ul>
{% for file in uploads %}
    <li><a href="{{ file.upload.url }}">{{ file.upload.name }}</a></li>
{% endfor %}
</ul>

Správa souborů a úložiště

Defaultní úložiště pro soubory v Django je lokální souborový systém – viz MEDIA_ROOT.
Kromě toho je možné implementovat vlastní úložiště – např. pro vzdálené systémy.
Projekt django-storages poskytuje rozhraní pro Amazon S3, Dropbox, atd.

Více o správě souborů v Django najdete v sekci Managing files.

Použití třídy ImageField

Místo třídy FileField lze v definici modelu použít třídu ImageField, která poskytuje dodatečné funkce pro zpracování obrázků:

  • pro značku <input> se automaticky použije atribut accept="image/*"
  • v aplikaci se provede validace koncovky souboru (např. .jpg, .png, atd.)
  • validace obrazových dat v souboru (ale není to 100%...)
  • využívá knihovnu Pillow, kterou je nutné nainstalovat:
    pip install Pillow
    
  • objekt UploadedFile má dodatečný atribut image, který odpovídá instanci PIL.Image použité pro validaci – soubor se ale po validaci uzavře, takže jsou k dispozici jen metadata (např. atributy format, width, height)

Příklad použití třídy ImageField

class ImageForm(forms.Form):
    img = forms.ImageField()
form = forms.ImageUploadForm(request.POST, request.FILES)
if form.is_valid():
    img_field = form.cleaned_data["img"]    # instance UploadedFile
    img = img_field.image                   # instance PIL.Image
    metadata = {
        "format": img.format,
        "width": img.width,
        "height": img.height,
    }
    # pokud potřebujeme data, musíme soubor znovu otevřít
    image = PIL.Image.open(img_field)