| @@ -1,5 +1,7 @@ | ||||
| Version 0.1 | ||||
| ----------- | ||||
|  - 0.1.6 (not released) | ||||
|   - Static files | ||||
|  - 0.1.5  | ||||
|   - Cookies | ||||
|   - Blueprint listeners and ordering | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										18
									
								
								docs/static_files.md
									
									
									
									
									
										Normal 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) | ||||
| ``` | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -13,6 +13,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 | ||||
|  | ||||
|  | ||||
| @@ -29,6 +30,9 @@ class Sanic: | ||||
|         self.loop = None | ||||
|         self.debug = None | ||||
|  | ||||
|         # Register alternative method names | ||||
|         self.go_fast = self.run | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Registration | ||||
|     # -------------------------------------------------------------------- # | ||||
| @@ -42,6 +46,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 | ||||
| @@ -85,7 +94,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 | ||||
| @@ -102,6 +121,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 | ||||
|     # -------------------------------------------------------------------- # | ||||
|   | ||||
							
								
								
									
										59
									
								
								sanic/static.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								sanic/static.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| from aiofiles.os import stat | ||||
| from os import path | ||||
| from re import sub | ||||
| from time import strftime, gmtime | ||||
|  | ||||
| from .exceptions import FileNotFound, InvalidUsage | ||||
| from .response import file, HTTPResponse | ||||
|  | ||||
|  | ||||
| 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 + '>' | ||||
|  | ||||
|     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 | ||||
|  | ||||
|             return await file(file_path, headers=headers) | ||||
|         except: | ||||
|             raise FileNotFound('File not found', | ||||
|                                path=file_or_directory, | ||||
|                                relative_url=file_uri) | ||||
|  | ||||
|     app.route(uri, methods=['GET'])(_handler) | ||||
| @@ -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 | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| @@ -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', | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										30
									
								
								tests/test_static.py
									
									
									
									
									
										Normal 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 | ||||
		Reference in New Issue
	
	Block a user
	 Channel Cat
					Channel Cat