From a9a15d3fc6f79162f31c3758f4114dc3131ac970 Mon Sep 17 00:00:00 2001 From: Mardix Date: Thu, 30 Jul 2015 00:46:30 -0400 Subject: [PATCH] Flask-Cloudy 0.10.0 --- CHANGELOG | 3 +- README.md | 22 ++++++---- example/__init__.py | 1 + example/app.py | 37 ++++++++++++++++ example/data/hello.txt | 3 ++ example/templates/index.html | 39 +++++++++++++++++ example/templates/view.html | 23 ++++++++++ flask_cloudy.py | 83 ++++++++++++++++++++++++++---------- setup.py | 2 +- tests/test_cloudy.py | 9 ++-- 10 files changed, 183 insertions(+), 39 deletions(-) create mode 100644 example/__init__.py create mode 100644 example/app.py create mode 100644 example/data/hello.txt create mode 100644 example/templates/index.html create mode 100644 example/templates/view.html diff --git a/CHANGELOG b/CHANGELOG index 2ced575..3ca27f8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ -0.7.0 +0.10.0 - Removed LOCAL_PATH configuration. Use CONTAINER as the LOCAL_PATH + - Change config prefix to STORAGE_* 0.6.0 - More pythonic diff --git a/README.md b/README.md index 88140ac..d2b8f58 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ For local file storage, it also provides a flask endpoint to access the files. @app.route("/upload", methods=["POST", "GET"]): def upload(): if request.method == "POST": - file = request.file.get("file") + file = request.files.get("file") my_upload = storage.upload(file) # some useful properties @@ -185,7 +185,7 @@ Storage: The **Storage** class allows you to access, upload, get an object from the Storage. -##### Storage(provider, key=None, secret=None, container=None) +##### Storage(provider, key=None, secret=None, container=None, allowed_extensions=None) - provider: the storage provider: @@ -209,6 +209,8 @@ The **Storage** class allows you to access, upload, get an object from the Stora - For cloud storage, use the **BUCKET NAME** - For LOCAL provider, it's the directory path where to access the files + +- allowed_extensions: List of extensions to upload to upload ##### Storage.init_app(app) @@ -239,7 +241,7 @@ It will also setup a server endpoint when STORAGE_PROVIDER == LOCAL @app.route("/upload", methods=["POST", "GET"]): def upload(): if request.method == "POST": - file = request.file.get("file") + file = request.files.get("file") my_upload = storage.upload(file) # some useful properties @@ -452,11 +454,15 @@ To save the object to a local path print(my_new_file) # Will print -> /my/new/path/my_object.txt -##### Object.download(name=None) +##### Object.download_url(timeout=60, name=None) Return a URL that triggers the browser download of the file. -Use the url to download the file +- timeout: int - The time in seconds to give access to the url + +- name: str - for LOCAL only, to rename the file being downloaded + +. storage = Storage(provider, key, secret, container) my_object = storage.get("my_object.txt") @@ -475,12 +481,12 @@ Use the url to download the file --- ---- +I hope you find this library useful, enjoy! -Thank you Mardix :) --- -License: MIT - Copyright 2015 Mardix \ No newline at end of file +License: MIT - Copyright 2015 Mardix + diff --git a/example/__init__.py b/example/__init__.py new file mode 100644 index 0000000..4e5980e --- /dev/null +++ b/example/__init__.py @@ -0,0 +1 @@ +__author__ = 'mardochee.macxis' diff --git a/example/app.py b/example/app.py new file mode 100644 index 0000000..28a7295 --- /dev/null +++ b/example/app.py @@ -0,0 +1,37 @@ + +from flask import Flask, request, render_template, redirect, abort, url_for +from flask_cloudy import Storage + +app = Flask(__name__) + +app.config.update({ + "STORAGE_PROVIDER": "LOCAL", + "STORAGE_CONTAINER": "./data", + "STORAGE_KEY": "", + "STORAGE_SECRET": "", + "STORAGE_SERVER": True +}) + +storage = Storage() +storage.init_app(app) + +@app.route("/") +def index(): + + return render_template("index.html", storage=storage) + +@app.route("/view/") +def view(object_name): + obj = storage.get(object_name) + print obj.name + return render_template("view.html", obj=obj) + +@app.route("/upload", methods=["POST"]) +def upload(): + file = request.files.get("file") + my_object = storage.upload(file) + return redirect(url_for("view", object_name=my_object.name)) + + +if __name__ == "__main__": + app.run(debug=True, port=5000) \ No newline at end of file diff --git a/example/data/hello.txt b/example/data/hello.txt new file mode 100644 index 0000000..84ddb84 --- /dev/null +++ b/example/data/hello.txt @@ -0,0 +1,3 @@ +Hello World! + +from flask_cloud import Storage \ No newline at end of file diff --git a/example/templates/index.html b/example/templates/index.html new file mode 100644 index 0000000..aaa63e4 --- /dev/null +++ b/example/templates/index.html @@ -0,0 +1,39 @@ + + + + + Flask-Cloudy + + + +

Flask-Cloudy

+ + +
+ Select image to upload: +
+ +
+ +
+ +

List of files available on the storage:

+ + + + + + + + {% for obj in storage %} + + + + + {% endfor %} + + +
NameSize
{{ obj.name }}{{ obj.size }} bytes
+ + + \ No newline at end of file diff --git a/example/templates/view.html b/example/templates/view.html new file mode 100644 index 0000000..d1c6d9d --- /dev/null +++ b/example/templates/view.html @@ -0,0 +1,23 @@ + + + + + Flask-Cloudy + + + +

