Skip to content

Commit

Permalink
feature: Provide an example to ensure uploads via Uppy works well
Browse files Browse the repository at this point in the history
As well as provide docs for chunk size configuration and provide
`make example` target to run example apps.
  • Loading branch information
playpauseandstop committed Mar 19, 2020
1 parent f4daab0 commit 261a4dc
Show file tree
Hide file tree
Showing 17 changed files with 394 additions and 59 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
- name: "Install package"
run: |
poetry config virtualenvs.in-project true
poetry install --no-dev
poetry install
python -m pip install tox==3.14.5 tox-gh-actions==1.1.0
- name: "Cache pre-commit"
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
ChangeLog
=========

1.0.0rc0 (In Development)
=========================

- Add example to ensure that upload via `Uppy <https://uppy.io>`_ JavaScript library
works as expected

1.0.0b2 (2020-03-18)
====================

Expand Down
15 changes: 10 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ PYTHON ?= $(POETRY) run python
SPHINXBUILD ?= $(POETRY) run sphinx-build
TOX ?= tox

# Test constants
TEST_APP_PORT = 8300
# Example constants
AIOHTTP_PORT = 8300

all: install

Expand All @@ -38,6 +38,14 @@ docs: .install
$(PYTHON) -m pip install -r docs/requirements.txt
$(MAKE) -C docs/ SPHINXBUILD="$(SPHINXBUILD)" html

example: .install
ifeq ($(EXAMPLE),)
# EXAMPLE env var is required, e.g. `make EXAMPLE=uploads example`
@exit 1
else
$(PYTHON) -m aiohttp.web --port $(AIOHTTP_PORT) examples.$(EXAMPLE):create_app
endif

install: .install
.install: pyproject.toml poetry.lock
$(POETRY) config virtualenvs.in-project true
Expand All @@ -57,9 +65,6 @@ open-docs: docs

test: install clean lint test-only

test-app:
$(PYTHON) -m aiohttp.web --port $(TEST_APP_PORT) tests.app:create_app

test-only:
rm -rf tests/test-uploads/
TOXENV=$(TOXENV) $(TOX) $(TOX_ARGS) -- $(TEST_ARGS)
2 changes: 1 addition & 1 deletion aiohttp_tus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

__all__ = ("setup_tus",)
__license__ = "BSD-3-Clause"
__version__ = "1.0.0b2"
__version__ = "1.0.0rc0"
55 changes: 55 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,61 @@ To allow upload files to ``../uploads`` directory for all clients via ``/uploads
upload_path=Path(__file__).parent.parent / "uploads"
)
Understanding tus.io Chunk Size
===============================

By default, `Uppy <https://uppy.io>`_ and some other tus.io clients do not setup chunk
size and tries to upload as large chunk, as possible. However as
:class:`aiohttp.web.Application` setting up ``client_max_size`` on app initialization
you might need to configure server to receive larger chunks as well as setup tus.io
client to use respected chunk sizes.

Examples below shown on how to config different parts to upload files with chunk size
of **4MB** (``4_000_000`` bytes)

aiohttp.web configuration
-------------------------

.. code-block:: python
from aiohttp import web
from aiohttp_tus import setup_tus
app = web.Application(client_max_size=4_000_000)
nginx configuration
-------------------

.. code-block:: nginx
location ~ ^/uploads.*$ {
client_max_body_size 4M;
proxy_pass http://localhost:8080;
}
tus.py configuration
--------------------

.. code-block:: bash
tus-upload --chunk-size=4000000 \
/path/to/large-file http://localhost:8080/uploads
uppy.io Configuration
---------------------

.. code-block:: javascript
uppy.use(Uppy.Tus, {
endpoint: "http://localhost:8080/uploads",
chunkSize: 3999999
})
.. important::
To make `Uppy.Tus <https://uppy.io/docs/tus/>`_ plugin work you need to specify
chunk size **at least 1 byte smaller** than ``client_max_size``. If you'll provide
chunk size equals to client max size upload will not work properly.

User Uploads
============

Expand Down
51 changes: 51 additions & 0 deletions examples/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
====================
aiohttp-tus Examples
====================

aiohttp_tus_app
===============

To illustrate that uploading via `tus.py <https://pypi.org/project/tus.py/>` library
works well.

To run,

.. code-block:: bash
make -C .. EXAMPLE=aiohttp_tus_app example
After, upload large files as,

.. code-block:: bash
poetry run tus-upload --chunk-size=1000000 /path/to/large-file http://localhost:8300/uploads
Then check that files uploaded to upload directory.

uploads
=======

To illustrate that uploading via `Uppy <https://uppy.io>`_ JavaScript library works
as expected.

To run,

.. code-block:: bash
make -C .. EXAMPLE=uploads example
After, open ``http://localhost:8080`` to try upload. All uploads will be available in
temporary directory.

.. important::
This example uses chunks size of 4MB, but you might want customize things by
setting up other chunk size via ``AIOHTTP_CLIENT_MAX_SIZE`` env var.

For example,

.. code-block:: bash
AIOHTTP_CLIENT_MAX_SIZE=10MB make -C .. EXAMPLE=uploads example
will set `client_max_size <https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Application>`_
param to ``10_000_000`` bytes.
Empty file added examples/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions examples/aiohttp_tus_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import shutil
import tempfile
from pathlib import Path
from typing import List

