Made static file serving part of Sanic

Added sanic.static, blueprint.static, documentation, and testing
This commit is contained in:
Channel Cat 2016-10-24 01:21:06 -07:00
parent d7fff12b71
commit bf6879e46f
14 changed files with 209 additions and 52 deletions

View File

@ -1,5 +1,7 @@
Version 0.1
-----------
- 0.1.6 (not released)
- Static files
- 0.1.5
- Cookies
- Blueprint listeners and ordering

View File

@ -51,6 +51,7 @@ app.run(host="0.0.0.0", port=8000)
* [Exceptions](docs/exceptions.md)
* [Blueprints](docs/blueprints.md)
* [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md)
* [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)

View File

@ -42,7 +42,7 @@ from sanic import Sanic
from my_blueprint import bp
app = Sanic(__name__)
app.register_blueprint(bp)
app.blueprint(bp)
app.run(host='0.0.0.0', port=8000, debug=True)
```
@ -79,6 +79,12 @@ Exceptions can also be applied exclusively to blueprints globally.
@bp.exception(NotFound)
def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
## Static files
Static files can also be served globally, under the blueprint prefix.
```python
bp.static('/folder/to/serve', '/web/path')
```
## Start and Stop

18
docs/static_files.md Normal file
View File

@ -0,0 +1,18 @@
# Static Files
Both directories and files can be served by registering with static
## Example
```python
app = Sanic(__name__)
# Serves files from the static folder to the URL /static
app.static('./static', '/static')
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
# is requested
app.static('/home/ubuntu/test.png', '/the_best.png')
app.run(host="0.0.0.0", port=8000)
```

View File

@ -33,6 +33,15 @@ class BlueprintSetup:
"""
self.app.exception(*args, **kwargs)(handler)
def add_static(self, file_or_directory, uri, *args, **kwargs):
"""
Registers static files to sanic
"""
if self.url_prefix:
uri = self.url_prefix + uri
self.app.static(file_or_directory, uri, *args, **kwargs)
def add_middleware(self, middleware, *args, **kwargs):
"""
Registers middleware to sanic
@ -112,3 +121,9 @@ class Blueprint:
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
return handler
return decorator
def static(self, file_or_directory, uri, *args, **kwargs):
"""
"""
self.record(
lambda s: s.add_static(file_or_directory, uri, *args, **kwargs))

View File

@ -21,6 +21,15 @@ class ServerError(SanicException):
status_code = 500
class FileNotFound(NotFound):
status_code = 404
def __init__(self, message, path, relative_url):
super().__init__(message)
self.path = path
self.relative_url = relative_url
class Handler:
handlers = None

View File

@ -1,6 +1,9 @@
from aiofiles import open as open_async
from datetime import datetime
from http.cookies import SimpleCookie
import ujson
from mimetypes import guess_type
from os import path
from ujson import dumps as json_dumps
COMMON_STATUS_CODES = {
200: b'OK',
@ -136,7 +139,7 @@ class HTTPResponse:
def json(body, status=200, headers=None):
return HTTPResponse(ujson.dumps(body), headers=headers, status=status,
return HTTPResponse(json_dumps(body), headers=headers, status=status,
content_type="application/json")
@ -148,3 +151,17 @@ def text(body, status=200, headers=None):
def html(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None):
filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file:
out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=200,
headers=headers,
content_type=mime_type,
body_bytes=out_stream)

View File

@ -82,6 +82,9 @@ class Router:
# Mark the whole route as unhashable if it has the hash key in it
if re.search('(^|[^^]){1}/', pattern):
properties['unhashable'] = True
# Mark the route as unhashable if it matches the hash key
elif re.search(pattern, '/'):
properties['unhashable'] = True
return '({})'.format(pattern)

View File

@ -12,6 +12,7 @@ from .log import log, logging
from .response import HTTPResponse
from .router import Router
from .server import serve
from .static import register as static_register
from .exceptions import ServerError
@ -28,6 +29,9 @@ class Sanic:
self.loop = None
self.debug = None
# Register alternative method names
self.go_fast = self.run
# -------------------------------------------------------------------- #
# Registration
# -------------------------------------------------------------------- #
@ -41,6 +45,11 @@ class Sanic:
:return: decorated function
"""
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith('/'):
uri = '/' + uri
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler)
return handler
@ -84,7 +93,17 @@ class Sanic:
attach_to = args[0]
return register_middleware
def register_blueprint(self, blueprint, **options):
# Static Files
def static(self, file_or_directory, uri, pattern='.+',
use_modified_since=True):
"""
Registers a root to serve files from. The input can either be a file
or a directory. See
"""
static_register(self, file_or_directory, uri, pattern,
use_modified_since)
def blueprint(self, blueprint, **options):
"""
Registers a blueprint on the application.
:param blueprint: Blueprint object
@ -101,6 +120,12 @@ class Sanic:
self._blueprint_order.append(blueprint)
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead")
return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- #
# Request Handling
# -------------------------------------------------------------------- #

View File

@ -1,48 +1,59 @@
import re
import os
from zlib import adler32
import mimetypes
from aiofiles.os import stat
from os import path
from re import sub
from time import strftime, gmtime
from sanic.response import HTTPResponse
from .exceptions import FileNotFound, InvalidUsage
from .response import file, HTTPResponse
def setup(app, dirname, url_prefix):
@app.middleware
async def static_middleware(request):
url = request.url
if url.startswith(url_prefix):
filename = url[len(url_prefix):]
if filename:
filename = secure_filename(filename)
filename = os.path.join(dirname, filename)
if os.path.isfile(filename):
return sendfile(filename)
def register(app, file_or_directory, uri, pattern, use_modified_since):
# TODO: Though sanic is not a file server, I feel like we should atleast
# make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching
"""
Registers a static directory handler with Sanic by adding a route to the
router and registering a handler.
:param app: Sanic
:param file_or_directory: File or directory path to serve from
:param uri: URL to serve from
:param pattern: regular expression used to match files in the URL
:param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the server's
"""
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
uri += '<file_uri:' + pattern + '>'
_split = re.compile(r'[\0%s]' % re.escape(''.join(
[os.path.sep, os.path.altsep or ''])))
async def _handler(request, file_uri=None):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \
if file_uri else file_or_directory
try:
headers = {}
# Check if the client has been sent this file before
# and it has not been modified since
if use_modified_since:
stats = await stat(file_path)
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT',
gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since:
return HTTPResponse(status=304)
headers['Last-Modified'] = modified_since
def secure_filename(path):
return _split.sub('', path)
return await file(file_path, headers=headers)
except:
raise FileNotFound('File not found',
path=file_or_directory,
relative_url=file_uri)
def sendfile(location, mimetype=None, add_etags=True):
headers = {}
filename = os.path.split(location)[-1]
with open(location, 'rb') as ins_file:
out_stream = ins_file.read()
if add_etags:
headers['ETag'] = '{}-{}-{}'.format(
int(os.path.getmtime(location)),
hex(os.path.getsize(location)),
adler32(location.encode('utf-8')) & 0xffffffff)
mimetype = mimetype or mimetypes.guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=200,
headers=headers,
content_type=mimetype,
body_bytes=out_stream)
app.route(uri, methods=['GET'])(_handler)

View File

@ -11,6 +11,7 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
async with aiohttp.ClientSession(cookies=cookies) as session:
async with getattr(session, method)(url, *args, **kwargs) as response:
response.text = await response.text()
response.body = await response.read()
return response

View File

@ -17,6 +17,7 @@ setup(
'uvloop>=0.5.3',
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
],
classifiers=[
'Development Status :: 2 - Pre-Alpha',

View File

@ -1,3 +1,5 @@
import inspect
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json, text
@ -17,7 +19,7 @@ def test_bp():
def handler(request):
return text('Hello')
app.register_blueprint(bp)
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
assert response.text == 'Hello'
@ -30,7 +32,7 @@ def test_bp_with_url_prefix():
def handler(request):
return text('Hello')
app.register_blueprint(bp)
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'Hello'
@ -49,8 +51,8 @@ def test_several_bp_with_url_prefix():
def handler2(request):
return text('Hello2')
app.register_blueprint(bp)
app.register_blueprint(bp2)
app.blueprint(bp)
app.blueprint(bp2)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'Hello'
@ -70,7 +72,7 @@ def test_bp_middleware():
async def handler(request):
return text('FAIL')
app.register_blueprint(blueprint)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app)
@ -97,7 +99,7 @@ def test_bp_exception_handler():
def handler_exception(request, exception):
return text("OK")
app.register_blueprint(blueprint)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/1')
assert response.status == 400
@ -140,8 +142,24 @@ def test_bp_listeners():
def handler_6(sanic, loop):
order.append(6)
app.register_blueprint(blueprint)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/')
assert order == [1,2,3,4,5,6]
assert order == [1,2,3,4,5,6]
def test_bp_static():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
blueprint = Blueprint('test_static')
blueprint.static(current_file, '/testing.file')
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents

30
tests/test_static.py Normal file
View File

@ -0,0 +1,30 @@
import inspect
import os
from sanic import Sanic
from sanic.utils import sanic_endpoint_test
def test_static_file():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
app.static(current_file, '/testing.file')
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents
def test_static_directory():
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
app.static(current_directory, '/dir')
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py')
assert response.status == 200
assert response.body == current_file_contents