├── .gitignore ├── CHANGELOG.rst ├── CONTRIBUTORS.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── REQS.DEVELOP.txt ├── REQS.txt ├── VERSION ├── docs ├── Makefile ├── _static │ └── soon.jpeg ├── _templates │ └── sidebar_intro.html ├── _themes │ ├── flask_theme_support.py │ └── kr │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ ├── flasky.css_t │ │ └── small_flask.css │ │ └── theme.conf ├── api.rst ├── changelog.rst ├── conf.py ├── contributors.rst ├── index.rst ├── install.rst ├── localstore.rst ├── quickstart.rst ├── s3store.rst └── sqlalchemy.rst ├── flask_store ├── __init__.py ├── exceptions.py ├── providers │ ├── __init__.py │ ├── local.py │ ├── s3.py │ └── temp.py ├── sqla.py └── utils.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | .env 45 | .tmuxp.yaml 46 | 47 | # Rope 48 | .ropeproject 49 | 50 | # Django stuff: 51 | *.log 52 | *.pot 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # OS 58 | *.DS_Store 59 | thumbs.db 60 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | 0.0.4.3 - Alpha 5 | --------------- 6 | * Bugfix: Python3 str error in setup 7 | 8 | 0.0.4.2 - Alpha 9 | --------------- 10 | * Minor Feature: New ``STORE_S3_ACL`` optional setting. S3 Uploads will auto be set to ``private`` 11 | unless ``STORE_S3_ACL`` specifies a different ACL. 12 | 13 | 0.0.4.1 - Alpha 14 | --------------- 15 | * Hotfix: Filename changed when saved is set on the provider instance 16 | 17 | 0.0.4 - Alpha 18 | ------------- 19 | * Changed: Minor change to API, Provider now requires file instance or path 20 | 21 | 0.0.3.1 - Alpha 22 | --------------- 23 | * Hotfix: Bug in FlaskStoreType where settings a ``None`` value would break the 24 | Provider, now checks the value is the expected instance type 25 | 26 | 0.0.3 - Alpha 27 | ------------- 28 | * Feature: SQLAlchemy Store Type 29 | * Changed: Renamed ``stores`` to ``providers`` 30 | * Removed: Removed ``FileStore`` wrapper class - it was a bad idea. 31 | 32 | 0.0.2 - Alpha 33 | ------------- 34 | * Feature: FileStore wrapper around provider files 35 | * Bugfix: S3 url generation 36 | 37 | 0.0.1 - Alpha 38 | ------------- 39 | * Feature: Local File Storage 40 | * Feature: S3 File Storage 41 | * Feature: S3 Gevented File Storage 42 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | Without the work of these people or organisations this project would not be 5 | possible, we salute you. 6 | 7 | * Soon London: http://thisissoon.com | @thisissoon 8 | * Chris Reeves: @krak3n 9 | * Greg Reed: @peeklondon 10 | * Radek Los: @radeklos 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 SOON_ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py README.rst MANIFEST.in CHANGELOG.rst REQS.txt REQS.DEVELOP.txt VERSION setup.cfg 2 | global-exclude *~ 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Makefile 3 | # 4 | 5 | .PHONY: clean-pyc clean-build docs 6 | 7 | help: 8 | @echo "clean - cleans up pyc files and build directoroes" 9 | @echo "clean-build - cleans build directoroes" 10 | @echo "clean-pyc - cleans pyc files" 11 | @echo "docs - generate Sphinx HTML documentation, including API docs" 12 | @echo "sdist - creates a distribution and lists contents" 13 | @echo "test - runs test suite" 14 | 15 | clean: clean-build clean-pyc 16 | 17 | clean-build: 18 | rm -fr build/ 19 | rm -fr dist/ 20 | rm -fr *.egg-info 21 | 22 | clean-pyc: 23 | find . -name '*.pyc' -exec rm -f {} + 24 | find . -name '*.pyo' -exec rm -f {} + 25 | find . -name '*~' -exec rm -f {} + 26 | 27 | docs: 28 | $(MAKE) -C docs clean 29 | $(MAKE) -C docs html 30 | open docs/_build/html/index.html 31 | 32 | develop: 33 | bash -c 'pip install -e .[develop]' 34 | 35 | test: 36 | python setup.py test 37 | 38 | release: clean 39 | python setup.py sdist upload -r pypi 40 | 41 | sdist: clean 42 | python setup.py sdist 43 | find dist -type f -exec ls {} \; | xargs tar -ztvf $$1 44 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ``Flask-Store`` 2 | =============== 3 | 4 | ``Flask-Store`` is a Flask Extension designed to provide easy file upload handling 5 | in the same vien as Django-Storages, allowing developers to user custom storage 6 | backends or one of the provided storage backends. 7 | 8 | .. warning:: 9 | 10 | This Flask Extenstion is under heavy development. It is likely API's will 11 | change over time but will be versioned so you can always stick to a version 12 | that works for you. 13 | 14 | Example Usage 15 | ------------- 16 | 17 | .. sourcecode:: python 18 | 19 | from flask import Flask, request 20 | from flask.ext.store import Store 21 | 22 | app = Flask(__name__) 23 | app.config['STORE_DOMAIN'] = 'http://127.0.0.1:5000' 24 | app.config['STORE_PATH'] = '/some/path/to/somewhere' 25 | store = Store(app) 26 | 27 | @app.route('/upload', methods=['POST', ]) 28 | def upload(): 29 | provider = store.Provider(request.files.get('afile')) 30 | provider.save() 31 | 32 | return provider.absolute_url 33 | 34 | if __name__ == "__main__": 35 | app.run() 36 | 37 | Included Providers 38 | ------------------ 39 | 40 | * Local File System 41 | * AWS Simple Storage Service (S3) 42 | -------------------------------------------------------------------------------- /REQS.DEVELOP.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Flask-Store Development Requirements 3 | # 4 | 5 | # Documentation Generation 6 | 7 | sphinx 8 | sphinxcontrib-napoleon 9 | 10 | # Required for documentation to build 11 | 12 | sqlalchemy 13 | 14 | # Debugging 15 | 16 | ipython 17 | pdbpp 18 | -------------------------------------------------------------------------------- /REQS.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Flask-Store Requirements 3 | # 4 | # This file is read when setup.py runs, reading each requirment 5 | # and placing it into the install_requires list. This allows for 6 | # python setup.py install to work correctly without the need for 7 | # installing requirements via pip or easy_install. 8 | # 9 | 10 | Flask>=0.10.1 11 | shortuuid>=0.1 12 | future>=0.15.2 13 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.4.3 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SOON_.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SOON_.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/SOON_" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SOON_" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/soon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thisissoon/Flask-Store/eee141b69aae08b7c2f5bfe19559e1ede9d58ed9/docs/_static/soon.jpeg -------------------------------------------------------------------------------- /docs/_templates/sidebar_intro.html: -------------------------------------------------------------------------------- 1 | 6 |

http://thisissoon.com

