├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── conf.py └── index.rst ├── flask_storage ├── __init__.py ├── _base.py ├── _compat.py ├── _utils.py ├── local.py ├── qiniu.py ├── s3.py ├── sae.py └── upyun.py ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── _base.py ├── flask.png ├── test_local.py └── test_qiniu.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg-info 4 | *.swp 5 | __pycache__ 6 | build 7 | develop-eggs 8 | dist 9 | eggs 10 | parts 11 | .DS_Store 12 | .installed.cfg 13 | docs/_build 14 | cover/ 15 | tmp/ 16 | .tox 17 | .cache 18 | .coverage 19 | venv/ 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git://github.com/lepture/flask-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | - "3.3" 7 | - "3.4" 8 | - "pypy" 9 | 10 | script: 11 | - nosetests -s 12 | 13 | after_success: 14 | - pip install coveralls 15 | - coverage run --source=flask_oauthlib setup.py -q nosetests 16 | - coveralls 17 | 18 | branches: 19 | only: 20 | - master 21 | 22 | notifications: 23 | email: false 24 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 5 | Version 0.1.1 6 | ------------- 7 | 8 | 9 | Version 0.1 10 | ----------- 11 | 12 | First preview release 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============= 3 | 4 | First, please do contribute! There are more than one way to contribute, and I will 5 | appreciate any way you choose. 6 | 7 | * introduce Flask-Storage to your friends, let Flask-Storage to be known 8 | * discuss Flask-Storage , and submit bugs with github issues 9 | * improve documentation for Flask-Storage 10 | * send patch with github pull request 11 | 12 | English and Chinese issues are acceptable, talk in your favorite language. 13 | 14 | Pull request and git commit message **must be in English**, if your commit message 15 | is in other language, it will be rejected. 16 | 17 | 18 | Issues 19 | ------ 20 | 21 | When you submit an issue, please format your content, a readable content helps a lot. 22 | You should have a little knowledge on Markdown_. 23 | 24 | .. _Markdown: http://github.github.com/github-flavored-markdown/ 25 | 26 | Code talks. If you can't make yourself understood, show me the code. Please make your 27 | case as simple as possible. 28 | 29 | 30 | Codebase 31 | -------- 32 | 33 | The codebase of Flask-Storage is highly tested and :pep:`8` compatible, as a way 34 | to guarantee functionality and keep all code written in a good style. 35 | 36 | You should follow the code style. Here are some tips to make things simple: 37 | 38 | * When you cloned this repo, run ``make``, it will prepare everything for you 39 | * Check the code style with ``make link`` 40 | 41 | 42 | Git Help 43 | -------- 44 | 45 | Something you should know about git. 46 | 47 | * don't add any code on the master branch, create a new one 48 | * don't add too many code in one pull request 49 | * all featured branches should be based on the master branch 50 | 51 | Take an example, if you want to add feature A and feature B, you should have two 52 | branches:: 53 | 54 | $ git branch feature-A 55 | $ git checkout feature-A 56 | 57 | Now code on feature-A branch, and when you finish feature A:: 58 | 59 | $ git checkout master 60 | $ git branch feature-B 61 | $ git checkout feature-B 62 | 63 | All branches must be based on the master branch. If your feature-B needs feature-A, 64 | you should send feature-A first, and wait for its merging. We may reject feature-A, 65 | and you should stop feature-B. 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 - 2015, Hsiaoming Yang 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * Neither the name of Flask-Storage nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 21 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 22 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 23 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test coverage clean clean-pyc clean-build docs 2 | 3 | lint: 4 | @flake8 flask_storage tests 5 | 6 | test: 7 | @rm -fr tmp 8 | @nosetests -s 9 | 10 | coverage: 11 | @rm -fr tmp 12 | @rm -f .coverage 13 | @nosetests --with-coverage --cover-package=flask_storage --cover-html 14 | 15 | clean: clean-build clean-pyc clean-docs 16 | 17 | 18 | clean-build: 19 | @rm -fr build/ 20 | @rm -fr dist/ 21 | @rm -fr *.egg-info 22 | 23 | 24 | clean-pyc: 25 | @find . -name '*.pyc' -exec rm -f {} + 26 | @find . -name '*.pyo' -exec rm -f {} + 27 | @find . -name '*~' -exec rm -f {} + 28 | @find . -name '__pycache__' -exec rm -fr {} + 29 | 30 | clean-docs: 31 | @rm -fr docs/_build 32 | 33 | docs: 34 | @$(MAKE) -C docs html 35 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-Storage 2 | ============= 3 | 4 | Flask-Storage is a collection of backends for file storage. 5 | 6 | The built-in backends: 7 | 8 | - Local backend 9 | - Upyun backend 10 | - Qiniu backend 11 | - S3 backend 12 | 13 | 14 | Installation 15 | ------------ 16 | 17 | Install flask-storage is simple with pip_:: 18 | 19 | $ pip install Flask-Storage 20 | 21 | If you don't have pip installed, try with easy_install:: 22 | 23 | $ easy_install Flask-Storage 24 | 25 | .. _pip: http://www.pip-installer.org/ 26 | 27 | 28 | Configuration 29 | ------------- 30 | -------------------------------------------------------------------------------- /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 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Storage.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Storage.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Storage" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Storage" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-Storage documentation build configuration file, created by 4 | # sphinx-quickstart on Wed May 29 18:55:59 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('.')) 20 | sys.path.insert(0, os.path.abspath('..')) 21 | sys.path.insert(0, os.path.abspath('_themes')) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.autodoc'] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'Flask-Storage' 46 | copyright = u'2013, Hsiaoming Yang' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The short X.Y version. 53 | version = '0.1.0' 54 | # The full version, including alpha/beta/rc tags. 55 | release = '0.1.0' 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all documents. 72 | #default_role = None 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | #add_module_names = True 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'flask_small' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | html_theme_options = { 102 | 'index_logo': None, 103 | 'github_fork': 'lepture/flask-storage' 104 | } 105 | 106 | # Add any paths that contain custom themes here, relative to this directory. 107 | html_theme_path = ['_themes'] 108 | 109 | # The name for this set of Sphinx documents. If None, it defaults to 110 | # " v documentation". 111 | #html_title = None 112 | 113 | # A shorter title for the navigation bar. Default is the same as html_title. 114 | #html_short_title = None 115 | 116 | # The name of an image file (relative to this directory) to place at the top 117 | # of the sidebar. 118 | #html_logo = None 119 | 120 | # The name of an image file (within the static path) to use as favicon of the 121 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 122 | # pixels large. 123 | #html_favicon = None 124 | 125 | # Add any paths that contain custom static files (such as style sheets) here, 126 | # relative to this directory. They are copied after the builtin static files, 127 | # so a file named "default.css" will overwrite the builtin "default.css". 128 | html_static_path = ['_static'] 129 | 130 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 131 | # using the given strftime format. 132 | #html_last_updated_fmt = '%b %d, %Y' 133 | 134 | # If true, SmartyPants will be used to convert quotes and dashes to 135 | # typographically correct entities. 136 | #html_use_smartypants = True 137 | 138 | # Custom sidebar templates, maps document names to template names. 139 | #html_sidebars = {} 140 | 141 | # Additional templates that should be rendered to pages, maps page names to 142 | # template names. 143 | #html_additional_pages = {} 144 | 145 | # If false, no module index is generated. 146 | #html_domain_indices = True 147 | 148 | # If false, no index is generated. 149 | #html_use_index = True 150 | 151 | # If true, the index is split into individual pages for each letter. 152 | #html_split_index = False 153 | 154 | # If true, links to the reST sources are added to the pages. 155 | #html_show_sourcelink = True 156 | 157 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 158 | #html_show_sphinx = True 159 | 160 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 161 | #html_show_copyright = True 162 | 163 | # If true, an OpenSearch description file will be output, and all pages will 164 | # contain a tag referring to it. The value of this option must be the 165 | # base URL from which the finished HTML is served. 166 | #html_use_opensearch = '' 167 | 168 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 169 | #html_file_suffix = None 170 | 171 | # Output file base name for HTML help builder. 172 | htmlhelp_basename = 'Flask-Storagedoc' 173 | 174 | 175 | # -- Options for LaTeX output -------------------------------------------------- 176 | 177 | latex_elements = { 178 | # The paper size ('letterpaper' or 'a4paper'). 179 | #'papersize': 'letterpaper', 180 | 181 | # The font size ('10pt', '11pt' or '12pt'). 182 | #'pointsize': '10pt', 183 | 184 | # Additional stuff for the LaTeX preamble. 185 | #'preamble': '', 186 | } 187 | 188 | # Grouping the document tree into LaTeX files. List of tuples 189 | # (source start file, target name, title, author, documentclass [howto/manual]). 190 | latex_documents = [ 191 | ('index', 'Flask-Storage.tex', u'Flask-Storage Documentation', 192 | u'Hsiaoming Yang', 'manual'), 193 | ] 194 | 195 | # The name of an image file (relative to this directory) to place at the top of 196 | # the title page. 197 | #latex_logo = None 198 | 199 | # For "manual" documents, if this is true, then toplevel headings are parts, 200 | # not chapters. 201 | #latex_use_parts = False 202 | 203 | # If true, show page references after internal links. 204 | #latex_show_pagerefs = False 205 | 206 | # If true, show URL addresses after external links. 207 | #latex_show_urls = False 208 | 209 | # Documents to append as an appendix to all manuals. 210 | #latex_appendices = [] 211 | 212 | # If false, no module index is generated. 213 | #latex_domain_indices = True 214 | 215 | 216 | # -- Options for manual page output -------------------------------------------- 217 | 218 | # One entry per manual page. List of tuples 219 | # (source start file, name, description, authors, manual section). 220 | man_pages = [ 221 | ('index', 'flask-storage', u'Flask-Storage Documentation', 222 | [u'Hsiaoming Yang'], 1) 223 | ] 224 | 225 | # If true, show URL addresses after external links. 226 | #man_show_urls = False 227 | 228 | 229 | # -- Options for Texinfo output ------------------------------------------------ 230 | 231 | # Grouping the document tree into Texinfo files. List of tuples 232 | # (source start file, target name, title, author, 233 | # dir menu entry, description, category) 234 | texinfo_documents = [ 235 | ('index', 'Flask-Storage', u'Flask-Storage Documentation', 236 | u'Hsiaoming Yang', 'Flask-Storage', 'One line description of project.', 237 | 'Miscellaneous'), 238 | ] 239 | 240 | # Documents to append as an appendix to all manuals. 241 | #texinfo_appendices = [] 242 | 243 | # If false, no module index is generated. 244 | #texinfo_domain_indices = True 245 | 246 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 247 | #texinfo_show_urls = 'footnote' 248 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Flask-Storage documentation master file, created by 2 | sphinx-quickstart on Wed May 29 18:55:59 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../README.rst 7 | 8 | API Reference 9 | ------------- 10 | 11 | This part of the documentation covers ench and every public class or 12 | function from Flask-Storage. 13 | 14 | .. module:: flask_storage 15 | 16 | .. autoclass:: LocalStorage 17 | :members: 18 | 19 | .. autoclass:: UpyunStorage 20 | :members: 21 | -------------------------------------------------------------------------------- /flask_storage/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_storage 4 | ~~~~~~~~~~~~~ 5 | 6 | Collection of storage backends. 7 | 8 | :copyright: (c) 2013 Hsiaoming Yang. 9 | """ 10 | 11 | from werkzeug.utils import import_string 12 | 13 | __version__ = '0.1.2' 14 | __author__ = 'Hsiaoming Yang ' 15 | 16 | 17 | backends = { 18 | 'local': 'flask_storage.local.LocalStorage', 19 | 's3': 'flask_storage.s3.S3Storage', 20 | 'upyun': 'flask_storage.upyun.UpyunStorage', 21 | 'qiniu': 'flask_storage.qiniu.QiniuStorage', 22 | } 23 | 24 | 25 | class Storage(object): 26 | """Create a storage instance. 27 | 28 | :param app: Flask app instance 29 | """ 30 | 31 | def __init__(self, app=None): 32 | self._backend = None 33 | self.app = app 34 | if app: 35 | self.init_app(app) 36 | 37 | def init_app(self, app): 38 | t = app.config.setdefault('STORAGE_TYPE', 'local') 39 | assert t in backends, "Storage type not supported." 40 | 41 | Backend = import_string(backends[t]) 42 | name = app.config.get('STORAGE_NAME', t) 43 | extensions = app.config.get('STORAGE_EXTENSIONS', None) 44 | config = app.config.get('STORAGE_CONFIG', {}) 45 | 46 | self._backend = Backend(name, extensions, config) 47 | 48 | def __getattr__(self, key): 49 | try: 50 | return object.__getattribute__(self, key) 51 | except AttributeError: 52 | if self._backend is None: 53 | raise RuntimeError("Backend not configured.") 54 | return getattr(self._backend, key) 55 | -------------------------------------------------------------------------------- /flask_storage/_base.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_storage._base 4 | ~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Common utilities for flask storage. 7 | 8 | :copyright: (c) 2013 Hsiaoming Yang. 9 | """ 10 | 11 | import os 12 | import logging 13 | try: 14 | from werkzeug.datastructures import FileStorage 15 | except ImportError: 16 | from werkzeug import FileStorage 17 | from ._compat import urljoin 18 | 19 | 20 | __all__ = ( 21 | 'TEXT', 'DOCUMENTS', 'IMAGES', 'AUDIO', 'DATA', 'SCRIPTS', 22 | 'ARCHIVES', 'EXECUTABLES', 'BaseStorage', 23 | 'UploadNotAllowed', 'UploadFileExists', 'make_request' 24 | ) 25 | 26 | log = logging.getLogger('flask_storage') 27 | 28 | TEXT = ('txt',) 29 | 30 | DOCUMENTS = ( 31 | 'rtf', 'odf', 'ods', 'gnumeric', 'abw', 32 | 'doc', 'docx', 'xls', 'xlsx' 33 | ) 34 | 35 | # This contains basic image types that are viewable by most browsers 36 | IMAGES = ('jpg', 'jpe', 'jpeg', 'png', 'gif', 'svg', 'bmp') 37 | 38 | # This contains audio file types 39 | AUDIO = ('wav', 'mp3', 'aac', 'ogg', 'oga', 'flac') 40 | 41 | # This is for structured data files 42 | DATA = ('csv', 'ini', 'json', 'plist', 'xml', 'yaml', 'yml') 43 | 44 | # This contains various types of scripts 45 | SCRIPTS = ('py', 'js', 'rb', 'sh', 'pl', 'php') 46 | 47 | # This contains archive and compression formats 48 | ARCHIVES = ('gz', 'bz2', 'zip', 'tar', 'tgz', 'txz', '7z') 49 | 50 | # This contains shared libraries and executable files 51 | EXECUTABLES = ('so', 'ext', 'dll') 52 | 53 | 54 | class BaseStorage(object): 55 | def __init__(self, name='base', extensions=None, config=None): 56 | self.name = name 57 | self.config = config 58 | self.extensions = extensions or IMAGES 59 | 60 | def url(self, filename): 61 | """Generate the url for a filename. 62 | 63 | :param filename: filename for generating the url.... 64 | """ 65 | urlbase = self.config.get('base_url') 66 | return urljoin(urlbase, filename) 67 | 68 | def extension_allowed(self, extname): 69 | if not self.extensions: 70 | return True 71 | return extname in self.extensions 72 | 73 | def check(self, storage): 74 | """ 75 | Check if the storage can be saved. 76 | 77 | :param storage: The storage to be saved. 78 | 79 | This function should be called everytime when you want to 80 | save a storage:: 81 | 82 | class DemoStorage(BaseStorage): 83 | def save(self, storage, filename): 84 | # check storage before saving it 85 | self.check(storage) 86 | """ 87 | if not isinstance(storage, FileStorage): 88 | raise TypeError('storage must be a werkzeug.FileStorage') 89 | 90 | _, extname = os.path.splitext(storage.filename) 91 | ext = extname.lower()[1:] 92 | if not self.extension_allowed(ext): 93 | raise UploadNotAllowed('Extension not allowed') 94 | 95 | def exists(self, filename): 96 | raise NotImplementedError 97 | 98 | def read(self, filename): 99 | raise NotImplementedError 100 | 101 | def write(self, filename, body, headers=None): 102 | raise NotImplementedError 103 | 104 | def delete(self, filename): 105 | raise NotImplementedError 106 | 107 | def save(self, storage, filename): 108 | raise NotImplementedError 109 | 110 | 111 | class UploadNotAllowed(Exception): 112 | """This exception is raised if the upload was not allowed.""" 113 | 114 | 115 | class UploadFileExists(Exception): 116 | """This exception is raised when the uploaded file exits.""" 117 | -------------------------------------------------------------------------------- /flask_storage/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | try: 3 | from urlparse import urljoin 4 | import urllib2 as http 5 | except ImportError: 6 | from urllib.parse import urljoin 7 | from urllib import request as http 8 | 9 | if sys.version_info[0] == 3: 10 | string_type = str 11 | else: 12 | string_type = unicode 13 | 14 | 15 | __all__ = ['urljoin', 'http', 'string_type', 'to_bytes'] 16 | 17 | 18 | def to_bytes(text): 19 | if isinstance(text, string_type): 20 | text = text.encode('utf-8') 21 | return text 22 | -------------------------------------------------------------------------------- /flask_storage/_utils.py: -------------------------------------------------------------------------------- 1 | class ConfigItem(object): 2 | """The configuration item which may be bound with a instance. 3 | 4 | :param name: the property name. 5 | :param namespace: optional. the name of the attribute which contains all 6 | configuration nested in the instance. 7 | :param default: optional. the value which be provided while the 8 | configuration item has been missed. 9 | :param required: optional. if this paramater be ``True`` , getting missed 10 | configuration item without default value will trigger a 11 | ``RuntimeError`` . 12 | """ 13 | 14 | def __init__(self, name, namespace="config", default=None, required=False): 15 | self.name = name 16 | self.namespace = namespace 17 | self.default = default 18 | self.required = required 19 | 20 | def __repr__(self): 21 | template = "ConfigItem(%r, namespace=%r, default=%r, required=%r)" 22 | return template % (self.name, self.name, self.default, self.required) 23 | 24 | def __get__(self, instance, owner): 25 | if instance is None: 26 | return self 27 | namespace = self._namespace(instance) 28 | if self.name not in namespace and self.required: 29 | raise RuntimeError("missing %s['%s'] in %r" % 30 | (self.namespace, self.name, instance)) 31 | return namespace.get(self.name, self.default) 32 | 33 | def __set__(self, instance, value): 34 | namespace = self._namespace(instance) 35 | namespace[self.name] = value 36 | 37 | def _namespace(self, instance): 38 | """Gets exists namespace or creates it.""" 39 | if not hasattr(instance, self.namespace): 40 | setattr(instance, self.namespace, {}) 41 | return getattr(instance, self.namespace) 42 | -------------------------------------------------------------------------------- /flask_storage/local.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_storage.local 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | Local storage, save the file in local directory. 7 | 8 | :copyright: (c) 2013 Hsiaoming Yang. 9 | """ 10 | 11 | import os 12 | import hashlib 13 | import time 14 | from ._compat import to_bytes, urljoin 15 | from ._base import BaseStorage, UploadFileExists 16 | from ._utils import ConfigItem 17 | 18 | 19 | class LocalStorage(BaseStorage): 20 | """Storage for local filesystem. 21 | 22 | Configuration: 23 | 24 | - base_dir: save file in base dir 25 | - base_url: base url root 26 | """ 27 | base_path = ConfigItem('base_path', required=True) 28 | base_dir = ConfigItem('base_dir', default='') 29 | base_url = ConfigItem('base_url', default='') 30 | secret_key = ConfigItem('secret_key') 31 | expires = ConfigItem('expires', default=3600) 32 | private = ConfigItem('private', default=False) 33 | 34 | def url(self, filename, base_dir=None): 35 | """Generate the url for a filename. 36 | 37 | :param filename: Name of the file. 38 | """ 39 | filename = os.path.join(base_dir or self.base_dir, filename) 40 | 41 | file_url = urljoin(self.base_url, filename) 42 | 43 | if self.private: 44 | expires, token = self.generate_download_token(filename) 45 | file_url = '{}?e={}&t={}'.format(file_url, expires, token) 46 | return file_url 47 | 48 | def generate_download_token(self, filename=None, expires=None): 49 | """Generate the token and expires for download. 50 | 51 | :param filename: Name of the file. 52 | """ 53 | expires = int(time.time()) + (expires or self.expires) 54 | token = hashlib.md5(self.secret_key+filename+str(expires)).hexdigest()[8:16] 55 | return expires, token 56 | 57 | def check_private_url(self, filename, expires, token): 58 | new_token = hashlib.md5(self.secret_key+filename+expires).hexdigest()[8:16] 59 | return new_token == token 60 | 61 | def exists(self, filename, base_dir=None): 62 | """Detect if the file exists. 63 | 64 | :param filename: name of the file. 65 | """ 66 | dest = os.path.join(self.base_path, base_dir or self.base_dir, filename) 67 | return os.path.exists(dest) 68 | 69 | def read(self, filename, base_dir=None): 70 | """Read content of a file.""" 71 | dest = os.path.join(self.base_path, base_dir or self.base_dir, filename) 72 | with open(dest) as f: 73 | return f.read() 74 | 75 | def write(self, filename, body, headers=None, base_dir=None): 76 | """Write content to a file.""" 77 | dest = os.path.join(self.base_path, base_dir or self.base_dir, filename) 78 | dirname = os.path.dirname(dest) 79 | if not os.path.exists(dirname): 80 | os.makedirs(dirname) 81 | 82 | with open(dest, 'wb') as f: 83 | return f.write(to_bytes(body)) 84 | 85 | def delete(self, filename, base_dir=None): 86 | """Delete the specified file. 87 | 88 | :param filename: name of the file. 89 | """ 90 | dest = os.path.join(self.base_path, base_dir or self.base_dir, filename) 91 | return os.remove(dest) 92 | 93 | def save(self, storage, filename=None, base_dir=None): 94 | """Save a storage (`werkzeug.FileStorage`) with the specified 95 | filename. 96 | 97 | :param storage: The storage to be saved. 98 | :param filename: The destination of the storage. 99 | """ 100 | 101 | self.check(storage) 102 | 103 | filename = filename if filename else storage.filename 104 | 105 | _, extname = os.path.splitext(filename) 106 | ext = extname.lower()[1:] 107 | if not self.extension_allowed(ext): 108 | _, extname = os.path.splitext(storage.filename) 109 | ext = extname.lower()[1:] 110 | filename = '{}.{}'.format(filename, ext) if ext else filename 111 | 112 | dest = os.path.join(self.base_path, base_dir or self.base_dir, filename) 113 | 114 | folder = os.path.dirname(dest) 115 | if not os.path.exists(folder): 116 | os.makedirs(folder) 117 | 118 | if os.path.exists(dest): 119 | raise UploadFileExists() 120 | 121 | storage.save(dest) 122 | return filename 123 | -------------------------------------------------------------------------------- /flask_storage/qiniu.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_storage.qiniu 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | Qiniu storage, save the file to Qiniu. 7 | """ 8 | 9 | from __future__ import absolute_import 10 | import os 11 | from werkzeug import cached_property 12 | import qiniu 13 | 14 | from ._base import BaseStorage 15 | from ._compat import urljoin 16 | from ._utils import ConfigItem 17 | 18 | 19 | class QiniuStorage(BaseStorage): 20 | 21 | access_key = ConfigItem('access_key', required=True) 22 | secret_key = ConfigItem('secret_key', required=True) 23 | 24 | bucket = ConfigItem('bucket', required=True) 25 | base_url = ConfigItem('base_url', required=True) 26 | base_dir = ConfigItem('base_dir', default='') 27 | private = ConfigItem('private', default=False) 28 | expires = ConfigItem('expires', default=3600) 29 | 30 | def __init__(self, name, extensions, config): 31 | super(QiniuStorage, self).__init__(name, extensions, config) 32 | 33 | @cached_property 34 | def auth(self): 35 | return qiniu.Auth(self.access_key, self.secret_key) 36 | 37 | @cached_property 38 | def _client(self): 39 | return qiniu.BucketManager(self.auth) 40 | 41 | def url(self, filename, base_dir=None): 42 | """Generate the url for a filename. 43 | 44 | :param filename: Name of the file. 45 | """ 46 | filename = os.path.join(base_dir or self.base_dir, filename) 47 | 48 | file_url = urljoin(self.base_url, filename) 49 | if self.private: 50 | return self.auth.private_download_url(file_url, expires=self.expires) 51 | return file_url 52 | 53 | def generate_upload_token(self, filename=None): 54 | """ 55 | Generate a upload token used by client. 56 | :param filename: Client can upload file if filename is None. 57 | Otherwise, client can modify the file. 58 | """ 59 | if filename: 60 | token = self.auth.upload_token(self.bucket, filename) 61 | else: 62 | token = self.auth.upload_token(self.bucket) 63 | return token 64 | 65 | def save(self, storage, filename=None, base_dir=None, token=None): 66 | self.check(storage) 67 | 68 | filename = filename if filename else storage.filename 69 | 70 | _, extname = os.path.splitext(filename) 71 | ext = extname.lower()[1:] 72 | if not self.extension_allowed(ext): 73 | _, extname = os.path.splitext(storage.filename) 74 | ext = extname.lower()[1:] 75 | filename = '{}.{}'.format(filename, ext) if ext else filename 76 | 77 | full_filename = os.path.join(base_dir or self.base_dir, filename) 78 | 79 | if token is None: 80 | token = self.generate_upload_token() 81 | stream = storage.stream 82 | ret, info = qiniu.put_data(token, full_filename, stream) 83 | if ret is None: 84 | raise QiniuException(info) 85 | return filename 86 | 87 | def delete(self, filename, base_dir=None): 88 | filename = os.path.join(base_dir or self.base_dir, filename) 89 | ret, info = self._client.delete(self.bucket, filename) 90 | if ret is None: 91 | raise QiniuException(info) 92 | return ret 93 | 94 | 95 | class QiniuException(Exception): 96 | pass 97 | -------------------------------------------------------------------------------- /flask_storage/s3.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_storage.s3 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | S3 storage, save the file to Amazon S3. 7 | 8 | :copyright: (c) 2013 Hsiaoming Yang. 9 | """ 10 | 11 | import mimetypes 12 | 13 | from werkzeug import cached_property 14 | from boto.s3.connection import S3Connection 15 | 16 | from ._base import BaseStorage, urljoin 17 | from ._utils import ConfigItem 18 | 19 | 20 | class S3Storage(BaseStorage): 21 | 22 | _params = ConfigItem('connection_params') 23 | 24 | access_key = ConfigItem('access_key', required=True) 25 | secret_key = ConfigItem('secret_key', required=True) 26 | bucket_name = ConfigItem('bucket', required=True) 27 | 28 | base_dir = ConfigItem('base_dir') 29 | base_url = ConfigItem('base_url') 30 | 31 | @cached_property 32 | def _connection(self): 33 | return S3Connection(self.access_key, self.secret_key, **self._params) 34 | 35 | @cached_property 36 | def bucket(self): 37 | if self.bucket_name not in self._connection: 38 | return self._connection.create_bucket(self.bucket_name) 39 | return self._connection.get_bucket(self.bucket_name) 40 | 41 | def url(self, filename): 42 | """Generate the url for a filename. 43 | 44 | :param filename: filename for generating the url 45 | """ 46 | if self.base_dir: 47 | filename = '%s/%s' % (self.base_dir, filename) 48 | return urljoin(self.base_url, filename) 49 | 50 | def read(self, filename): 51 | if self.base_dir: 52 | filename = '%s/%s' % (self.base_dir, filename) 53 | k = self.bucket.get_key(filename) 54 | if not k: 55 | return None 56 | return k.read() 57 | 58 | def _generate_key(self, filename, headers=None): 59 | if self.base_dir: 60 | filename = '%s/%s' % (self.base_dir, filename) 61 | 62 | k = self.bucket.new_key(filename) 63 | if not headers or 'Content-Type' not in headers: 64 | ct = mimetypes.guess_type(filename)[0] 65 | if ct: 66 | k.set_metadata('Content-Type', ct) 67 | 68 | return k 69 | 70 | def write(self, filename, body, headers=None): 71 | k = self._generate_key(filename, headers) 72 | # since Flask-Storage is designed for public storage 73 | # we need to set it public-read 74 | return k.set_contents_from_string( 75 | body, headers=headers, policy='public-read' 76 | ) 77 | 78 | def save(self, storage, filename, headers=None, check=True): 79 | """Save a storage (`werkzeug.FileStorage`) with the specified 80 | filename. 81 | 82 | :param storage: The storage to be saved. 83 | :param filename: The destination of the storage. 84 | """ 85 | 86 | if check: 87 | self.check(storage) 88 | 89 | k = self._generate_key(filename) 90 | return k.set_contents_from_stream( 91 | storage.stream, headers=headers, policy='public-read' 92 | ) 93 | -------------------------------------------------------------------------------- /flask_storage/sae.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_storage.contrib.sae 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Storage for SAE backend. 7 | """ 8 | 9 | from ._base import BaseStorage 10 | from ._utils import ConfigItem 11 | 12 | 13 | class SaeStorage(BaseStorage): 14 | 15 | bucket = ConfigItem('STORAGE_SAE_BUCKET') 16 | 17 | def save(self): 18 | pass # TODO 19 | -------------------------------------------------------------------------------- /flask_storage/upyun.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | flask_storage.upyun 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | Upyun storage, upload files to upyun. 7 | 8 | :copyright: (c) 2013 Hsiaoming Yang. 9 | """ 10 | 11 | import os 12 | import base64 13 | 14 | from ._base import BaseStorage 15 | from ._compat import http, urljoin 16 | from ._utils import ConfigItem 17 | 18 | 19 | _missing = object() 20 | 21 | 22 | def http_request(uri, headers=None, data=None, method=None): 23 | if headers is None: 24 | headers = {} 25 | 26 | if data and not method: 27 | method = 'POST' 28 | elif not method: 29 | method = 'GET' 30 | 31 | req = http.Request(uri, headers=headers, data=data) 32 | req.get_method = lambda: method.upper() 33 | try: 34 | resp = http.urlopen(req) 35 | except http.HTTPError as resp: 36 | pass 37 | content = resp.read() 38 | resp.close() 39 | return resp, content 40 | 41 | 42 | class UpyunStorage(BaseStorage): 43 | 44 | bucket = ConfigItem('bucket') 45 | base_dir = ConfigItem('base_dir') 46 | base_url = ConfigItem('base_url', default=_missing) 47 | 48 | username = ConfigItem('username') 49 | password = ConfigItem('password') 50 | 51 | @property 52 | def root(self): 53 | uri = 'http://v0.api.upyun.com/%s/' % self.bucket 54 | if self.base_dir: 55 | uri = urljoin(uri, self.base_dir) 56 | return uri 57 | 58 | def request(self, uri, data=None, method=None, headers=None): 59 | """Make a request for upyun api. 60 | 61 | You rarely need this API, use save instead. 62 | """ 63 | auth = base64.b64encode('%s:%s' % (self.username, self.password)) 64 | 65 | if not headers: 66 | headers = {} 67 | 68 | headers['Authorization'] = 'Basic %s' % auth 69 | return http_request(uri, headers=headers, data=data, method=method) 70 | 71 | def url(self, filename): 72 | """Generate the url for a filename. 73 | 74 | :param filename: Name of the file. 75 | """ 76 | if self.base_url is _missing: 77 | base_url = 'http://%s.b0.upaiyun.com/' % self.bucket 78 | else: 79 | base_url = self.base_url 80 | 81 | if self.base_dir: 82 | urlbase = urljoin(base_url, self.base_dir) 83 | 84 | return urljoin(urlbase, filename) 85 | 86 | def usage(self): 87 | """Find the usage of your bucket. 88 | 89 | This function returns an integer. 90 | """ 91 | uri = '%s?usage' % self.root 92 | resp, content = self.request(uri) 93 | return content 94 | 95 | def save(self, storage, filename): 96 | """Save a storage (`werkzeug.FileStorage`) with the specified 97 | filename. 98 | 99 | :param storage: The storage to be saved. 100 | :param filename: The filename you want to save as. 101 | """ 102 | self.check(storage) 103 | uri = urljoin(self.root, filename) 104 | headers = {'Mkdir': 'true'} 105 | stream = storage.stream 106 | if isinstance(stream, file): 107 | length = os.fstat(stream.fileno()).st_size 108 | headers['Content-Length'] = length 109 | self.request(uri, stream, 'PUT', headers) 110 | return self.url(filename) 111 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | qiniu 2 | boto 3 | Flask 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | import re 11 | 12 | 13 | def fread(filepath): 14 | with open(filepath) as f: 15 | return f.read() 16 | 17 | 18 | def version(): 19 | content = fread('flask_storage/__init__.py') 20 | m = re.findall(r'__version__\s*=\s*\'(.*)\'', content) 21 | return m[0] 22 | 23 | 24 | extras_require = dict( 25 | qiniu=['qiniu'], 26 | s3=['boto'], 27 | full=['qiniu', 'boto'], 28 | ) 29 | 30 | 31 | setup( 32 | name='Flask-Storage', 33 | version=version(), 34 | author='Hsiaoming Yang', 35 | author_email='me@lepture.com', 36 | url='https://github.com/lepture/flask-storage', 37 | packages=["flask_storage"], 38 | description="Flask upload and storage extensions.", 39 | long_description=fread('README.rst'), 40 | license='BSD', 41 | platforms='any', 42 | zip_safe=False, 43 | include_package_data=True, 44 | install_requires=[ 45 | 'Flask', 46 | ], 47 | tests_require=[ 48 | 'nose', 49 | ], 50 | extras_require=extras_require, 51 | test_suite='nose.collector', 52 | classifiers=[ 53 | 'Development Status :: 3 - Alpha', 54 | 'Environment :: Web Environment', 55 | 'Intended Audience :: Developers', 56 | 'License :: OSI Approved', 57 | 'License :: OSI Approved :: BSD License', 58 | 'Operating System :: MacOS', 59 | 'Operating System :: POSIX', 60 | 'Operating System :: POSIX :: Linux', 61 | 'Programming Language :: Python', 62 | 'Programming Language :: Python :: 2.6', 63 | 'Programming Language :: Python :: 2.7', 64 | 'Programming Language :: Python :: Implementation', 65 | 'Programming Language :: Python :: Implementation :: CPython', 66 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 67 | 'Topic :: Software Development :: Libraries :: Python Modules', 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepture/flask-storage/b246c4a8b60bbb39fa6adbc76e7454429312c41b/tests/__init__.py -------------------------------------------------------------------------------- /tests/_base.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | import unittest 3 | 4 | from flask import Flask, request 5 | 6 | 7 | class BaseCase(unittest.TestCase): 8 | CONFIG = { 9 | 'TESTING': True, 10 | 'DEBUG': True, 11 | } 12 | 13 | def setUp(self): 14 | self.app = self.create_app() 15 | self.client = self.app.test_client() 16 | 17 | def create_app(self): 18 | app = Flask(__name__) 19 | app.config.update(self.CONFIG) 20 | 21 | @app.route('/upload', methods=['POST']) 22 | def upload(): 23 | image = request.files.get('image') 24 | return str(self.storage.save(image, 'flask.png')) 25 | 26 | return app 27 | 28 | def upload(self): 29 | image = self.app.open_resource("flask.png") 30 | response = self.client.post('/upload', data={'image': image}) 31 | return response 32 | 33 | def patch(self, target, *args, **kwargs): 34 | patcher = patch(target, *args, **kwargs) 35 | patched_object = patcher.start() 36 | self.addCleanup(patcher.stop) 37 | return patched_object 38 | -------------------------------------------------------------------------------- /tests/flask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lepture/flask-storage/b246c4a8b60bbb39fa6adbc76e7454429312c41b/tests/flask.png -------------------------------------------------------------------------------- /tests/test_local.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from flask_storage.local import LocalStorage 5 | from ._base import BaseCase 6 | 7 | 8 | class TestLocalStorage(BaseCase): 9 | CONFIG = dict( 10 | base_dir='tmp', 11 | base_url='/url/' 12 | ) 13 | storage = LocalStorage('local', None, CONFIG) 14 | 15 | def setUp(self): 16 | super(TestLocalStorage, self).setUp() 17 | self._clean_up() 18 | 19 | def tearDown(self): 20 | super(TestLocalStorage, self).tearDown() 21 | self._clean_up() 22 | 23 | def _clean_up(self): 24 | if os.path.isdir(self.CONFIG['base_dir']): 25 | shutil.rmtree(self.CONFIG['base_dir']) 26 | 27 | def test_upload(self): 28 | response = self.upload() 29 | assert response.status_code == 200 30 | assert response.data == b'/url/flask.png' 31 | assert os.path.exists('tmp/flask.png') 32 | -------------------------------------------------------------------------------- /tests/test_qiniu.py: -------------------------------------------------------------------------------- 1 | from flask_storage.qiniu import QiniuStorage 2 | from ._base import BaseCase 3 | 4 | 5 | class TestQiniuStorage(BaseCase): 6 | CONFIG = dict( 7 | access_key='test_access_key', 8 | secret_key='test_secret_key', 9 | bucket='qiniu_test' 10 | ) 11 | 12 | storage = QiniuStorage('qiniu', None, CONFIG) 13 | 14 | def setUp(self): 15 | super(TestQiniuStorage, self).setUp() 16 | self.put_data = self.patch('qiniu.put_data') 17 | # self.upload_token = self.patch('qiniu.rs.PutPolicy.token') 18 | 19 | def test_upload(self): 20 | ret, err = {'key': 'flask.png'}, None 21 | self.put_data.return_value = ret, err 22 | response = self.upload() 23 | # assert self.upload_token.call_count == 1 24 | assert self.put_data.call_count == 1 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34 3 | [testenv] 4 | deps = 5 | boto 6 | qiniu 7 | nose 8 | nose-cov 9 | mock 10 | commands = 11 | nosetests --with-cov --cov flask_storage tests/ 12 | --------------------------------------------------------------------------------