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 Version 0.1
----------- -----------
- 0.1.6 (not released)
- Static files
- 0.1.5 - 0.1.5
- Cookies - Cookies
- Blueprint listeners and ordering - Blueprint listeners and ordering

View File

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

View File

@ -42,7 +42,7 @@ from sanic import Sanic
from my_blueprint import bp from my_blueprint import bp
app = Sanic(__name__) app = Sanic(__name__)
app.register_blueprint(bp) app.blueprint(bp)
app.run(host='0.0.0.0', port=8000, debug=True) 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) @bp.exception(NotFound)
def ignore_404s(request, exception): def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url)) 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 ## 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) 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): def add_middleware(self, middleware, *args, **kwargs):
""" """
Registers middleware to sanic Registers middleware to sanic
@ -112,3 +121,9 @@ class Blueprint:
self.record(lambda s: s.add_exception(handler, *args, **kwargs)) self.record(lambda s: s.add_exception(handler, *args, **kwargs))
return handler return handler
return decorator 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 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: class Handler:
handlers = None handlers = None

View File

@ -1,6 +1,9 @@
from aiofiles import open as open_async
from datetime import datetime from datetime import datetime
from http.cookies import SimpleCookie 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 = { COMMON_STATUS_CODES = {
200: b'OK', 200: b'OK',
@ -136,7 +139,7 @@ class HTTPResponse:
def json(body, status=200, headers=None): 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") content_type="application/json")
@ -148,3 +151,17 @@ def text(body, status=200, headers=None):
def html(body, status=200, headers=None): def html(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers, return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8") 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 # Mark the whole route as unhashable if it has the hash key in it
if re.search('(^|[^^]){1}/', pattern): if re.search('(^|[^^]){1}/', pattern):
properties['unhashable'] = True properties['unhashable'] = True
# Mark the route as unhashable if it matches the hash key
elif re.search(pattern, '/'):
properties['unhashable'] = True
return '({})'.format(pattern) return '({})'.format(pattern)

View File

@ -12,6 +12,7 @@ from .log import log, logging
from .response import HTTPResponse from .response import HTTPResponse
from .router import Router from .router import Router
from .server import serve from .server import serve
from .static import register as static_register
from .exceptions import ServerError from .exceptions import ServerError
@ -28,6 +29,9 @@ class Sanic:
self.loop = None self.loop = None
self.debug = None self.debug = None
# Register alternative method names
self.go_fast = self.run
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
# Registration # Registration
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #
@ -41,6 +45,11 @@ class Sanic:
:return: decorated function :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): def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler) self.router.add(uri=uri, methods=methods, handler=handler)
return handler return handler
@ -84,7 +93,17 @@ class Sanic:
attach_to = args[0] attach_to = args[0]
return register_middleware 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. Registers a blueprint on the application.
:param blueprint: Blueprint object :param blueprint: Blueprint object
@ -101,6 +120,12 @@ class Sanic:
self._blueprint_order.append(blueprint) self._blueprint_order.append(blueprint)
blueprint.register(self, options) 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 # Request Handling
# -------------------------------------------------------------------- # # -------------------------------------------------------------------- #

View File

@ -1,48 +1,59 @@
import re from aiofiles.os import stat
import os from os import path
from zlib import adler32 from re import sub
import mimetypes 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): def register(app, file_or_directory, uri, pattern, use_modified_since):
@app.middleware # TODO: Though sanic is not a file server, I feel like we should atleast
async def static_middleware(request): # make a good effort here. Modified-since is nice, but we could
url = request.url # also look into etags, expires, and caching
if url.startswith(url_prefix): """
filename = url[len(url_prefix):] Registers a static directory handler with Sanic by adding a route to the
if filename: router and registering a handler.
filename = secure_filename(filename) :param app: Sanic
filename = os.path.join(dirname, filename) :param file_or_directory: File or directory path to serve from
if os.path.isfile(filename): :param uri: URL to serve from
return sendfile(filename) :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( async def _handler(request, file_uri=None):
[os.path.sep, os.path.altsep or '']))) # 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
def secure_filename(path): # Strip all / that in the beginning of the URL to help prevent python
return _split.sub('', path) # 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
def sendfile(location, mimetype=None, add_etags=True): try:
headers = {} headers = {}
filename = os.path.split(location)[-1] # 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
with open(location, 'rb') as ins_file: return await file(file_path, headers=headers)
out_stream = ins_file.read() except:
raise FileNotFound('File not found',
path=file_or_directory,
relative_url=file_uri)
if add_etags: app.route(uri, methods=['GET'])(_handler)
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)

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 aiohttp.ClientSession(cookies=cookies) as session:
async with getattr(session, method)(url, *args, **kwargs) as response: async with getattr(session, method)(url, *args, **kwargs) as response:
response.text = await response.text() response.text = await response.text()
response.body = await response.read()
return response return response

View File

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

View File

@ -1,3 +1,5 @@
import inspect
from sanic import Sanic from sanic import Sanic
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.response import json, text from sanic.response import json, text
@ -17,7 +19,7 @@ def test_bp():
def handler(request): def handler(request):
return text('Hello') return text('Hello')
app.register_blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app) request, response = sanic_endpoint_test(app)
assert response.text == 'Hello' assert response.text == 'Hello'
@ -30,7 +32,7 @@ def test_bp_with_url_prefix():
def handler(request): def handler(request):
return text('Hello') return text('Hello')
app.register_blueprint(bp) app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/') request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'Hello' assert response.text == 'Hello'
@ -49,8 +51,8 @@ def test_several_bp_with_url_prefix():
def handler2(request): def handler2(request):
return text('Hello2') return text('Hello2')
app.register_blueprint(bp) app.blueprint(bp)
app.register_blueprint(bp2) app.blueprint(bp2)
request, response = sanic_endpoint_test(app, uri='/test1/') request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'Hello' assert response.text == 'Hello'
@ -70,7 +72,7 @@ def test_bp_middleware():
async def handler(request): async def handler(request):
return text('FAIL') return text('FAIL')
app.register_blueprint(blueprint) app.blueprint(blueprint)
request, response = sanic_endpoint_test(app) request, response = sanic_endpoint_test(app)
@ -97,7 +99,7 @@ def test_bp_exception_handler():
def handler_exception(request, exception): def handler_exception(request, exception):
return text("OK") return text("OK")
app.register_blueprint(blueprint) app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/1') request, response = sanic_endpoint_test(app, uri='/1')
assert response.status == 400 assert response.status == 400
@ -140,8 +142,24 @@ def test_bp_listeners():
def handler_6(sanic, loop): def handler_6(sanic, loop):
order.append(6) order.append(6)
app.register_blueprint(blueprint) app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/') 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