from aiohttp import web

from aiohttp_tus import setup_tus


def create_app(argv: List[str] = None) -> web.Application:
upload_path = Path(tempfile.gettempdir()) / "aiohttp-tus-app-uploads"
upload_path.mkdir(mode=0o755, exist_ok=True)

print("aiohttp_tus test app")
print(f"Uploading files to {upload_path.absolute()}\n")

app = setup_tus(web.Application(), upload_path=upload_path)

app["aiohttp_tus_upload_path"] = upload_path
app.on_shutdown.append(remove_upload_path)

return app


async def remove_upload_path(app: web.Application) -> None:
upload_path = app["aiohttp_tus_upload_path"]
print(f"\nRemoving {upload_path.absolute()} directory")
shutil.rmtree(upload_path)
3 changes: 3 additions & 0 deletions examples/uploads/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .app import create_app

__all__ = ("create_app",)
40 changes: 40 additions & 0 deletions examples/uploads/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import shutil
import tempfile
from pathlib import Path
from typing import List

import jinja2
from aiohttp import web
from aiohttp_jinja2 import setup as setup_jinja2

from aiohttp_tus import setup_tus
from . import views
from .constants import APP_UPLOAD_PATH_KEY
from .utils import get_client_max_size


def create_app(argv: List[str] = None) -> web.Application:
upload_path = Path(tempfile.gettempdir()) / "aiohttp-tus-example-uploads"
upload_path.mkdir(mode=0o755, exist_ok=True)

app = setup_tus(
web.Application(client_max_size=get_client_max_size()), upload_path=upload_path,
)
app[APP_UPLOAD_PATH_KEY] = upload_path
setup_jinja2(
app, loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates")
)

app.router.add_get("/", views.index)
app.on_shutdown.append(remove_upload_path)

print("aiohttp-tus example app")
print(f"Uploading files to {upload_path.absolute()}\n")

return app


async def remove_upload_path(app: web.Application) -> None:
upload_path = app[APP_UPLOAD_PATH_KEY]
print(f"\nRemoving {upload_path.absolute()}")
shutil.rmtree(upload_path)
2 changes: 2 additions & 0 deletions examples/uploads/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
APP_UPLOAD_PATH_KEY = "aiohttp_tus_upload_path"
DEFAULT_CLIENT_MAX_SIZE = "4MB"
72 changes: 72 additions & 0 deletions examples/uploads/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!DOCTYPE html>

<html>
<head>
<title>aiohttp-tus &amp; uppy.io example</title>
<meta charset="utf-8" />

<!-- Load Uppy CSS bundle. It is advisable to install Uppy
from npm/yarn instead, and pick and choose the plugins/styles you need.
But for experimenting, you can use Transloadit’s CDN, Edgly: -->
<link rel="stylesheet" href="https://transloadit.edgly.net/releases/uppy/v1.10.1/uppy.min.css">
</head>

<body>
<h1>
<a href="https://aiohttp-tus.readthedocs.io">aiohttp-tus</a> &amp;
<a href="https://uppy.io">uppy.io</a> example
</h1>

<ul className="margin-bottom: 0.5rem">
<li>aiohttp-tus version: <code>1.0.0rc0</code></li>
<li>Uppy.io version: <code>1.10.1</code></li>
</ul>

<div class="UppyDragDrop"></div>
<div class="UppyDragDrop-Progress" style="margin-top: 0.5rem"></div>

<div class="uploaded-files">
<h4>Uploaded files:</h4>
<ol></ol>
</div>

<div class="help-text" style="margin-top: 0.5rem">
<i>Allowed to upload up to <b>10 files</b> per session.</i>
</div>

<!-- Load Uppy JS bundle. -->
<script src="https://transloadit.edgly.net/releases/uppy/v1.10.1/uppy.min.js"></script>
<script>
var uppy = Uppy.Core({
debug: true,
autoProceed: true,
restrictions: {
minNumberOfFiles: 1,
maxNumberOfFiles: 10
}
});
uppy.use(Uppy.DragDrop, {
target: '.UppyDragDrop'
});
uppy.use(Uppy.StatusBar, {
target: '.UppyDragDrop-Progress',
hideUploadButton: true,
hideAfterFinish: false
});
uppy.use(Uppy.Tus, {
chunkSize: {{ chunk_size }},
endpoint: '/uploads',
limit: 5
});
uppy.on('upload-success', function (file, response) {
var url = response.uploadURL
var fileName = file.name

document.querySelector('.uploaded-files ol').innerHTML +=
'<li>' + fileName + '</li>'
});

console.log('--> Uppy pre-built version with Tus & DragDrop has loaded');
</script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/uploads/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import os

import humanfriendly

from .constants import DEFAULT_CLIENT_MAX_SIZE


def get_client_max_size() -> int:
return humanfriendly.parse_size( # type: ignore
os.getenv("AIOHTTP_CLIENT_MAX_SIZE") or DEFAULT_CLIENT_MAX_SIZE
)
8 changes: 8 additions & 0 deletions examples/uploads/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from aiohttp import web
from aiohttp_jinja2 import render_template


async def index(request: web.Request) -> web.Response:
return render_template(
"index.html", request, {"chunk_size": request.app._client_max_size - 1}
)

0 comments on commit 261a4dc

Please sign in to comment.