Flask-Cloudy: View File

+<- Home +

+ +Name: {{ obj.name }}

+Size: {{ obj.size }} bytes

+ +Short url: {{ obj.short_url }}

+ +View file: {{ obj.url }}

+ +{% set download_url = obj.download_url() %} +Download: {{ download_url }}

+ + \ No newline at end of file diff --git a/flask_cloudy.py b/flask_cloudy.py index 0e739a5..d7c931b 100644 --- a/flask_cloudy.py +++ b/flask_cloudy.py @@ -3,6 +3,10 @@ Flask-Cloudy """ import os +import datetime +import base64 +import hmac +import hashlib import warnings from werkzeug.utils import secure_filename from werkzeug.datastructures import FileStorage @@ -13,9 +17,11 @@ from libcloud.storage.types import Provider, ObjectDoesNotExistError from libcloud.storage.providers import DRIVERS, get_driver from libcloud.storage.base import Object as BaseObject, StorageDriver from libcloud.storage.drivers import local -from six.moves.urllib.parse import urlparse, urlunparse, urljoin +from six.moves.urllib.parse import urlparse, urlunparse, urljoin, urlencode import slugify + + SERVER_ENDPOINT = "FLASK_CLOUDY_SERVER" EXTENSIONS = { @@ -126,7 +132,7 @@ class Storage(object): :param secret: str - provider secret :param container: str - the name of the container (bucket or a dir name if local) :param allowed_extensions: list - extensions allowed for upload - :param app: Flask object - + :param app: object - Flask instance :param kwargs: any other params will pass to the provider initialization :return: """ @@ -134,10 +140,10 @@ class Storage(object): if app: self.init_app(app) - if allowed_extensions: - self.allowed_extensions = allowed_extensions - if provider: + if allowed_extensions: + self.allowed_extensions = allowed_extensions + kwparams = { "key": key, "secret": secret @@ -343,11 +349,15 @@ class Storage(object): obj = self.get(object_name) if obj: dl = request.args.get("dl") - name = request.args.get("name") + name = request.args.get("name", obj.name) + + if get_file_extension(name) != obj.extension: + name += ".%s" % obj.extension + _url = obj.get_cdn_url() return send_file(_url, as_attachment=True if dl else False, - attachment_filename=name or False, + attachment_filename=name, conditional=True) else: abort(404) @@ -369,7 +379,7 @@ class Object(object): container @method - download() + download() use save_to() instead delete() """ @@ -492,21 +502,6 @@ class Object(object): """ return "%s/%s" % (self.container.name, self.name) - def download(self, name=None, signed=True): - """ - Trigger a browse download - :return: - """ - if "local" in self.driver.name.lower(): - return url_for(SERVER_ENDPOINT, - object_name=self.path, - dl=1, - name=name) - else: - if signed: - pass - pass - def save_to(self, destination, name=None, overwrite=False, delete_on_failure=True): """ To save the object in a local path @@ -527,3 +522,45 @@ class Object(object): overwrite_existing=overwrite, delete_on_failure=delete_on_failure) return obj_path if file else None + + def download_url(self, timeout=60, name=None): + """ + Trigger a browse download + :param timeout: int - Time in seconds to expire the download + :param name: str - for LOCAL only, to rename the file being downloaded + :return: str + """ + if "local" in self.driver.name.lower(): + return url_for(SERVER_ENDPOINT, + object_name=self.name, + dl=1, + name=name, + _external=True) + else: + driver_name = self.driver.name.lower() + expires = (datetime.datetime.now() + + datetime.timedelta(seconds=timeout)).strftime("%s") + + if 's3' in driver_name or 'google' in driver_name: + + s2s = "GET\n\n\n{expires}\n/{object_name}"\ + .format(expires=expires, object_name=self.path) + h = hmac.new(self.driver.secret, s2s, hashlib.sha1) + s = base64.encodestring(h.digest()).strip() + _keyIdName = "AWSAccessKeyId" if "s3" in driver_name else "GoogleAccessId" + params = { + _keyIdName: self.driver.key, + "Expires": expires, + "Signature": s + } + urlkv = urlencode(params) + return "%s?%s" % (self.secure_url, urlkv) + + elif 'cloudfiles' in driver_name: + return self.driver.ex_get_object_temp_url(self._obj, + method="GET", + timeout=expires) + else: + raise NotImplemented("This provider '%s' doesn't support or " + "doesn't have a signed url " + "implemented yet" % self.provider_name) diff --git a/setup.py b/setup.py index e448e61..f53cfc8 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ Supported storage: from setuptools import setup, find_packages __NAME__ = "Flask-Cloudy" -__version__ = "0.7.0" +__version__ = "0.10.0" __author__ = "Mardix" __license__ = "MIT" __copyright__ = "2015" diff --git a/tests/test_cloudy.py b/tests/test_cloudy.py index bf60da3..4eecc10 100644 --- a/tests/test_cloudy.py +++ b/tests/test_cloudy.py @@ -24,12 +24,6 @@ class App(object): STORAGE_ALLOWED_EXTENSIONS=[]) -def _setup_function(): - pass - -def _teardown_function(): - pass - def test_get_file_extension(): filename = "hello.jpg" assert get_file_extension(filename) == "jpg" @@ -170,3 +164,6 @@ def test_save_to(): file2 = o.save_to(CWD + "/data", name="my_new_file", overwrite=True) assert os.path.isfile(file) assert file2 == CWD + "/data/my_new_file.txt" + + +from flask_cloudy import Storage \ No newline at end of file