7 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import Keyword, Name, Comment, String, Error, \ 4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal 5 | 6 | 7 | class FlaskyStyle(Style): 8 | background_color = "#f8f8f8" 9 | default_style = "" 10 | 11 | styles = { 12 | # No corresponding class for the following: 13 | #Text: "", # class: '' 14 | Whitespace: "underline #f8f8f8", # class: 'w' 15 | Error: "#a40000 border:#ef2929", # class: 'err' 16 | Other: "#000000", # class 'x' 17 | 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | 21 | Keyword: "bold #004461", # class: 'k' 22 | Keyword.Constant: "bold #004461", # class: 'kc' 23 | Keyword.Declaration: "bold #004461", # class: 'kd' 24 | Keyword.Namespace: "bold #004461", # class: 'kn' 25 | Keyword.Pseudo: "bold #004461", # class: 'kp' 26 | Keyword.Reserved: "bold #004461", # class: 'kr' 27 | Keyword.Type: "bold #004461", # class: 'kt' 28 | 29 | Operator: "#582800", # class: 'o' 30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 31 | 32 | Punctuation: "bold #000000", # class: 'p' 33 | 34 | # because special names such as Name.Class, Name.Function, etc. 35 | # are not recognized as such later in the parsing, we choose them 36 | # to look the same as ordinary variables. 37 | Name: "#000000", # class: 'n' 38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 39 | Name.Builtin: "#004461", # class: 'nb' 40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 41 | Name.Class: "#000000", # class: 'nc' - to be revised 42 | Name.Constant: "#000000", # class: 'no' - to be revised 43 | Name.Decorator: "#888", # class: 'nd' - to be revised 44 | Name.Entity: "#ce5c00", # class: 'ni' 45 | Name.Exception: "bold #cc0000", # class: 'ne' 46 | Name.Function: "#000000", # class: 'nf' 47 | Name.Property: "#000000", # class: 'py' 48 | Name.Label: "#f57900", # class: 'nl' 49 | Name.Namespace: "#000000", # class: 'nn' - to be revised 50 | Name.Other: "#000000", # class: 'nx' 51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 52 | Name.Variable: "#000000", # class: 'nv' - to be revised 53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 56 | 57 | Number: "#990000", # class: 'm' 58 | 59 | Literal: "#000000", # class: 'l' 60 | Literal.Date: "#000000", # class: 'ld' 61 | 62 | String: "#4e9a06", # class: 's' 63 | String.Backtick: "#4e9a06", # class: 'sb' 64 | String.Char: "#4e9a06", # class: 'sc' 65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 66 | String.Double: "#4e9a06", # class: 's2' 67 | String.Escape: "#4e9a06", # class: 'se' 68 | String.Heredoc: "#4e9a06", # class: 'sh' 69 | String.Interpol: "#4e9a06", # class: 'si' 70 | String.Other: "#4e9a06", # class: 'sx' 71 | String.Regex: "#4e9a06", # class: 'sr' 72 | String.Single: "#4e9a06", # class: 's1' 73 | String.Symbol: "#4e9a06", # class: 'ss' 74 | 75 | Generic: "#000000", # class: 'g' 76 | Generic.Deleted: "#a40000", # class: 'gd' 77 | Generic.Emph: "italic #000000", # class: 'ge' 78 | Generic.Error: "#ef2929", # class: 'gr' 79 | Generic.Heading: "bold #000080", # class: 'gh' 80 | Generic.Inserted: "#00A000", # class: 'gi' 81 | Generic.Output: "#888", # class: 'go' 82 | Generic.Prompt: "#745334", # class: 'gp' 83 | Generic.Strong: "bold #000000", # class: 'gs' 84 | Generic.Subheading: "bold #800080", # class: 'gu' 85 | Generic.Traceback: "bold #a40000", # class: 'gt' 86 | } 87 | -------------------------------------------------------------------------------- /docs/_themes/kr/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 9 | 10 | {% endblock %} 11 | {%- block relbar2 %}{% endblock %} 12 | {%- block footer %} 13 | 16 | {%- endblock %} 17 | -------------------------------------------------------------------------------- /docs/_themes/kr/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /docs/_themes/kr/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. Modifications by Kenneth Reitz 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | {% set page_width = '1100px' %} 10 | {% set sidebar_width = '320px' %} 11 | {% set hover_color = '#000000' %} 12 | 13 | 14 | @import url(https://fonts.googleapis.com/css?family=Nixie+One); 15 | @import url(http://fonts.googleapis.com/css?family=Lato:300,400,700,300italic,400italic,700italic); 16 | 17 | @import url("basic.css"); 18 | 19 | /* -- page layout ----------------------------------------------------------- */ 20 | 21 | body { 22 | font-family: 'Lato', Helvetica, Arial, sans-serif; 23 | font-size: 17px; 24 | background-color: white; 25 | color: #000; 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | div.document { 31 | width: {{ page_width }}; 32 | margin: 30px auto 0 auto; 33 | } 34 | 35 | div.documentwrapper { 36 | float: left; 37 | width: 100%; 38 | } 39 | 40 | div.bodywrapper { 41 | margin: 0 0 0 {{ sidebar_width }}; 42 | } 43 | 44 | div.sphinxsidebar { 45 | width: {{ sidebar_width }}; 46 | } 47 | 48 | hr { 49 | border: 1px solid #B1B4B6; 50 | } 51 | 52 | div.body { 53 | background-color: #ffffff; 54 | color: #3E4349; 55 | padding: 0 30px 0 30px; 56 | } 57 | 58 | img.floatingflask { 59 | padding: 0 0 10px 10px; 60 | float: right; 61 | } 62 | 63 | div.footer { 64 | width: {{ page_width }}; 65 | margin: 20px auto 30px auto; 66 | font-size: 14px; 67 | color: #888; 68 | text-align: right; 69 | } 70 | 71 | div.footer a { 72 | color: #888; 73 | } 74 | 75 | div.related { 76 | display: none; 77 | } 78 | 79 | div.sphinxsidebar a { 80 | color: #444; 81 | text-decoration: none; 82 | border-bottom: 1px dotted #999; 83 | } 84 | 85 | div.sphinxsidebar a:hover { 86 | border-bottom: 1px solid #999; 87 | } 88 | 89 | div.sphinxsidebar { 90 | font-size: 14px; 91 | line-height: 1.5; 92 | } 93 | 94 | div.sphinxsidebarwrapper { 95 | padding: 18px 10px; 96 | } 97 | 98 | div.sphinxsidebarwrapper p.logo { 99 | padding: 0; 100 | margin: 0 0 10px 0; 101 | text-align: left; 102 | } 103 | 104 | div.sphinxsidebar h3, 105 | div.sphinxsidebar h4 { 106 | font-family: 'Nixie One', 'Garamond', 'Georgia', serif; 107 | color: #444; 108 | font-size: 24px; 109 | font-weight: normal; 110 | margin: 0 0 5px 0; 111 | padding: 0; 112 | } 113 | 114 | div.sphinxsidebar h4 { 115 | font-size: 20px; 116 | } 117 | 118 | div.sphinxsidebar h3 a { 119 | color: #444; 120 | } 121 | 122 | div.sphinxsidebar p.logo a, 123 | div.sphinxsidebar h3 a, 124 | div.sphinxsidebar p.logo a:hover, 125 | div.sphinxsidebar h3 a:hover { 126 | border: none; 127 | } 128 | 129 | div.sphinxsidebar p { 130 | color: #555; 131 | margin: 10px 0; 132 | } 133 | 134 | div.sphinxsidebar ul { 135 | margin: 10px 0; 136 | padding: 0; 137 | color: #000; 138 | } 139 | 140 | div.sphinxsidebar input { 141 | border: 1px solid #ccc; 142 | font-family: 'Georgia', serif; 143 | font-size: 1em; 144 | } 145 | 146 | /* -- body styles ----------------------------------------------------------- */ 147 | 148 | a { 149 | color: #004B6B; 150 | text-decoration: underline; 151 | } 152 | 153 | a:hover { 154 | color: {{ hover_color }}; 155 | text-decoration: underline; 156 | } 157 | 158 | div.body h1, 159 | div.body h2, 160 | div.body h3, 161 | div.body h4, 162 | div.body h5, 163 | div.body h6 { 164 | font-family: 'Nixie One', 'Garamond', 'Georgia', serif; 165 | font-weight: normal; 166 | margin: 30px 0px 10px 0px; 167 | padding: 0; 168 | } 169 | 170 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 171 | div.body h2 { font-size: 180%; } 172 | div.body h3 { font-size: 150%; } 173 | div.body h4 { font-size: 130%; } 174 | div.body h5 { font-size: 100%; } 175 | div.body h6 { font-size: 100%; } 176 | 177 | a.headerlink { 178 | color: #ddd; 179 | padding: 0 4px; 180 | text-decoration: none; 181 | } 182 | 183 | a.headerlink:hover { 184 | color: #444; 185 | background: #eaeaea; 186 | } 187 | 188 | div.body p, div.body dd, div.body li { 189 | line-height: 1.4em; 190 | } 191 | 192 | div.admonition { 193 | background: #fafafa; 194 | margin: 20px -30px; 195 | padding: 10px 30px; 196 | border-top: 1px solid #ccc; 197 | border-bottom: 1px solid #ccc; 198 | } 199 | 200 | div.admonition tt.xref, div.admonition a tt { 201 | border-bottom: 1px solid #fafafa; 202 | } 203 | 204 | dd div.admonition { 205 | margin-left: -60px; 206 | padding-left: 60px; 207 | } 208 | 209 | div.admonition p.admonition-title { 210 | font-family: 'Garamond', 'Georgia', serif; 211 | font-weight: normal; 212 | font-size: 24px; 213 | margin: 0 0 10px 0; 214 | padding: 0; 215 | line-height: 1; 216 | } 217 | 218 | div.admonition p.last { 219 | margin-bottom: 0; 220 | } 221 | 222 | div.highlight { 223 | background-color: white; 224 | } 225 | 226 | dt:target, .highlight { 227 | background: #FAF3E8; 228 | } 229 | 230 | div.note { 231 | background-color: #eee; 232 | border: 1px solid #ccc; 233 | } 234 | 235 | div.seealso { 236 | background-color: #ffc; 237 | border: 1px solid #ff6; 238 | } 239 | 240 | div.topic { 241 | background-color: #eee; 242 | } 243 | 244 | p.admonition-title { 245 | display: inline; 246 | } 247 | 248 | p.admonition-title:after { 249 | content: ":"; 250 | } 251 | 252 | pre, tt { 253 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 254 | font-size: 0.9em; 255 | } 256 | 257 | img.screenshot { 258 | } 259 | 260 | tt.descname, tt.descclassname { 261 | font-size: 0.95em; 262 | } 263 | 264 | tt.descname { 265 | padding-right: 0.08em; 266 | } 267 | 268 | img.screenshot { 269 | -moz-box-shadow: 2px 2px 4px #eee; 270 | -webkit-box-shadow: 2px 2px 4px #eee; 271 | box-shadow: 2px 2px 4px #eee; 272 | } 273 | 274 | table.docutils { 275 | border: 1px solid #888; 276 | -moz-box-shadow: 2px 2px 4px #eee; 277 | -webkit-box-shadow: 2px 2px 4px #eee; 278 | box-shadow: 2px 2px 4px #eee; 279 | } 280 | 281 | table.docutils td, table.docutils th { 282 | border: 1px solid #888; 283 | padding: 0.25em 0.7em; 284 | } 285 | 286 | table.field-list, table.footnote { 287 | border: none; 288 | -moz-box-shadow: none; 289 | -webkit-box-shadow: none; 290 | box-shadow: none; 291 | } 292 | 293 | table.footnote { 294 | margin: 15px 0; 295 | width: 100%; 296 | border: 1px solid #eee; 297 | background: #fdfdfd; 298 | font-size: 0.9em; 299 | } 300 | 301 | table.footnote + table.footnote { 302 | margin-top: -15px; 303 | border-top: none; 304 | } 305 | 306 | table.field-list th { 307 | padding: 0 0.8em 0 0; 308 | } 309 | 310 | table.field-list td { 311 | padding: 0; 312 | } 313 | 314 | table.footnote td.label { 315 | width: 0px; 316 | padding: 0.3em 0 0.3em 0.5em; 317 | } 318 | 319 | table.footnote td { 320 | padding: 0.3em 0.5em; 321 | } 322 | 323 | dl { 324 | margin: 0; 325 | padding: 0; 326 | } 327 | 328 | dl dd { 329 | margin-left: 30px; 330 | } 331 | 332 | blockquote { 333 | margin: 0 0 0 30px; 334 | padding: 0; 335 | } 336 | 337 | ul, ol { 338 | margin: 10px 0 10px 30px; 339 | padding: 0; 340 | } 341 | 342 | pre { 343 | background: #eee; 344 | padding: 7px 30px; 345 | margin: 15px -30px; 346 | line-height: 1.3em; 347 | } 348 | 349 | dl pre, blockquote pre, li pre { 350 | margin-left: -60px; 351 | padding-left: 60px; 352 | } 353 | 354 | dl dl pre { 355 | margin-left: -90px; 356 | padding-left: 90px; 357 | } 358 | 359 | tt { 360 | background-color: #ecf0f3; 361 | color: #222; 362 | padding: 2px; 363 | } 364 | 365 | tt.xref, a tt { 366 | background-color: #FBFBFB; 367 | border-bottom: 1px solid white; 368 | } 369 | 370 | a.reference { 371 | text-decoration: none; 372 | border-bottom: 1px dotted #004B6B; 373 | } 374 | 375 | a.reference:hover { 376 | border-bottom: 1px solid {{ hover_color }}; 377 | } 378 | 379 | a.footnote-reference { 380 | text-decoration: none; 381 | font-size: 0.7em; 382 | vertical-align: top; 383 | border-bottom: 1px dotted #004B6B; 384 | } 385 | 386 | a.footnote-reference:hover { 387 | border-bottom: 1px solid {{ hover_color }}; 388 | } 389 | 390 | a:hover tt { 391 | background: #EEE; 392 | } 393 | 394 | 395 | @media screen and (max-width: 600px) { 396 | 397 | div.sphinxsidebar { 398 | display: none; 399 | } 400 | 401 | div.document { 402 | width: 100%; 403 | 404 | } 405 | 406 | div.documentwrapper { 407 | margin-left: 0; 408 | margin-top: 0; 409 | margin-right: 0; 410 | margin-bottom: 0; 411 | } 412 | 413 | div.bodywrapper { 414 | margin-top: 0; 415 | margin-right: 0; 416 | margin-bottom: 0; 417 | margin-left: 0; 418 | } 419 | 420 | ul { 421 | margin-left: 0; 422 | } 423 | 424 | .document { 425 | width: auto; 426 | } 427 | 428 | .footer { 429 | width: auto; 430 | } 431 | 432 | .bodywrapper { 433 | margin: 0; 434 | } 435 | 436 | .footer { 437 | width: auto; 438 | } 439 | 440 | .github { 441 | display: none; 442 | } 443 | 444 | } 445 | 446 | /* misc. */ 447 | 448 | .revsys-inline { 449 | display: none!important; 450 | } 451 | 452 | /* version */ 453 | .versionadded, .versionchanged { 454 | margin-bottom: 20px; 455 | background: #D1EDFF; 456 | margin: 20px -30px; 457 | border-top: 1px solid #83CEFC; 458 | border-bottom: 1px solid #83CEFC; 459 | } 460 | 461 | .versionadded li p, .versionchanged li p { 462 | margin:0px; 463 | } 464 | 465 | .versionadded, .deprecated { 466 | padding: 10px 10px 0px 10px; 467 | margin-bottom: 20px; 468 | } 469 | 470 | .deprecated { 471 | background-color: #F7ADA0; 472 | border-top: 1px solid #E3563D; 473 | border-bottom: 1px solid #E3563D; 474 | margin: 20px -30px; 475 | } 476 | 477 | .versionadded p, .deprecated p { 478 | margin-bottom:0px; 479 | } 480 | 481 | .versionmodified { 482 | font-family: 'Nixie One', 'Garamond', 'Georgia', serif;; 483 | font-size: 22px; 484 | font-style: normal; 485 | font-weight: 500; 486 | margin-bottom:10px; 487 | display:block; 488 | } 489 | -------------------------------------------------------------------------------- /docs/_themes/kr/static/small_flask.css: -------------------------------------------------------------------------------- 1 | /* 2 | * small_flask.css_t 3 | * ~~~~~~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | body { 10 | margin: 0; 11 | padding: 20px 30px; 12 | } 13 | 14 | div.documentwrapper { 15 | float: none; 16 | background: white; 17 | } 18 | 19 | div.sphinxsidebar { 20 | display: block; 21 | float: none; 22 | width: 102.5%; 23 | margin: 50px -30px -20px -30px; 24 | padding: 10px 20px; 25 | background: #333; 26 | color: white; 27 | } 28 | 29 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 30 | div.sphinxsidebar h3 a { 31 | color: white; 32 | } 33 | 34 | div.sphinxsidebar a { 35 | color: #aaa; 36 | } 37 | 38 | div.sphinxsidebar p.logo { 39 | display: none; 40 | } 41 | 42 | div.document { 43 | width: 100%; 44 | margin: 0; 45 | } 46 | 47 | div.related { 48 | display: block; 49 | margin: 0; 50 | padding: 10px 0 20px 0; 51 | } 52 | 53 | div.related ul, 54 | div.related ul li { 55 | margin: 0; 56 | padding: 0; 57 | } 58 | 59 | div.footer { 60 | display: none; 61 | } 62 | 63 | div.bodywrapper { 64 | margin: 0; 65 | } 66 | 67 | div.body { 68 | min-height: 0; 69 | padding: 0; 70 | } 71 | 72 | .rtd_doc_footer { 73 | display: none; 74 | } 75 | 76 | .document { 77 | width: auto; 78 | } 79 | 80 | .footer { 81 | width: auto; 82 | } 83 | 84 | .footer { 85 | width: auto; 86 | } 87 | 88 | .github { 89 | display: none; 90 | } -------------------------------------------------------------------------------- /docs/_themes/kr/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | touch_icon = 8 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============== 3 | 4 | .. automodule:: flask_store 5 | :members: 6 | 7 | .. automodule:: flask_store.exceptions 8 | :members: 9 | 10 | .. automodule:: flask_store.sqla 11 | :members: 12 | 13 | .. automodule:: flask_store.utils 14 | :members: 15 | 16 | .. automodule:: flask_store.providers 17 | :members: 18 | 19 | .. automodule:: flask_store.providers.local 20 | :members: 21 | 22 | .. automodule:: flask_store.providers.s3 23 | :members: 24 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | import sys 5 | import os 6 | 7 | # Add flask_velox to the Path 8 | root = os.path.abspath( 9 | os.path.join( 10 | os.path.dirname(__file__), 11 | '..', 12 | ) 13 | ) 14 | 15 | sys.path.append(os.path.join(root, 'doit')) 16 | now = datetime.datetime.utcnow() 17 | year = now.year 18 | version = open(os.path.join(root, 'VERSION')).read().splitlines()[0] 19 | 20 | import flask_store # noqa 21 | 22 | 23 | # Project details 24 | project = u'Flask-Store' 25 | copyright = u'{0}, Soon London Ltd'.format(year) 26 | version = version 27 | release = version 28 | 29 | # Sphinx Config 30 | templates_path = ['_templates'] 31 | source_suffix = '.rst' 32 | master_doc = 'index' 33 | 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.viewcode', 37 | 'sphinx.ext.todo', 38 | 'sphinxcontrib.napoleon'] 39 | 40 | exclude_patterns = [] 41 | 42 | # Theme 43 | sys.path.append(os.path.abspath('_themes')) 44 | html_theme_path = ['_themes', ] 45 | html_static_path = ['_static', ] 46 | html_theme = 'kr' 47 | html_sidebars = { 48 | 'index': ['sidebar_intro.html', 'localtoc.html', 'relations.html', 49 | 'sourcelink.html', 'searchbox.html'], 50 | '**': ['sidebar_intro.html', 'localtoc.html', 'relations.html', 51 | 'sourcelink.html', 'searchbox.html'] 52 | } 53 | -------------------------------------------------------------------------------- /docs/contributors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTORS.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Usage Documentation 4 | ------------------- 5 | 6 | .. toctree:: 7 | :maxdepth: 3 8 | 9 | install 10 | quickstart 11 | sqlalchemy 12 | localstore 13 | s3store 14 | 15 | Reference 16 | --------- 17 | 18 | .. toctree:: 19 | :maxdepth: 3 20 | 21 | api 22 | changelog 23 | contributors 24 | 25 | Indices and tables 26 | ================== 27 | 28 | * :ref:`genindex` 29 | * :ref:`modindex` 30 | * :ref:`search` 31 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Simply grab it from PyPI:: 5 | 6 | pip install Flask-Store 7 | -------------------------------------------------------------------------------- /docs/localstore.rst: -------------------------------------------------------------------------------- 1 | Local Store 2 | =========== 3 | 4 | .. note:: 5 | 6 | This document assumes you have already read the 7 | :doc:`quickstart` guide. 8 | 9 | As we discussed in the :doc:`quickstart` guide Flask-Store uses the 10 | :class:`flask_store.providers.local.LocalProvidder` class as the default 11 | provider and here we will discuss some of the more advanced concepts of this 12 | store provider. 13 | 14 | Enable 15 | ------ 16 | 17 | This is the default provider but if you wish to be explicit (+1) then 18 | simply set the following in your application configuration:: 19 | 20 | STORE_PROVIDER='flask_store.providers.local.LocalProvider' 21 | 22 | Configuration 23 | ------------- 24 | 25 | The following configuration variables are available for you to customise. 26 | 27 | +--------------------------------+-----------------------------------+ 28 | | Name | Example Value | 29 | +================================+===================================+ 30 | | ``STORE_PATH`` | ``/somewhere/on/disk`` | 31 | +--------------------------------+-----------------------------------+ 32 | | This tells Flask-Store where to save uploaded files too. For this | 33 | | provider it must be an absolute path to a location on disk you | 34 | | have permission to write too. If the directory does not exist the | 35 | | provider will attempt to create the directory | 36 | +--------------------------------+-----------------------------------+ 37 | | ``STORE_URL_PREFIX`` | ``/uploads`` | 38 | +--------------------------------+-----------------------------------+ 39 | | Used to generate the URL for the uploaded file. The ``LocalStore`` | 40 | | will automatically register a route with your Flask application | 41 | | so the file can be accessed. Do not place domains in the path | 42 | | prefix. | 43 | +--------------------------------+-----------------------------------+ 44 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | Getting up and running with Flask-Store is pretty easy. By default Flask-Store 5 | will use local file system storage to store your files. All you need to do is 6 | to tell it where you want your uploaded files to live. 7 | 8 | Step 1: Integration 9 | ------------------- 10 | 11 | First lets initialise the Flask-Store extension with our Flask application 12 | object. 13 | 14 | .. sourcecode:: python 15 | 16 | from flask import Flask 17 | from flask.ext.store import Store 18 | 19 | app = Flask(__name__) 20 | store = Store(app) 21 | 22 | if __name__ == "__main__": 23 | app.run() 24 | 25 | That is all there is to it. If you use an application factory then you can use 26 | :meth:`flask_store.Store.init_app` method instead: 27 | 28 | .. sourcecode:: python 29 | 30 | from flask import Flask 31 | from flask.ext.store import Store 32 | 33 | store = Store() 34 | 35 | def create_app(): 36 | app = Flask(__name__) 37 | store.init_app(app) 38 | 39 | if __name__ == "__main__": 40 | app.run() 41 | 42 | Step 2: Configuration 43 | --------------------- 44 | 45 | So all we need to do now is tell Flask-Store where to save files once they have 46 | been uploaded. For asolute url generation we also need to tell Flask-Store about 47 | the domain where the files can accessed. 48 | 49 | To do this we just need to set a configuration variable called ``STORE_PATH`` 50 | and ``STORE_DOMAIN``. 51 | 52 | For brevity we will not show the application factory way because its pretty much 53 | identical. 54 | 55 | .. sourcecode:: python 56 | 57 | from flask import Flask 58 | from flask.ext.store import Store 59 | 60 | app = Flask(__name__) 61 | app.config['STORE_DOMAIN'] = 'http://127.0.0.1:5000' 62 | app.config['STORE_PATH'] = '/some/path/to/somewhere' 63 | store = Store(app) 64 | 65 | if __name__ == "__main__": 66 | app.run() 67 | 68 | Now when Flask-Store saves a file it will be located here: 69 | ``/some/path/to/somewhere``. 70 | 71 | Step 3: Add a route 72 | -------------------- 73 | 74 | Now we just need to save a file. We just need a route which gets a file from the 75 | request object and send it to our Flask-Store Provider (by default local 76 | Storage) to save it. 77 | 78 | .. note:: 79 | 80 | It is important to note the Flask-Store makes no attempt to validate your 81 | file size, extensions or what not, it just does one thing and that is save 82 | files somewhere. So if you need validation you should use something like 83 | ``WTForms`` to validate incoming data from the user. 84 | 85 | .. sourcecode:: python 86 | 87 | from flask import Flask, request 88 | from flask.ext.store import Store 89 | 90 | app = Flask(__name__) 91 | app.config['STORE_DOMAIN'] = 'http://127.0.0.1:5000' 92 | app.config['STORE_PATH'] = '/some/path/to/somewhere' 93 | store = Store(app) 94 | 95 | @app.route('/upload', methods=['POST', ]) 96 | def upload(): 97 | provider = store.Provider(request.files.get('afile')) 98 | provider.save() 99 | 100 | return provider.absolute_url 101 | 102 | if __name__ == "__main__": 103 | app.run() 104 | 105 | Now if we were to ``curl`` a file to our upload route we should get a url 106 | back which tells how we can access it. 107 | 108 | .. sourcecode:: bash 109 | 110 | curl -i -F afile=@localfile.jpg http://127.0.0.1:5000/upload 111 | 112 | We should get back something like: 113 | 114 | .. sourcecode:: http 115 | 116 | HTTP/1.0 200 OK 117 | Content-Type: text/html; charset=utf-8 118 | Content-Length: 44 119 | Server: Werkzeug/0.9.6 Python/2.7.5 120 | Date: Thu, 17 Jul 2014 11:32:02 GMT 121 | 122 | http://127.0.0.1:5000/uploads/localfile.jpg% 123 | 124 | Now if you went to ``http://127.0.0.1:5000/uploads/localfile.jpg`` in 125 | your browser you should see the image you uploaded. That is because 126 | Flask-Store automatically registers a route for serving files. 127 | 128 | .. note:: 129 | 130 | By the way, if you don't like the url you can change it by setting 131 | ``STORE_URL_PREFIX`` in your application configuration. 132 | 133 | Step 4: There is no Step 4 134 | -------------------------- 135 | 136 | Have a beer (or alcoholic beverage (or not) of your choice), that was 137 | exhausting. 138 | -------------------------------------------------------------------------------- /docs/s3store.rst: -------------------------------------------------------------------------------- 1 | S3 Store 2 | ======== 3 | 4 | .. note:: 5 | 6 | This document assumes you have already read the 7 | :doc:`quickstart` guide. 8 | 9 | The S3 Store allows you to forward your uploaded files up to an AWS 10 | Simple Storage Service (S3) bucket. This takes the problem of storing large 11 | numbers of files away from you onto Amazon. 12 | 13 | .. note:: 14 | 15 | Amazon's ``boto`` is required. Boto is not included as a install 16 | requirement for Flask-Store as not everyone will want to use the S3 17 | provider. To install just run:: 18 | 19 | pip install boto 20 | 21 | Enable 22 | ------ 23 | 24 | To use this provider simply set the following in your application 25 | configuration:: 26 | 27 | STORE_PROVIDER='flask_store.providers.s3.S3Provider' 28 | 29 | Configuration 30 | ------------- 31 | 32 | The following configuration variables are availible to you. 33 | 34 | +----------------------------+---------------------------------------+ 35 | | Name | Example Value | 36 | +============================+=======================================+ 37 | | ``STORE_PATH`` | ``/some/place/in/bucket`` | 38 | +----------------------------+---------------------------------------+ 39 | | For the ``S3Provider`` is basically your key name prefix rather | 40 | | than an actual location. So for the example value above the key | 41 | | for a file might be: ``/some/place/in/bucket/foo.jpg`` | 42 | +----------------------------+---------------------------------------+ 43 | | ``STORE_DOMAIN`` | ``https://bucket.s3.amazonaws.com`` | 44 | +----------------------------+---------------------------------------+ 45 | | Your S3 bucket domain, this is used to generate an absolute url. | 46 | +----------------------------+---------------------------------------+ 47 | | ``STORE_S3_REGION`` | ``us-east-1`` | 48 | +----------------------------+---------------------------------------+ 49 | | The region in which your bucket lives | 50 | +----------------------------+---------------------------------------+ 51 | | ``STORE_S3_BUCKET`` | ``your.bucket.name`` | 52 | +----------------------------+---------------------------------------+ 53 | | The name of the S3 bucket to upload files too | 54 | +----------------------------+---------------------------------------+ 55 | | ``STORE_S3_ACCESS_KEY`` | ``ABCDEFG12345`` | 56 | +----------------------------+---------------------------------------+ 57 | | Your AWS access key which has permission to upload files to the | 58 | | ``STORE_S3_BUCKET``. | 59 | +----------------------------+---------------------------------------+ 60 | | ``STORE_S3_SECRET_KEY`` | ``ABCDEFG12345`` | 61 | +----------------------------+---------------------------------------+ 62 | | Your AWS access secret key | 63 | +--------------------------------------------------------------------+ 64 | | ``STORE_S3_ACL`` | ``public-read`` | 65 | +----------------------------+---------------------------------------+ 66 | | ACL to set uploaded files, defaults to ``private``, see S3_ACL_ | 67 | +----------------------------+---------------------------------------+ 68 | 69 | .. _S3_ACL: http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl 70 | 71 | S3 Gevent Store 72 | =============== 73 | 74 | .. note:: 75 | 76 | This document assumes you have already read the 77 | :doc:`quickstart` guide. 78 | 79 | The :class:`flask_store.providers.s3.S3GeventProvider` allows you to run the 80 | upload to S3 process in a Gevent Greenlet process. This allows your webserver 81 | to send a response back to the client whilst the upload to S3 happends in 82 | the background. 83 | 84 | Obviously this means that when the request has finished the upload may not have 85 | finished and the key not exist in the bucket. You will need to build your 86 | application around this. 87 | 88 | .. note:: 89 | 90 | The ``gevent`` package is required. Gevent is not included as a install 91 | requirement for Flask-Store as not everyone will want to use the S3 Gevent 92 | provider. To install just run:: 93 | 94 | pip install gevent 95 | 96 | Enable 97 | ------ 98 | 99 | To use this provider simply set the following in your application 100 | configuration:: 101 | 102 | STORE_PROVIDER='flask_store.providers.s3.S3GeventProvider' 103 | 104 | Configuration 105 | ------------- 106 | 107 | .. note:: 108 | 109 | This is a sub class of :class:`flask_store.providers.s3.S3Provider` and 110 | therefore all the same confiuration options apply. 111 | -------------------------------------------------------------------------------- /docs/sqlalchemy.rst: -------------------------------------------------------------------------------- 1 | SQLAlchemy 2 | ========== 3 | 4 | If you use SQLAlchemy to store data in a database you can take advantage of the 5 | bundled :class:`flask_store.sqla.FlaskStoreType` which will take a lot of the 6 | cruft away for you. 7 | 8 | .. note:: 9 | 10 | In the following examples we are assuming you have your application 11 | setup using the application factory pattern. We will also not 12 | show the application factory method. 13 | 14 | Model 15 | ----- 16 | 17 | As normal you would use Flask-SQLAlchemy to define your model but you would use 18 | the :class:`flask_store.sqla.FlaskStoreType` type when defining the field type 19 | for the field you want to store the file path too. 20 | 21 | .. sourcecode:: python 22 | 23 | from flask_store.sqla import FlaskStoreType 24 | from yourapp.ext import db 25 | 26 | class MyModel(db.Model): 27 | field = db.Column(FlaskStoreType(128, location='/some/where')) 28 | 29 | This will act as a standard unicode string field. You do not need to pass a 30 | ``max_length`` integer as we have here as this will default to ``256``. 31 | 32 | The ``location`` keyword argument we have passed as an optional **relative** 33 | path to where your file should be saved too from the ``STORE_PATH`` defined 34 | in your Flask Application Configuration as described in the :doc:`quickstart` 35 | guide. 36 | 37 | Saving 38 | ------ 39 | 40 | When wanting to save the file you just need to set the attribute to be the 41 | instance of the request file uploaded, this will save the file to the location. 42 | 43 | .. sourcecode:: python 44 | 45 | from yourapp import create_app 46 | from yourapp.ext import db 47 | from yourapp.models import MyModel 48 | 49 | app = create_app() 50 | 51 | @route('/foo') 52 | def foo(): 53 | foo = MyModel() 54 | foo.field = request.files.get('foo') 55 | 56 | db.session.add(foo) 57 | db.session.commit() 58 | 59 | return foo.absolute_url 60 | 61 | Accessing 62 | --------- 63 | 64 | When accessing an object the relative path stored in the database will be 65 | automatically converted to a store provider instance. This will give you access 66 | to the object: 67 | 68 | .. sourcecode:: python 69 | 70 | from yourapp import create_app 71 | from yourapp.ext import db 72 | from yourapp.models import MyModel 73 | 74 | app = create_app() 75 | 76 | @route('/bar') 77 | def foo(): 78 | foo = MyModel.query.get(1) 79 | 80 | return foo.absolute_url 81 | -------------------------------------------------------------------------------- /flask_store/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | flask_store 5 | =========== 6 | 7 | Adds simple file handling for different providers to your application. Provides 8 | the following providers out of the box: 9 | 10 | * Local file storeage 11 | * Amazon Simple File Storage (requires ``boto`` to be installed) 12 | """ 13 | 14 | # Python 2/3 imports 15 | try: 16 | from urllib.parse import urlparse 17 | from urllib.parse import urljoin 18 | except ImportError: 19 | from urlparse.urlparse import urljoin 20 | from urlparse import urlparse 21 | 22 | 23 | from flask import current_app, send_from_directory 24 | from flask_store.exceptions import NotConfiguredError 25 | from importlib import import_module 26 | from werkzeug.local import LocalProxy 27 | 28 | 29 | DEFAULT_PROVIDER = "flask_store.providers.local.LocalProvider" 30 | Provider = LocalProxy(lambda: store_provider()) 31 | 32 | 33 | def store_provider(): 34 | """Returns the default provider class as defined in the application 35 | configuration. 36 | 37 | Returns 38 | ------- 39 | class 40 | The provider class 41 | """ 42 | 43 | store = current_app.extensions["store"] 44 | return store.store.Provider 45 | 46 | 47 | class StoreState(object): 48 | """Stores the state of Flask-Store from application init.""" 49 | 50 | def __init__(self, store, app): 51 | self.store = store 52 | self.app = app 53 | 54 | 55 | class Store(object): 56 | """Flask-Store integration into Flask applications. Flask-Store can 57 | be integrated in two different ways depending on how you have setup your 58 | Flask application. 59 | 60 | You can bind to a specific flask application:: 61 | 62 | app = Flask(__name__) 63 | store = Store(app) 64 | 65 | Or if you use an application factory you can use 66 | :meth:`flask_store.Store.init_app`:: 67 | 68 | store = Store() 69 | def create_app(): 70 | app = Flask(__name__) 71 | store.init_app(app) 72 | return app 73 | """ 74 | 75 | def __init__(self, app=None): 76 | """Constructor. Basically acts as a proxy to 77 | :meth:`flask_store.Store.init_app`. 78 | 79 | Key Arguments 80 | ------------- 81 | app : flask.app.Flask, optional 82 | Optional Flask application instance, default None 83 | """ 84 | 85 | if app: 86 | self.init_app(app) 87 | 88 | def init_app(self, app): 89 | """Sets up application default confugration options and sets a 90 | ``Provider`` property which can be used to access the default 91 | provider class which handles the saving of files. 92 | 93 | Arguments 94 | --------- 95 | app : flask.app.Flask 96 | Flask application instance 97 | """ 98 | 99 | app.config.setdefault("STORE_DOMAIN", None) 100 | app.config.setdefault("STORE_PROVIDER", DEFAULT_PROVIDER) 101 | 102 | if not hasattr(app, "extensions"): 103 | app.extensions = {} 104 | app.extensions["store"] = StoreState(self, app) 105 | 106 | # Set the provider class 107 | self.Provider = self.provider(app) 108 | 109 | # Set configuration defaults based on provider 110 | self.set_provider_defaults(app) 111 | 112 | # Ensure that any required configuration vars exist 113 | self.check_config(app) 114 | 115 | # Register a flask route - the provider must have register_route = True 116 | self.register_route(app) 117 | 118 | def check_config(self, app): 119 | """Checks the required application configuration variables are set 120 | in the flask application. 121 | 122 | Arguments 123 | --------- 124 | app : flask.app.Flask 125 | Flask application instance 126 | 127 | Raises 128 | ------ 129 | NotConfiguredError 130 | In the event a required config parameter is required by the 131 | Store. 132 | """ 133 | 134 | if hasattr(self.Provider, "REQUIRED_CONFIGURATION"): 135 | for name in self.Provider.REQUIRED_CONFIGURATION: 136 | if not app.config.get(name): 137 | raise NotConfiguredError( 138 | "{0} must be configured in your flask application " 139 | "configuration".format(name) 140 | ) 141 | 142 | def provider(self, app): 143 | """Fetches the provider class as defined by the application 144 | configuration. 145 | 146 | Arguments 147 | --------- 148 | app : flask.app.Flask 149 | Flask application instance 150 | 151 | Raises 152 | ------ 153 | ImportError 154 | If the class or module cannot be imported 155 | 156 | Returns 157 | ------- 158 | class 159 | The provider class 160 | """ 161 | 162 | if not hasattr(self, "_provider"): 163 | parts = app.config["STORE_PROVIDER"].split(".") 164 | klass = parts.pop() 165 | path = ".".join(parts) 166 | 167 | module = import_module(path) 168 | if not hasattr(module, klass): 169 | raise ImportError("{0} provider not found at {1}".format(klass, path)) 170 | 171 | self._provider = getattr(module, klass) 172 | 173 | return getattr(self, "_provider") 174 | 175 | def set_provider_defaults(self, app): 176 | """If the provider has a ``app_defaults`` static method then this 177 | simply calls that method. This will set sensible application 178 | configuration options for the provider. 179 | 180 | Arguments 181 | --------- 182 | app : flask.app.Flask 183 | Flask application instance 184 | """ 185 | 186 | if hasattr(self.Provider, "app_defaults"): 187 | self.Provider.app_defaults(app) 188 | 189 | def register_route(self, app): 190 | """Registers a default route for serving uploaded assets via 191 | Flask-Store, this is based on the absolute and relative paths 192 | defined in the app configuration. 193 | 194 | Arguments 195 | --------- 196 | app : flask.app.Flask 197 | Flask application instance 198 | """ 199 | 200 | def serve(filename): 201 | return send_from_directory(app.config["STORE_PATH"], filename) 202 | 203 | # Only do this if the Provider says so 204 | if self.Provider.register_route: 205 | url = urljoin( 206 | app.config["STORE_URL_PREFIX"].lstrip("/") + "/", "" 207 | ) 208 | app.add_url_rule("/" + url, "flask.store.file", serve) 209 | -------------------------------------------------------------------------------- /flask_store/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | flask_store.exceptions 5 | ====================== 6 | 7 | Custom Flask-Store exception classes. 8 | """ 9 | 10 | 11 | class NotConfiguredError(Exception): 12 | """ Raise this exception in the event the flask application has not been 13 | configured properly. 14 | """ 15 | 16 | pass 17 | -------------------------------------------------------------------------------- /flask_store/providers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | flask_store.providers 5 | ===================== 6 | 7 | Base store functionality and classes. 8 | """ 9 | 10 | from urllib.parse import urljoin 11 | 12 | import os 13 | import shortuuid 14 | 15 | from flask import current_app 16 | from flask_store.utils import is_path, path_to_uri 17 | from werkzeug.utils import secure_filename 18 | from werkzeug.datastructures import FileStorage 19 | 20 | 21 | class Provider(object): 22 | """Base provider class all storage providers should inherit from. This 23 | class provides some of the base functionality for all providers. Override 24 | as required. 25 | """ 26 | 27 | #: By default Providers do not require a route to be registered 28 | register_route = False 29 | 30 | def __init__(self, fp, location=None): 31 | """Constructor. When extending this class do not forget to call 32 | ``super``. 33 | 34 | This sets up base instance variables which can be used thoughtout the 35 | instance. 36 | 37 | Arguments 38 | --------- 39 | fp : werkzeug.datastructures.FileStorage, str 40 | A FileStorage instance or absolute path to a file 41 | 42 | Keyword Arguments 43 | ----------------- 44 | location : str, optional 45 | Relative location directory, this is appended to the 46 | ``STORE_PATH``, default None 47 | """ 48 | 49 | # The base store path for the provider 50 | self.store_path = self.join(current_app.config["STORE_PATH"]) 51 | 52 | # Save the fp - could be a FileStorage instance or a path 53 | self.fp = fp 54 | 55 | # Get the filename 56 | if is_path(fp): 57 | self.filename = os.path.basename(fp) 58 | else: 59 | if not isinstance(fp, FileStorage): 60 | raise ValueError( 61 | "File pointer must be an instance of a " 62 | "werkzeug.datastructures.FileStorage" 63 | ) 64 | self.filename = fp.filename 65 | 66 | # Save location 67 | self.location = location 68 | 69 | # Appends location to the store path 70 | if location: 71 | self.store_path = self.join(self.store_path, location) 72 | 73 | @property 74 | def relative_path(self): 75 | """Returns the relative path to the file, so minus the base 76 | path but still includes the location if it is set. 77 | 78 | Returns 79 | ------- 80 | str 81 | Relative path to file 82 | """ 83 | 84 | parts = [] 85 | if self.location: 86 | parts.append(self.location) 87 | parts.append(self.filename) 88 | 89 | return self.join(*parts) 90 | 91 | @property 92 | def absolute_path(self): 93 | """Returns the absollute file path to the file. 94 | 95 | Returns 96 | ------- 97 | str 98 | Absolute file path 99 | """ 100 | 101 | return self.join(self.store_path, self.filename) 102 | 103 | @property 104 | def relative_url(self): 105 | """Returns the relative URL, basically minus the domain. 106 | 107 | Returns 108 | ------- 109 | str 110 | Realtive URL to file 111 | """ 112 | 113 | parts = [ 114 | current_app.config["STORE_URL_PREFIX"], 115 | ] 116 | if self.location: 117 | parts.append(self.location) 118 | parts.append(self.filename) 119 | 120 | return path_to_uri(self.url_join(*parts)) 121 | 122 | @property 123 | def absolute_url(self): 124 | """Absolute url contains a domain if it is set in the configuration, 125 | the url predix, location and the actual file name. 126 | 127 | Returns 128 | ------- 129 | str 130 | Full absolute URL to file 131 | """ 132 | 133 | if not current_app.config["STORE_DOMAIN"]: 134 | path = self.relative_url 135 | 136 | path = urljoin(current_app.config["STORE_DOMAIN"], self.relative_url) 137 | 138 | return path_to_uri(path) 139 | 140 | def safe_filename(self, filename): 141 | """If the file already exists the file will be renamed to contain a 142 | short url safe UUID. This will avoid overwtites. 143 | 144 | Arguments 145 | --------- 146 | filename : str 147 | A filename to check if it exists 148 | 149 | Returns 150 | ------- 151 | str 152 | A safe filenaem to use when writting the file 153 | """ 154 | 155 | while self.exists(filename): 156 | dir_name, file_name = os.path.split(filename) 157 | file_root, file_ext = os.path.splitext(file_name) 158 | uuid = shortuuid.uuid() 159 | filename = secure_filename("{0}_{1}{2}".format(file_root, uuid, file_ext)) 160 | 161 | return filename 162 | 163 | def url_join(self, *parts): 164 | """Safe url part joining. 165 | 166 | Arguments 167 | --------- 168 | \*parts : list 169 | List of parts to join together 170 | 171 | Returns 172 | ------- 173 | str 174 | Joined url parts 175 | """ 176 | 177 | path = "" 178 | for i, part in enumerate(parts): 179 | if i > 0: 180 | part = part.lstrip("/") 181 | path = urljoin(path.rstrip("/") + "/", part.rstrip("/")) 182 | 183 | return path.lstrip("/") 184 | 185 | def join(self, *args, **kwargs): 186 | """Each provider needs to implement how to safely join parts of a 187 | path together to result in a path which can be used for the provider. 188 | 189 | Raises 190 | ------ 191 | NotImplementedError 192 | If the "join" method has not been implemented 193 | """ 194 | 195 | raise NotImplementedError( 196 | 'You must define a "join" method in the {0} provider.'.format( 197 | self.__class__.__name__ 198 | ) 199 | ) 200 | 201 | def exists(self, *args, **kwargs): 202 | """Placeholder "exists" method. This should be overridden by custom 203 | providers and return a ``boolean`` depending on if the file exists 204 | of not for the provider. 205 | 206 | Raises 207 | ------ 208 | NotImplementedError 209 | If the "exists" method has not been implemented 210 | """ 211 | 212 | raise NotImplementedError( 213 | 'You must define a "exists" method in the {0} provider.'.format( 214 | self.__class__.__name__ 215 | ) 216 | ) 217 | 218 | def save(self, *args, **kwargs): 219 | """Placeholder "sabe" method. This should be overridden by custom 220 | providers and save the file object to the provider. 221 | 222 | Raises 223 | ------ 224 | NotImplementedError 225 | If the "save" method has not been implemented 226 | """ 227 | 228 | raise NotImplementedError( 229 | 'You must define a "save" method in the {0} provider.'.format( 230 | self.__class__.__name__ 231 | ) 232 | ) 233 | -------------------------------------------------------------------------------- /flask_store/providers/local.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | flask_store.providers.local 5 | =========================== 6 | 7 | Local file storage for your Flask application. 8 | 9 | Example 10 | ------- 11 | 12 | .. sourcecode:: python 13 | 14 | from flask import Flask, request 15 | from flask.ext.store import Provider, Store 16 | from wtforms import Form 17 | from wtforms.fields import FileField 18 | 19 | class FooForm(Form): 20 | foo = FileField('foo') 21 | 22 | app = Flask(__app__) 23 | app.config['STORE_PATH'] = '/some/file/path' 24 | 25 | store = Store(app) 26 | 27 | @app,route('/upload') 28 | def upload(): 29 | form = FooForm() 30 | form.validate_on_submit() 31 | 32 | if not form.errors: 33 | provider = store.Provider(request.files.get('foo')) 34 | provider.save() 35 | 36 | """ 37 | 38 | import errno 39 | import os 40 | 41 | from flask_store.providers import Provider 42 | 43 | 44 | class LocalProvider(Provider): 45 | """ The default provider for Flask-Store. Handles saving files onto the 46 | local file system. 47 | """ 48 | 49 | #: Ensure a route is registered for serving files 50 | register_route = True 51 | 52 | @staticmethod 53 | def app_defaults(app): 54 | """ Sets sensible application configuration settings for this 55 | provider. 56 | 57 | Arguments 58 | --------- 59 | app : flask.app.Flask 60 | Flask application at init 61 | """ 62 | 63 | # For Local file storage the default store path is the current 64 | # working directory 65 | app.config.setdefault('STORE_PATH', os.getcwd()) 66 | 67 | # Default URL Prefix 68 | app.config.setdefault('STORE_URL_PREFIX', '/uploads') 69 | 70 | def join(self, *parts): 71 | """ Joins paths together in a safe manor. 72 | 73 | Arguments 74 | --------- 75 | \*parts : list 76 | List of arbitrary paths to join together 77 | 78 | Returns 79 | ------- 80 | str 81 | Joined paths 82 | """ 83 | 84 | path = '' 85 | for i, part in enumerate(parts): 86 | if i > 0: 87 | part = part.lstrip(os.path.sep) 88 | path = os.path.join(path, part) 89 | 90 | return path.rstrip(os.path.sep) 91 | 92 | def exists(self, filename): 93 | """ Returns boolean of the provided filename exists at the compiled 94 | absolute path. 95 | 96 | Arguments 97 | --------- 98 | name : str 99 | Filename to check its existence 100 | 101 | Returns 102 | ------- 103 | bool 104 | Whether the file exists on the file system 105 | """ 106 | 107 | path = self.join(self.store_path, filename) 108 | return os.path.exists(path) 109 | 110 | def save(self): 111 | """ Save the file on the local file system. Simply builds the paths 112 | and calls :meth:`werkzeug.datastructures.FileStorage.save` on the 113 | file object. 114 | """ 115 | 116 | fp = self.fp 117 | filename = self.safe_filename(self.filename) 118 | path = self.join(self.store_path, filename) 119 | directory = os.path.dirname(path) 120 | 121 | if not os.path.exists(directory): 122 | # Taken from Django - Race condition between os.path.exists and 123 | # os.mkdirs 124 | try: 125 | os.makedirs(directory) 126 | except OSError as e: 127 | if e.errno != errno.EEXIST: 128 | raise 129 | 130 | if not os.path.isdir(directory): 131 | raise IOError('{0} is not a directory'.format(directory)) 132 | 133 | # Save the file 134 | fp.save(path) 135 | fp.close() 136 | 137 | # Update the filename - it may have changes 138 | self.filename = filename 139 | 140 | def open(self): 141 | """ Opens the file and returns the file handler. 142 | 143 | Returns 144 | ------- 145 | file 146 | Open file handler 147 | """ 148 | 149 | path = self.join(self.store_path, self.filename) 150 | try: 151 | fp = open(path, 'rb') 152 | except IOError: 153 | raise IOError('File does not exist: {0}'.format(self.absolute_path)) 154 | 155 | return fp 156 | -------------------------------------------------------------------------------- /flask_store/providers/s3.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | flask_store.providers.s3 5 | ======================== 6 | 7 | AWS Simple Storage Service file Store. 8 | 9 | Example 10 | ------- 11 | .. sourcecode:: python 12 | 13 | from flask import Flask, request 14 | from flask.ext.Store import Provider, Store 15 | from wtforms import Form 16 | from wtforms.fields import FileField 17 | 18 | class FooForm(Form): 19 | foo = FileField('foo') 20 | 21 | app = Flask(__app__) 22 | app.config['STORE_PROVIDER'] = 'flask_store.providers.s3.S3Provider' 23 | app.config['STORE_S3_ACCESS_KEY'] = 'foo' 24 | app.confog['STORE_S3_SECRET_KEY'] = 'bar' 25 | 26 | store = Store(app) 27 | 28 | @app,route('/upload') 29 | def upload(): 30 | form = FooForm() 31 | form.validate_on_submit() 32 | 33 | provider = Provider(form.files.get('foo')) 34 | provider.save() 35 | """ 36 | 37 | try: 38 | import boto 39 | BOTO_INSTALLED = True 40 | except ImportError: 41 | BOTO_INSTALLED = False 42 | 43 | try: 44 | import gevent.monkey 45 | gevent.monkey.patch_all() 46 | GEVENT_INSTALLED = True 47 | except ImportError: 48 | GEVENT_INSTALLED = False 49 | 50 | # Standard Libs 51 | import io 52 | import mimetypes 53 | import os 54 | 55 | # Third Party Libs 56 | from flask import copy_current_request_context, current_app 57 | from flask_store.exceptions import NotConfiguredError 58 | from flask_store.providers import Provider 59 | from flask_store.providers.temp import TemporaryStore 60 | from werkzeug.datastructures import FileStorage 61 | 62 | 63 | class S3Provider(Provider): 64 | """ Amazon Simple Storage Service Store (S3). Allows files to be stored in 65 | an AWS S3 bucket. 66 | """ 67 | 68 | #: Required application configuration variables 69 | REQUIRED_CONFIGURATION = [ 70 | 'STORE_S3_ACCESS_KEY', 71 | 'STORE_S3_SECRET_KEY', 72 | 'STORE_S3_BUCKET', 73 | 'STORE_S3_REGION'] 74 | 75 | @staticmethod 76 | def app_defaults(app): 77 | """ Sets sensible application configuration settings for this 78 | provider. 79 | 80 | Arguments 81 | --------- 82 | app : flask.app.Flask 83 | Flask application at init 84 | """ 85 | 86 | # For S3 by default the STORE_PATH is the root of the bucket 87 | app.config.setdefault('STORE_PATH', '/') 88 | 89 | # For S3 the STORE_PATH makes up part of the key and therefore 90 | # doubles up as the STORE_URL_PREFIX 91 | app.config.setdefault('STORE_URL_PREFIX', app.config['STORE_PATH']) 92 | 93 | # Default ACL 94 | # http://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl 95 | app.config.setdefault('STORE_S3_ACL', 'private') 96 | 97 | if not BOTO_INSTALLED: 98 | raise ImportError( 99 | 'boto must be installed to use the S3Provider or the ' 100 | 'S3GeventProvider') 101 | 102 | def connect(self): 103 | """ Returns an S3 connection instance. 104 | """ 105 | 106 | if not hasattr(self, '_s3connection'): 107 | s3connection = boto.s3.connect_to_region( 108 | current_app.config['STORE_S3_REGION'], 109 | aws_access_key_id=current_app.config['STORE_S3_ACCESS_KEY'], 110 | aws_secret_access_key=current_app.config['STORE_S3_SECRET_KEY']) 111 | setattr(self, '_s3connection', s3connection) 112 | return getattr(self, '_s3connection') 113 | 114 | def bucket(self, s3connection): 115 | """ Returns an S3 bucket instance 116 | """ 117 | 118 | return s3connection.get_bucket( 119 | current_app.config.get('STORE_S3_BUCKET')) 120 | 121 | def join(self, *parts): 122 | """ Joins paths into a url. 123 | 124 | Arguments 125 | --------- 126 | \*parts : list 127 | List of arbitrary paths to join together 128 | 129 | Returns 130 | ------- 131 | str 132 | S3 save joined paths 133 | """ 134 | 135 | return self.url_join(*parts) 136 | 137 | def exists(self, filename): 138 | """ Checks if the file already exists in the bucket using Boto. 139 | 140 | Arguments 141 | --------- 142 | name : str 143 | Filename to check its existence 144 | 145 | Returns 146 | ------- 147 | bool 148 | Whether the file exists on the file system 149 | """ 150 | 151 | s3connection = self.connect() 152 | bucket = self.bucket(s3connection) 153 | path = self.join(self.store_path, filename) 154 | 155 | key = boto.s3.key.Key(name=path, bucket=bucket) 156 | 157 | return key.exists() 158 | 159 | def save(self): 160 | """ Takes the uploaded file and uploads it to S3. 161 | 162 | Note 163 | ---- 164 | This is a blocking call and therefore will increase the time for your 165 | application to respond to the client and may cause request timeouts. 166 | """ 167 | 168 | fp = self.fp 169 | s3connection = self.connect() 170 | bucket = self.bucket(s3connection) 171 | filename = self.safe_filename(self.filename) 172 | path = self.join(self.store_path, filename) 173 | mimetype, encoding = mimetypes.guess_type(filename) 174 | 175 | fp.seek(0) 176 | 177 | key = bucket.new_key(path) 178 | key.set_metadata('Content-Type', mimetype) 179 | key.set_contents_from_file(fp) 180 | key.set_acl(current_app.config.get('STORE_S3_ACL')) 181 | 182 | # Update the filename - it may have changes 183 | self.filename = filename 184 | 185 | def open(self): 186 | """ Opens an S3 key and returns an oepn File Like object pointer. 187 | 188 | Returns 189 | ------- 190 | _io.BytesIO 191 | In memory file data 192 | """ 193 | 194 | s3connection = self.connect() 195 | bucket = self.bucket(s3connection) 196 | key = bucket.get_key(self.relative_path) 197 | 198 | if not key: 199 | raise IOError('File does not exist: {0}'.format(self.relative_path)) 200 | 201 | return io.BytesIO(key.read()) # In memory 202 | 203 | 204 | class S3GeventProvider(S3Provider): 205 | """ A Gevent Support for :class:`.S3Provider`. Calling :meth:`.save` 206 | here will spawn a greenlet which will handle the actual upload process. 207 | """ 208 | 209 | def __init__(self, *args, **kwargs): 210 | """ 211 | """ 212 | 213 | if not GEVENT_INSTALLED: 214 | raise NotConfiguredError( 215 | 'You must have gevent installed to use the S3GeventProvider') 216 | 217 | super(S3GeventProvider, self).__init__(*args, **kwargs) 218 | 219 | def save(self): 220 | """ Acts as a proxy to the actual save method in the parent class. The 221 | save method will be called in a ``greenlet`` so ``gevent`` must be 222 | installed. 223 | 224 | Since the origional request will close the file object we write the 225 | file to a temporary location on disk and create a new 226 | :class:`werkzeug.datastructures.FileStorage` instance with the stram 227 | being the temporary file. 228 | """ 229 | 230 | fp = self.fp 231 | temp = TemporaryStore(fp) 232 | path = temp.save() 233 | filename = self.safe_filename(fp.filename) 234 | 235 | @copy_current_request_context 236 | def _save(): 237 | self.fp = FileStorage( 238 | stream=open(path, 'rb'), 239 | filename=filename, 240 | name=fp.name, 241 | content_type=fp.content_type, 242 | content_length=fp.content_length, 243 | headers=fp.headers) 244 | 245 | super(S3GeventProvider, self).save() 246 | 247 | # Cleanup - Delete the temp file 248 | os.unlink(path) 249 | 250 | gevent.spawn(_save) 251 | 252 | self.filename = filename 253 | -------------------------------------------------------------------------------- /flask_store/providers/temp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | flask_store.providers.temp 5 | ========================== 6 | """ 7 | 8 | import tempfile 9 | 10 | from flask_store.providers.local import LocalProvider 11 | 12 | 13 | class TemporaryStore(LocalProvider): 14 | """ 15 | """ 16 | 17 | def save(self): 18 | """ 19 | """ 20 | 21 | fp = self.fp 22 | with tempfile.NamedTemporaryFile(delete=False) as temp: 23 | temp.writelines(fp) 24 | temp.flush() 25 | 26 | return temp.name 27 | -------------------------------------------------------------------------------- /flask_store/sqla.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | flask_store.sqlalchemy 5 | ====================== 6 | 7 | Custom SQLAlchemy types for handling Flask-Store instances in SQLAlchemy. 8 | """ 9 | 10 | 11 | try: 12 | import sqlalchemy 13 | SQLALCHEMY_INSTALLED = True 14 | except ImportError: 15 | SQLALCHEMY_INSTALLED = False 16 | 17 | 18 | from flask_store import Provider 19 | from flask_store.exceptions import NotConfiguredError 20 | from werkzeug.datastructures import FileStorage 21 | 22 | 23 | class FlaskStoreType(sqlalchemy.types.TypeDecorator): 24 | """ A SQL Alchemy custom type which will save a file using the Flask 25 | Application Configured Store Provider and saves the relative path the the 26 | database. 27 | 28 | Also creates a fresh provider instance when accessing the data attribute 29 | from an instance. 30 | 31 | Example 32 | ------- 33 | .. sourcecode:: python 34 | 35 | from flask import Flask 36 | from flask_sqlalchemy import SQLAlchemy 37 | from flask_store import Store 38 | from flask_store.sqla import FlaskStoreType 39 | 40 | app = Flask(__name__) 41 | 42 | db = SQLAlchemy(app) 43 | store = Store(app) 44 | 45 | class MyModel(db.Model): 46 | field = db.Column(FlaskStoreType(location='/some/place')) 47 | """ 48 | 49 | #: Implements a standard unicode type 50 | impl = sqlalchemy.Unicode(256) 51 | 52 | def __init__(self, max_length=256, location=None, *args, **kwargs): 53 | """ Contructor, sets the location of the file relative from the 54 | base path. 55 | """ 56 | 57 | if not SQLALCHEMY_INSTALLED: 58 | raise NotConfiguredError( 59 | 'You need to install sqlalchemy to use the FlaskStoreType') 60 | 61 | self.location = location 62 | 63 | super(FlaskStoreType, self).__init__(*args, **kwargs) 64 | self.impl = sqlalchemy.types.Unicode(max_length) 65 | 66 | def process_bind_param(self, value, dialect): 67 | """ Called when setting the value be stored in the database field, this 68 | will be the files relative file path. 69 | 70 | Arguments 71 | --------- 72 | value : werkzeug.datastructures.FileStorage 73 | The uploaded file to save 74 | dialect : sqlalchemy.engine.interfaces.Dialect 75 | The dialect 76 | 77 | Returns 78 | ------- 79 | str 80 | The files realtive path on what ever storage backend defined in the 81 | Flask Application configuration 82 | """ 83 | 84 | if not isinstance(value, FileStorage) or not value.filename: 85 | return None 86 | 87 | provider = Provider(value, location=self.location) 88 | provider.save() 89 | 90 | return provider.relative_path 91 | 92 | def process_result_value(self, value, dialect): 93 | """ Called when accessing the value from the database and returning the 94 | appropriate provider file wrapper. 95 | 96 | Arguments 97 | --------- 98 | value : str 99 | The stored relative path in the database 100 | dialect : sqlalchemy.engine.interfaces.Dialect 101 | The dialect 102 | 103 | Returns 104 | ------- 105 | obj 106 | An instance of the Store Provider class 107 | """ 108 | 109 | if not value: 110 | return None 111 | 112 | provider = Provider(value, location=self.location) 113 | return provider 114 | -------------------------------------------------------------------------------- /flask_store/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | flask_store.utils 5 | ================= 6 | """ 7 | 8 | import os 9 | from past.builtins import basestring 10 | 11 | 12 | def path_to_uri(path): 13 | """ Swaps \\ for / Other stuff will happen here in the future. 14 | """ 15 | 16 | return path.replace('\\', '/') 17 | 18 | 19 | def is_path(f): 20 | """ Determines if the passed argument is a string or not, if is a string 21 | it is assumed to be a path. Taken from Pillow, all credit goes to the Pillow 22 | / PIL team. 23 | 24 | Arguments 25 | --------- 26 | f 27 | Could be anything 28 | 29 | Returns 30 | ------- 31 | bool 32 | Is a string or not 33 | """ 34 | 35 | if bytes is str: 36 | return isinstance(f, basestring) 37 | else: 38 | return isinstance(f, (bytes, str)) 39 | 40 | 41 | def is_directory(f): 42 | """ Checks if an object is a string, and that it points to a directory. 43 | Taken from Pillow, all credit goes to the Pillow / PIL team. 44 | 45 | Arguments 46 | --------- 47 | f 48 | Could be anything 49 | 50 | Returns 51 | ------- 52 | bool 53 | Is a path to a directory or not 54 | """ 55 | 56 | return is_path(f) and os.path.isdir(f) 57 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = 3 | --spec 4 | --cov flask_store 5 | --cov-config .coveragerc 6 | --cov-report term-missing 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | #!/usr/bin/env python 3 | 4 | """ 5 | Flask-Store 6 | ----------- 7 | 8 | Flask-Store is a Flask Extenstion which provides easy Django-Storages style 9 | file handling to various storage backends. 10 | """ 11 | 12 | # Temporary patch for issue reported here: 13 | # https://groups.google.com/forum/#!topic/nose-users/fnJ-kAUbYHQ 14 | import multiprocessing # noqa 15 | import os 16 | import sys 17 | import warnings 18 | 19 | from setuptools import setup, find_packages 20 | from setuptools.command.test import test as TestCommand 21 | 22 | 23 | class PyTest(TestCommand): 24 | 25 | def finalize_options(self): 26 | TestCommand.finalize_options(self) 27 | self.test_args = [] 28 | self.test_suite = True 29 | 30 | def run_tests(self): 31 | # import here, cause outside the eggs aren't loaded 32 | import pytest 33 | errno = pytest.main(self.test_args) 34 | sys.exit(errno) 35 | 36 | 37 | def read_requirements(filename): 38 | """ Read requirements file and process them into a list 39 | for usage in the setup function. 40 | 41 | Arguments 42 | --------- 43 | filename : str 44 | Path to the file to read line by line 45 | 46 | Returns 47 | -------- 48 | list 49 | list of requirements:: 50 | 51 | ['package==1.0', 'thing>=9.0'] 52 | """ 53 | 54 | requirements = [] 55 | 56 | try: 57 | if os.path.isfile(filename): 58 | with open(filename, 'rb') as f: 59 | for line in f.readlines(): 60 | line = line.strip() 61 | if not line or line.startswith(b'#') or line == '': 62 | continue 63 | requirements.append(line.decode('utf-8')) 64 | else: 65 | warnings.warn('{0} was not found'.format(filename)) 66 | except IOError: 67 | warnings.warn('{0} was not found'.format(filename)) 68 | 69 | return requirements 70 | 71 | # Get current working directory 72 | 73 | try: 74 | SETUP_DIRNAME = os.path.dirname(__file__) 75 | except NameError: 76 | SETUP_DIRNAME = os.path.dirname(sys.argv[0]) 77 | 78 | # Change to current working directory 79 | 80 | if SETUP_DIRNAME != '': 81 | os.chdir(SETUP_DIRNAME) 82 | 83 | # Requirements 84 | 85 | INSTALL_REQUIREMENTS = read_requirements('REQS.txt') 86 | TESTING_REQUIREMENTS = read_requirements('REQS.TESTING.txt') 87 | DEVELOP_REQUIREMENTS = read_requirements('REQS.DEVELOP.txt') \ 88 | + TESTING_REQUIREMENTS 89 | 90 | # Include the Change Log on PyPi 91 | 92 | long_description = open('README.rst').read() 93 | changelog = open('CHANGELOG.rst').read() 94 | long_description += '\n' + changelog 95 | 96 | # Setup 97 | 98 | setup( 99 | name='Flask-Store', 100 | version=open('VERSION').read().strip(), 101 | author='SOON_', 102 | author_email='dorks@thisissoon.com', 103 | maintainer='Chris Reeves', 104 | maintainer_email='hello@chris.reeves.io', 105 | url='http://flask-store.soon.build', 106 | description='Provides Django-Storages like file storage backends for ' 107 | 'Flask Applications.', 108 | long_description=long_description, 109 | packages=find_packages( 110 | exclude=[ 111 | 'tests' 112 | ]), 113 | include_package_data=True, 114 | zip_safe=False, 115 | # Dependencies 116 | install_requires=INSTALL_REQUIREMENTS, 117 | extras_require={ 118 | 'develop': DEVELOP_REQUIREMENTS 119 | }, 120 | # Testing 121 | tests_require=TESTING_REQUIREMENTS, 122 | cmdclass={ 123 | 'test': PyTest 124 | }, 125 | # Dependencies not hosted on PyPi 126 | dependency_links=[], 127 | # Classifiers for Package Indexing 128 | # Entry points, for example Flask-Script 129 | entry_points={}, 130 | # Meta 131 | classifiers=[ 132 | 'Framework :: Flask', 133 | 'Environment :: Web Environment', 134 | 'Intended Audience :: Developers', 135 | 'License :: OSI Approved :: MIT License', 136 | 'Development Status :: 3 - Alpha', 137 | 'Operating System :: OS Independent', 138 | 'Programming Language :: Python', 139 | 'Programming Language :: Python :: 2', 140 | 'Programming Language :: Python :: 2.7', 141 | 'Programming Language :: Python :: 3', 142 | 'Programming Language :: Python :: 3.5', 143 | 'Topic :: Software Development', 144 | 'Topic :: Software Development :: Libraries :: Python Modules', 145 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content'], 146 | license='MIT', 147 | keywords=['Flask', 'Files', 'Storage']) 148 | --------------------------------------------------------------------------------