├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── _themes │ ├── README │ ├── flask │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ ├── flask_small │ │ ├── layout.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py ├── conf.py ├── index.rst └── make.bat ├── example ├── photolog.py ├── static │ └── style.css └── templates │ ├── index.html │ ├── layout.html │ ├── login.html │ └── new.html ├── flask_uploads.py ├── setup.py ├── tests.py └── tox.ini /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Matthew "LeafStorm" Frazier 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include tests/*.py 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-Uploads 2 | 3 | Flask-Uploads provides file uploads for Flask. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | pip install flask-uploads 9 | ``` 10 | -------------------------------------------------------------------------------- /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 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Uploads.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Uploads.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Uploads" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Uploads" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | background-color: #ddd; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background: #fafafa; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | hr { 39 | border: 1px solid #B1B4B6; 40 | } 41 | 42 | div.body { 43 | background-color: #ffffff; 44 | color: #3E4349; 45 | padding: 0 30px 30px 30px; 46 | min-height: 34em; 47 | } 48 | 49 | img.floatingflask { 50 | padding: 0 0 10px 10px; 51 | float: right; 52 | } 53 | 54 | div.footer { 55 | position: absolute; 56 | right: 0; 57 | margin-top: -70px; 58 | text-align: right; 59 | color: #888; 60 | padding: 10px; 61 | font-size: 14px; 62 | } 63 | 64 | div.footer a { 65 | color: #888; 66 | text-decoration: underline; 67 | } 68 | 69 | div.related { 70 | line-height: 32px; 71 | color: #888; 72 | } 73 | 74 | div.related ul { 75 | padding: 0 0 0 10px; 76 | } 77 | 78 | div.related a { 79 | color: #444; 80 | } 81 | 82 | div.sphinxsidebar { 83 | font-size: 14px; 84 | line-height: 1.5; 85 | } 86 | 87 | div.sphinxsidebarwrapper { 88 | padding: 0 20px; 89 | } 90 | 91 | div.sphinxsidebarwrapper p.logo { 92 | padding: 20px 0 10px 0; 93 | margin: 0; 94 | text-align: center; 95 | } 96 | 97 | div.sphinxsidebar h3, 98 | div.sphinxsidebar h4 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | color: #222; 101 | font-size: 24px; 102 | font-weight: normal; 103 | margin: 20px 0 5px 0; 104 | padding: 0; 105 | } 106 | 107 | div.sphinxsidebar h4 { 108 | font-size: 20px; 109 | } 110 | 111 | div.sphinxsidebar h3 a { 112 | color: #444; 113 | } 114 | 115 | div.sphinxsidebar p { 116 | color: #555; 117 | margin: 10px 0; 118 | } 119 | 120 | div.sphinxsidebar ul { 121 | margin: 10px 0; 122 | padding: 0; 123 | color: #000; 124 | } 125 | 126 | div.sphinxsidebar a { 127 | color: #444; 128 | text-decoration: none; 129 | } 130 | 131 | div.sphinxsidebar a:hover { 132 | text-decoration: underline; 133 | } 134 | 135 | div.sphinxsidebar input { 136 | border: 1px solid #ccc; 137 | font-family: 'Georgia', serif; 138 | font-size: 1em; 139 | } 140 | 141 | /* -- body styles ----------------------------------------------------------- */ 142 | 143 | a { 144 | color: #004B6B; 145 | text-decoration: underline; 146 | } 147 | 148 | a:hover { 149 | color: #6D4100; 150 | text-decoration: underline; 151 | } 152 | 153 | div.body { 154 | padding-bottom: 40px; /* saved for footer */ 155 | } 156 | 157 | div.body h1, 158 | div.body h2, 159 | div.body h3, 160 | div.body h4, 161 | div.body h5, 162 | div.body h6 { 163 | font-family: 'Garamond', 'Georgia', serif; 164 | font-weight: normal; 165 | margin: 30px 0px 10px 0px; 166 | padding: 0; 167 | } 168 | 169 | div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; } 170 | div.body h2 { font-size: 180%; } 171 | div.body h3 { font-size: 150%; } 172 | div.body h4 { font-size: 130%; } 173 | div.body h5 { font-size: 100%; } 174 | div.body h6 { font-size: 100%; } 175 | 176 | a.headerlink { 177 | color: white; 178 | padding: 0 4px; 179 | text-decoration: none; 180 | } 181 | 182 | a.headerlink:hover { 183 | color: #444; 184 | background: #eaeaea; 185 | } 186 | 187 | div.body p, div.body dd, div.body li { 188 | line-height: 1.4em; 189 | } 190 | 191 | div.admonition { 192 | background: #fafafa; 193 | margin: 20px -30px; 194 | padding: 10px 30px; 195 | border-top: 1px solid #ccc; 196 | border-bottom: 1px solid #ccc; 197 | } 198 | 199 | div.admonition p.admonition-title { 200 | font-family: 'Garamond', 'Georgia', serif; 201 | font-weight: normal; 202 | font-size: 24px; 203 | margin: 0 0 10px 0; 204 | padding: 0; 205 | line-height: 1; 206 | } 207 | 208 | div.admonition p.last { 209 | margin-bottom: 0; 210 | } 211 | 212 | div.highlight{ 213 | background-color: white; 214 | } 215 | 216 | dt:target, .highlight { 217 | background: #FAF3E8; 218 | } 219 | 220 | div.note { 221 | background-color: #eee; 222 | border: 1px solid #ccc; 223 | } 224 | 225 | div.seealso { 226 | background-color: #ffc; 227 | border: 1px solid #ff6; 228 | } 229 | 230 | div.topic { 231 | background-color: #eee; 232 | } 233 | 234 | div.warning { 235 | background-color: #ffe4e4; 236 | border: 1px solid #f66; 237 | } 238 | 239 | p.admonition-title { 240 | display: inline; 241 | } 242 | 243 | p.admonition-title:after { 244 | content: ":"; 245 | } 246 | 247 | pre, tt { 248 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 249 | font-size: 0.9em; 250 | } 251 | 252 | img.screenshot { 253 | } 254 | 255 | tt.descname, tt.descclassname { 256 | font-size: 0.95em; 257 | } 258 | 259 | tt.descname { 260 | padding-right: 0.08em; 261 | } 262 | 263 | img.screenshot { 264 | -moz-box-shadow: 2px 2px 4px #eee; 265 | -webkit-box-shadow: 2px 2px 4px #eee; 266 | box-shadow: 2px 2px 4px #eee; 267 | } 268 | 269 | table.docutils { 270 | border: 1px solid #888; 271 | -moz-box-shadow: 2px 2px 4px #eee; 272 | -webkit-box-shadow: 2px 2px 4px #eee; 273 | box-shadow: 2px 2px 4px #eee; 274 | } 275 | 276 | table.docutils td, table.docutils th { 277 | border: 1px solid #888; 278 | padding: 0.25em 0.7em; 279 | } 280 | 281 | table.field-list, table.footnote { 282 | border: none; 283 | -moz-box-shadow: none; 284 | -webkit-box-shadow: none; 285 | box-shadow: none; 286 | } 287 | 288 | table.footnote { 289 | margin: 15px 0; 290 | width: 100%; 291 | border: 1px solid #eee; 292 | } 293 | 294 | table.field-list th { 295 | padding: 0 0.8em 0 0; 296 | } 297 | 298 | table.field-list td { 299 | padding: 0; 300 | } 301 | 302 | table.footnote td { 303 | padding: 0.5em; 304 | } 305 | 306 | dl { 307 | margin: 0; 308 | padding: 0; 309 | } 310 | 311 | dl dd { 312 | margin-left: 30px; 313 | } 314 | 315 | pre { 316 | background: #eee; 317 | padding: 7px 30px; 318 | margin: 15px -30px; 319 | line-height: 1.3em; 320 | } 321 | 322 | dl pre { 323 | margin-left: -60px; 324 | padding-left: 60px; 325 | } 326 | 327 | dl dl pre { 328 | margin-left: -90px; 329 | padding-left: 90px; 330 | } 331 | 332 | tt { 333 | background-color: #ecf0f3; 334 | color: #222; 335 | /* padding: 1px 2px; */ 336 | } 337 | 338 | tt.xref, a tt { 339 | background-color: #FBFBFB; 340 | } 341 | 342 | a:hover tt { 343 | background: #EEE; 344 | } 345 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | color: #000; 20 | background: white; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 40px auto 0 auto; 32 | width: 700px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | color: #3E4349; 42 | padding: 0 30px 30px 30px; 43 | } 44 | 45 | img.floatingflask { 46 | padding: 0 0 10px 10px; 47 | float: right; 48 | } 49 | 50 | div.footer { 51 | text-align: right; 52 | color: #888; 53 | padding: 10px; 54 | font-size: 14px; 55 | width: 650px; 56 | margin: 0 auto 40px auto; 57 | } 58 | 59 | div.footer a { 60 | color: #888; 61 | text-decoration: underline; 62 | } 63 | 64 | div.related { 65 | line-height: 32px; 66 | color: #888; 67 | } 68 | 69 | div.related ul { 70 | padding: 0 0 0 10px; 71 | } 72 | 73 | div.related a { 74 | color: #444; 75 | } 76 | 77 | /* -- body styles ----------------------------------------------------------- */ 78 | 79 | a { 80 | color: #004B6B; 81 | text-decoration: underline; 82 | } 83 | 84 | a:hover { 85 | color: #6D4100; 86 | text-decoration: underline; 87 | } 88 | 89 | div.body { 90 | padding-bottom: 40px; /* saved for footer */ 91 | } 92 | 93 | div.body h1, 94 | div.body h2, 95 | div.body h3, 96 | div.body h4, 97 | div.body h5, 98 | div.body h6 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | font-weight: normal; 101 | margin: 30px 0px 10px 0px; 102 | padding: 0; 103 | } 104 | 105 | {% if theme_index_logo %} 106 | div.indexwrapper h1 { 107 | text-indent: -999999px; 108 | background: url({{ theme_index_logo }}) no-repeat center center; 109 | height: {{ theme_index_logo_height }}; 110 | } 111 | {% endif %} 112 | 113 | div.body h2 { font-size: 180%; } 114 | div.body h3 { font-size: 150%; } 115 | div.body h4 { font-size: 130%; } 116 | div.body h5 { font-size: 100%; } 117 | div.body h6 { font-size: 100%; } 118 | 119 | a.headerlink { 120 | color: white; 121 | padding: 0 4px; 122 | text-decoration: none; 123 | } 124 | 125 | a.headerlink:hover { 126 | color: #444; 127 | background: #eaeaea; 128 | } 129 | 130 | div.body p, div.body dd, div.body li { 131 | line-height: 1.4em; 132 | } 133 | 134 | div.admonition { 135 | background: #fafafa; 136 | margin: 20px -30px; 137 | padding: 10px 30px; 138 | border-top: 1px solid #ccc; 139 | border-bottom: 1px solid #ccc; 140 | } 141 | 142 | div.admonition p.admonition-title { 143 | font-family: 'Garamond', 'Georgia', serif; 144 | font-weight: normal; 145 | font-size: 24px; 146 | margin: 0 0 10px 0; 147 | padding: 0; 148 | line-height: 1; 149 | } 150 | 151 | div.admonition p.last { 152 | margin-bottom: 0; 153 | } 154 | 155 | div.highlight{ 156 | background-color: white; 157 | } 158 | 159 | dt:target, .highlight { 160 | background: #FAF3E8; 161 | } 162 | 163 | div.note { 164 | background-color: #eee; 165 | border: 1px solid #ccc; 166 | } 167 | 168 | div.seealso { 169 | background-color: #ffc; 170 | border: 1px solid #ff6; 171 | } 172 | 173 | div.topic { 174 | background-color: #eee; 175 | } 176 | 177 | div.warning { 178 | background-color: #ffe4e4; 179 | border: 1px solid #f66; 180 | } 181 | 182 | p.admonition-title { 183 | display: inline; 184 | } 185 | 186 | p.admonition-title:after { 187 | content: ":"; 188 | } 189 | 190 | pre, tt { 191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 192 | font-size: 0.85em; 193 | } 194 | 195 | img.screenshot { 196 | } 197 | 198 | tt.descname, tt.descclassname { 199 | font-size: 0.95em; 200 | } 201 | 202 | tt.descname { 203 | padding-right: 0.08em; 204 | } 205 | 206 | img.screenshot { 207 | -moz-box-shadow: 2px 2px 4px #eee; 208 | -webkit-box-shadow: 2px 2px 4px #eee; 209 | box-shadow: 2px 2px 4px #eee; 210 | } 211 | 212 | table.docutils { 213 | border: 1px solid #888; 214 | -moz-box-shadow: 2px 2px 4px #eee; 215 | -webkit-box-shadow: 2px 2px 4px #eee; 216 | box-shadow: 2px 2px 4px #eee; 217 | } 218 | 219 | table.docutils td, table.docutils th { 220 | border: 1px solid #888; 221 | padding: 0.25em 0.7em; 222 | } 223 | 224 | table.field-list, table.footnote { 225 | border: none; 226 | -moz-box-shadow: none; 227 | -webkit-box-shadow: none; 228 | box-shadow: none; 229 | } 230 | 231 | table.footnote { 232 | margin: 15px 0; 233 | width: 100%; 234 | border: 1px solid #eee; 235 | } 236 | 237 | table.field-list th { 238 | padding: 0 0.8em 0 0; 239 | } 240 | 241 | table.field-list td { 242 | padding: 0; 243 | } 244 | 245 | table.footnote td { 246 | padding: 0.5em; 247 | } 248 | 249 | dl { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | dl dd { 255 | margin-left: 30px; 256 | } 257 | 258 | pre { 259 | padding: 0; 260 | margin: 15px -30px; 261 | padding: 8px; 262 | line-height: 1.3em; 263 | padding: 7px 30px; 264 | background: #eee; 265 | border-radius: 2px; 266 | -moz-border-radius: 2px; 267 | -webkit-border-radius: 2px; 268 | } 269 | 270 | dl pre { 271 | margin-left: -60px; 272 | padding-left: 60px; 273 | } 274 | 275 | tt { 276 | background-color: #ecf0f3; 277 | color: #222; 278 | /* padding: 1px 2px; */ 279 | } 280 | 281 | tt.xref, a tt { 282 | background-color: #FBFBFB; 283 | } 284 | 285 | a:hover tt { 286 | background: #EEE; 287 | } 288 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = '' 9 | index_logo_height = 120px 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /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/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-Uploads documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jul 8 14:12:15 2010. 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.append(os.path.abspath('_themes')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Flask-Uploads' 44 | copyright = u'2010, Matthew "LeafStorm" Frazier' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.1.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | default_role = 'obj' 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | #pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'flask_small' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | html_theme_options = {'github_fork': None, 'index_logo': None} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | html_theme_path = ['_themes'] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = '' 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'Flask-Uploadsdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'Flask-Uploads.tex', u'Flask-Uploads Documentation', 182 | u'Matthew "LeafStorm" Frazier', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'flask-uploads', u'Flask-Uploads Documentation', 215 | [u'Matthew "LeafStorm" Frazier'], 1) 216 | ] 217 | 218 | 219 | # Example configuration for intersphinx: refer to the Python standard library. 220 | intersphinx_mapping = {'http://docs.python.org/': None, 221 | 'http://flask.pocoo.org/docs/': None} 222 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Flask-Uploads 3 | ============= 4 | .. currentmodule:: flaskext.uploads 5 | 6 | Flask-Uploads allows your application to flexibly and efficiently handle file 7 | uploading and serving the uploaded files. 8 | You can create different sets of uploads - one for document attachments, one 9 | for photos, etc. - and the application can be configured to save them all in 10 | different places and to generate different URLs for them. 11 | 12 | .. contents:: 13 | :local: 14 | :backlinks: none 15 | 16 | 17 | Configuration 18 | ============= 19 | If you're just deploying an application that uses Flask-Uploads, you can 20 | customize its behavior extensively from the application's configuration. 21 | Check the application's documentation or source code to see how it loads its 22 | configuration. 23 | 24 | The settings below apply for a single set of uploads, replacing `FILES` with 25 | the name of the set (i.e. `PHOTOS`, `ATTACHMENTS`): 26 | 27 | `UPLOADED_FILES_DEST` 28 | This indicates the directory uploaded files will be saved to. 29 | 30 | `UPLOADED_FILES_URL` 31 | If you have a server set up to serve the files in this set, this should be 32 | the URL they are publicly accessible from. Include the trailing slash. 33 | 34 | `UPLOADED_FILES_ALLOW` 35 | This lets you allow file extensions not allowed by the upload set in the 36 | code. 37 | 38 | `UPLOADED_FILES_DENY` 39 | This lets you deny file extensions allowed by the upload set in the code. 40 | 41 | To save on configuration time, there are two settings you can provide 42 | that apply as "defaults" if you don't provide the proper settings otherwise. 43 | 44 | `UPLOADS_DEFAULT_DEST` 45 | If you set this, then if an upload set's destination isn't otherwise 46 | declared, then its uploads will be stored in a subdirectory of this 47 | directory. For example, if you set this to ``/var/uploads``, then a set 48 | named photos will store its uploads in ``/var/uploads/photos``. 49 | 50 | `UPLOADS_DEFAULT_URL` 51 | If you have a server set up to serve from `UPLOADS_DEFAULT_DEST`, then 52 | set the server's base URL here. Continuing the example above, if 53 | ``/var/uploads`` is accessible from ``http://localhost:5001``, then you 54 | would set this to ``http://localhost:5001/`` and URLs for the photos set 55 | would start with ``http://localhost:5001/photos``. Include the trailing 56 | slash. 57 | 58 | However, you don't have to set any of the ``_URL`` settings - if you don't, 59 | then they will be served internally by Flask. They are just there so if you 60 | have heavy upload traffic, you can have a faster production server like Nginx 61 | or Lighttpd serve the uploads. 62 | 63 | If you are running Flask 0.6 or greater, or the application uses 64 | ``patch_request_class(app, None)``, then you can set `MAX_CONTENT_LENGTH` to 65 | limit the size of uploaded files. 66 | 67 | 68 | Upload Sets 69 | =========== 70 | An "upload set" is a single collection of files. You just declare them in the 71 | code:: 72 | 73 | photos = UploadSet('photos', IMAGES) 74 | 75 | And then you can use the `~UploadSet.save` method to save uploaded files and 76 | `~UploadSet.path` and `~UploadSet.url` to access them. For example:: 77 | 78 | @app.route('/upload', methods=['GET', 'POST']) 79 | def upload(): 80 | if request.method == 'POST' and 'photo' in request.files: 81 | filename = photos.save(request.files['photo']) 82 | rec = Photo(filename=filename, user=g.user.id) 83 | rec.store() 84 | flash("Photo saved.") 85 | return redirect(url_for('show', id=rec.id)) 86 | return render_template('upload.html') 87 | 88 | @app.route('/photo/') 89 | def show(id): 90 | photo = Photo.load(id) 91 | if photo is None: 92 | abort(404) 93 | url = photos.url(photo.filename) 94 | return render_template('show.html', url=url, photo=photo) 95 | 96 | If you have a "default location" for storing uploads - for example, if your 97 | app has an "instance" directory like `Zine`_ and uploads should be saved to 98 | the instance directory's ``uploads`` folder - you can pass a ``default_dest`` 99 | callable to the set constructor. It takes the application as its argument. 100 | For example:: 101 | 102 | media = UploadSet('media', default_dest=lambda app: app.instance_path) 103 | 104 | This won't prevent a different destination from being set in the config, 105 | though. It's just to save your users a little configuration time. 106 | 107 | .. _Zine: http://zine.pocoo.org/ 108 | 109 | 110 | App Configuration 111 | ================= 112 | An upload set's configuration is stored on an app. That way, you can have 113 | upload sets being used by multiple apps at once. You use the 114 | `configure_uploads` function to load the configuration for the upload sets. 115 | You pass in the app and all of the upload sets you want configured. Calling 116 | `configure_uploads` more than once is safe. :: 117 | 118 | configure_uploads(app, (photos, media)) 119 | 120 | If your app has a factory function, that is a good place to call this 121 | function. 122 | 123 | By default, though, Flask doesn't put any limits on the size of the uploaded 124 | data. To protect your application, you can use `patch_request_class`. If you 125 | call it with `None` as the second parameter, it will use the 126 | `MAX_CONTENT_LENGTH` setting to determine how large the upload can be. :: 127 | 128 | patch_request_class(app, None) 129 | 130 | You can also call it with a number to set an absolute limit, but that only 131 | exists for backwards compatibility reasons and is not recommended for 132 | production use. In addition, it's not necessary for Flask 0.6 or greater, so 133 | if your application is only intended to run on Flask 0.6, you don't need it. 134 | 135 | 136 | File Upload Forms 137 | ================= 138 | To actually upload the files, you need to properly set up your form. A form 139 | that uploads files needs to have its method set to POST and its enctype 140 | set to ``multipart/form-data``. If it's set to GET, it won't work at all, and 141 | if you don't set the enctype, only the filename will be transferred. 142 | 143 | The field itself should be an ````. 144 | 145 | .. code-block:: html+jinja 146 | 147 |
148 | ... 149 | 150 | ... 151 |
152 | 153 | 154 | Non-ASCII Filename Support 155 | ========================== 156 | Flask-Uplaods use Werkzeug's ``secure_filename()`` to check filename, it will omit 157 | Non-ASCII string. When the filename is completely consist of Non-ASCII string, 158 | such as Chinese or Japanese, it will return empty filename like ``.jpg``. If your 159 | files may encounter a situation like this, you have to set it's name or generate 160 | random filename:: 161 | 162 | uset.save(file, name='photo_123.') 163 | # If name ends with a dot, the file's extension will be appended to the end. 164 | 165 | 166 | API Documentation 167 | ================= 168 | This documentation is generated directly from the source code. 169 | 170 | 171 | Upload Sets 172 | ----------- 173 | .. autoclass:: UploadSet 174 | :members: 175 | 176 | .. autoclass:: UploadConfiguration 177 | 178 | 179 | Application Setup 180 | ----------------- 181 | .. autofunction:: configure_uploads 182 | 183 | .. autofunction:: patch_request_class 184 | 185 | 186 | Extension Constants 187 | ------------------- 188 | These are some default sets of extensions you can pass to the `UploadSet` 189 | constructor. 190 | 191 | .. autoclass:: AllExcept 192 | 193 | .. autodata:: DEFAULTS 194 | 195 | .. autodata:: ALL 196 | 197 | .. autodata:: TEXT 198 | 199 | .. autodata:: IMAGES 200 | 201 | .. autodata:: AUDIO 202 | 203 | .. autodata:: DOCUMENTS 204 | 205 | .. autodata:: DATA 206 | 207 | .. autodata:: SCRIPTS 208 | 209 | .. autodata:: ARCHIVES 210 | 211 | .. autodata:: EXECUTABLES 212 | 213 | 214 | Testing Utilities 215 | ----------------- 216 | .. autoclass:: TestingFileStorage 217 | 218 | 219 | Backwards Compatibility 220 | ======================= 221 | Version 0.1.3 222 | ------------- 223 | * The `_uploads` module/blueprint will not be registered if it is not needed 224 | to serve uploads. 225 | 226 | 227 | Version 0.1.1 228 | ------------- 229 | * `patch_request_class` now changes `max_content_length` instead of 230 | `max_form_memory_size`. 231 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Uploads.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Uploads.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /example/photolog.py: -------------------------------------------------------------------------------- 1 | """ 2 | photolog.py 3 | =========== 4 | This is a simple example app for Flask-Uploads. It uses Flask-CouchDB as well, 5 | because I like CouchDB. It's a basic photolog app that lets you submit blog 6 | posts that are photos. 7 | """ 8 | import datetime 9 | import uuid 10 | from flask import (Flask, request, url_for, redirect, render_template, flash, 11 | session, g) 12 | from flask.ext.couchdb import (CouchDBManager, Document, TextField, 13 | DateTimeField, ViewField) 14 | from flask.ext.uploads import (UploadSet, configure_uploads, IMAGES, 15 | UploadNotAllowed) 16 | 17 | # defaults 18 | 19 | DEBUG = False 20 | SECRET_KEY = ('\xa3\xb6\x15\xe3E\xc4\x8c\xbaT\x14\xd1:' 21 | '\xafc\x9c|.\xc0H\x8d\xf2\xe5\xbd\xd5') 22 | 23 | UPLOADED_PHOTOS_DEST = '/tmp/photolog' 24 | 25 | ADMIN_USERNAME = 'admin' 26 | ADMIN_PASSWORD = 'flaskftw' 27 | 28 | COUCHDB_SERVER = 'http://localhost:5984/' 29 | COUCHDB_DATABASE = 'flask-photolog' 30 | 31 | 32 | # application 33 | 34 | app = Flask(__name__) 35 | app.config.from_object(__name__) 36 | app.config.from_envvar('PHOTOLOG_SETTINGS', silent=True) 37 | 38 | 39 | # uploads 40 | 41 | uploaded_photos = UploadSet('photos', IMAGES) 42 | configure_uploads(app, uploaded_photos) 43 | 44 | 45 | # documents 46 | 47 | manager = CouchDBManager() 48 | 49 | def unique_id(): 50 | return hex(uuid.uuid4().time)[2:-1] 51 | 52 | 53 | class Post(Document): 54 | doc_type = 'post' 55 | title = TextField() 56 | filename = TextField() 57 | caption = TextField() 58 | published = DateTimeField(default=datetime.datetime.utcnow) 59 | 60 | @property 61 | def imgsrc(self): 62 | return uploaded_photos.url(self.filename) 63 | 64 | all = ViewField('photolog', '''\ 65 | function (doc) { 66 | if (doc.doc_type == 'post') 67 | emit(doc.published, doc); 68 | }''', descending=True) 69 | 70 | 71 | manager.add_document(Post) 72 | manager.setup(app) 73 | 74 | 75 | # utils 76 | 77 | def to_index(): 78 | return redirect(url_for('index')) 79 | 80 | 81 | @app.before_request 82 | def login_handle(): 83 | g.logged_in = bool(session.get('logged_in')) 84 | 85 | 86 | # views 87 | 88 | @app.route('/') 89 | def index(): 90 | posts = Post.all() 91 | return render_template('index.html', posts=posts) 92 | 93 | 94 | @app.route('/new', methods=['GET', 'POST']) 95 | def new(): 96 | if request.method == 'POST': 97 | photo = request.files.get('photo') 98 | title = request.form.get('title') 99 | caption = request.form.get('caption') 100 | if not (photo and title and caption): 101 | flash("You must fill in all the fields") 102 | else: 103 | try: 104 | filename = uploaded_photos.save(photo) 105 | except UploadNotAllowed: 106 | flash("The upload was not allowed") 107 | else: 108 | post = Post(title=title, caption=caption, filename=filename) 109 | post.id = unique_id() 110 | post.store() 111 | flash("Post successful") 112 | return to_index() 113 | return render_template('new.html') 114 | 115 | 116 | @app.route('/login', methods=['GET', 'POST']) 117 | def login(): 118 | if session.get('logged_in'): 119 | flash("You are already logged in") 120 | return to_index() 121 | if request.method == 'POST': 122 | username = request.form.get('username') 123 | password = request.form.get('password') 124 | if (username == app.config['ADMIN_USERNAME'] and 125 | password == app.config['ADMIN_PASSWORD']): 126 | session['logged_in'] = True 127 | flash("Successfully logged in") 128 | return to_index() 129 | else: 130 | flash("Those credentials were incorrect") 131 | return render_template('login.html') 132 | 133 | 134 | @app.route('/logout') 135 | def logout(): 136 | if session.get('logged_in'): 137 | session['logged_in'] = False 138 | flash("Successfully logged out") 139 | else: 140 | flash("You weren't logged in to begin with") 141 | return to_index() 142 | 143 | 144 | if __name__ == '__main__': 145 | app.run(debug=True) 146 | -------------------------------------------------------------------------------- /example/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | color: white; 4 | font-family: sans-serif; 5 | } 6 | 7 | h1#title { 8 | text-align: center; 9 | font-size: 2em; 10 | } 11 | 12 | div#nav { 13 | text-align: right; 14 | width: 600px; 15 | margin: auto; 16 | font-size: smaller; 17 | } 18 | 19 | div#body { 20 | width: 600px; 21 | margin: 10px auto; 22 | border: 2px solid #888; 23 | padding: 10px; 24 | background-color: white; 25 | color: black; 26 | } 27 | 28 | div#footer { 29 | text-align: center; 30 | font-size: 0.9em; 31 | } 32 | 33 | div.flashes { 34 | margin-top: -10px; 35 | margin-left: -10px; 36 | margin-right: -10px; 37 | border-bottom: 2px solid #888; 38 | padding: 10px; 39 | background-color: #ddd; 40 | } 41 | 42 | .byline { 43 | font-style: italic; 44 | } 45 | 46 | a { 47 | color: #ccc; 48 | } 49 | 50 | div#body a { 51 | color: #333; 52 | } 53 | 54 | p.image img { 55 | max-width: 580px; 56 | } 57 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Index{% endblock title %} 4 | 5 | {% block body %} 6 | 7 | {% for post in posts %} 8 |

{{ post.title }}

9 | 10 | 11 | 12 |

13 | 14 |

15 | 16 | {% if post.caption -%} 17 |

{{ post.caption }}

18 | {% endif %} 19 | 20 | {% else %} 21 |

No posts yet.

22 | {% endfor %} 23 | 24 | {% endblock body %} 25 | -------------------------------------------------------------------------------- /example/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Photolog: {% block title %}Welcome{% endblock title %} 4 | 5 | 6 |

Photolog

7 | 8 | 17 | 18 |
19 | {% with flashes = get_flashed_messages() %} 20 | {% if flashes %} 21 |
22 |
    23 | {% for f in flashes %} 24 |
  • {{ f }}
  • 25 | {% endfor %} 26 |
27 |
28 | {% endif %} 29 | {% endwith %} 30 | 31 | {% block body %} 32 | {% endblock body %} 33 |
34 | 35 | 38 | -------------------------------------------------------------------------------- /example/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Login{% endblock title %} 4 | 5 | {% block body %} 6 | 7 |

Login

8 | 9 |
10 | 11 |
12 |
Username 13 |
14 | 15 |
Password 16 |
17 |
18 | 19 |

20 | 21 |

22 | 23 | {% endblock body %} 24 | -------------------------------------------------------------------------------- /example/templates/new.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Index{% endblock title %} 4 | 5 | {% block body %} 6 | 7 |

New Post

8 | 9 |
10 | 11 |
12 | 13 |
Title 14 |
15 | 16 |
File 17 |
18 | 19 |
Caption 20 |
21 | 22 |
23 | 24 |

25 | 26 |

27 | 28 | {% endblock body %} 29 | -------------------------------------------------------------------------------- /flask_uploads.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flaskext.uploads 4 | ================ 5 | This module provides upload support for Flask. The basic pattern is to set up 6 | an `UploadSet` object and upload your files to it. 7 | 8 | :copyright: 2010 Matthew "LeafStorm" Frazier 9 | :license: MIT/X11, see LICENSE for details 10 | """ 11 | 12 | import sys 13 | 14 | PY3 = sys.version_info[0] == 3 15 | 16 | if PY3: 17 | string_types = str, 18 | else: 19 | string_types = basestring, 20 | 21 | import os.path 22 | import posixpath 23 | 24 | from flask import current_app, send_from_directory, abort, url_for 25 | from itertools import chain 26 | from werkzeug.datastructures import FileStorage 27 | from werkzeug.utils import secure_filename 28 | 29 | from flask import Blueprint 30 | 31 | # Extension presets 32 | 33 | #: This just contains plain text files (.txt). 34 | TEXT = ('txt',) 35 | 36 | #: This contains various office document formats (.rtf, .odf, .ods, .gnumeric, 37 | #: .abw, .doc, .docx, .xls, .xlsx and .pdf). Note that the macro-enabled versions 38 | #: of Microsoft Office 2007 files are not included. 39 | DOCUMENTS = tuple('rtf odf ods gnumeric abw doc docx xls xlsx pdf'.split()) 40 | 41 | #: This contains basic image types that are viewable from most browsers (.jpg, 42 | #: .jpe, .jpeg, .png, .gif, .svg, .bmp and .webp). 43 | IMAGES = tuple('jpg jpe jpeg png gif svg bmp webp'.split()) 44 | 45 | #: This contains audio file types (.wav, .mp3, .aac, .ogg, .oga, and .flac). 46 | AUDIO = tuple('wav mp3 aac ogg oga flac'.split()) 47 | 48 | #: This is for structured data files (.csv, .ini, .json, .plist, .xml, .yaml, 49 | #: and .yml). 50 | DATA = tuple('csv ini json plist xml yaml yml'.split()) 51 | 52 | #: This contains various types of scripts (.js, .php, .pl, .py .rb, and .sh). 53 | #: If your Web server has PHP installed and set to auto-run, you might want to 54 | #: add ``php`` to the DENY setting. 55 | SCRIPTS = tuple('js php pl py rb sh'.split()) 56 | 57 | #: This contains archive and compression formats (.gz, .bz2, .zip, .tar, 58 | #: .tgz, .txz, and .7z). 59 | ARCHIVES = tuple('gz bz2 zip tar tgz txz 7z'.split()) 60 | 61 | #: This contains nonexecutable source files - those which need to be 62 | #: compiled or assembled to binaries to be used. They are generally safe to 63 | #: accept, as without an existing RCE vulnerability, they cannot be compiled, 64 | #: assembled, linked, or executed. Supports C, C++, Ada, Rust, Go (Golang), 65 | #: FORTRAN, D, Java, C Sharp, F Sharp (compiled only), COBOL, Haskell, and 66 | #: assembly. 67 | SOURCE = tuple(('c cpp c++ h hpp h++ cxx hxx hdl ' # C/C++ 68 | + 'ada ' # Ada 69 | + 'rs ' # Rust 70 | + 'go ' # Go 71 | + 'f for f90 f95 f03 ' # FORTRAN 72 | + 'd dd di ' # D 73 | + 'java ' # Java 74 | + 'hs ' # Haskell 75 | + 'cs ' # C Sharp 76 | + 'fs ' # F Sharp compiled source (NOT .fsx, which is interactive-ready) 77 | + 'cbl cob ' # COBOL 78 | + 'asm s ' # Assembly 79 | ).split()) 80 | 81 | #: This contains shared libraries and executable files (.so, .exe and .dll). 82 | #: Most of the time, you will not want to allow this - it's better suited for 83 | #: use with `AllExcept`. 84 | EXECUTABLES = tuple('so exe dll'.split()) 85 | 86 | #: The default allowed extensions - `TEXT`, `DOCUMENTS`, `DATA`, and `IMAGES`. 87 | DEFAULTS = TEXT + DOCUMENTS + IMAGES + DATA 88 | 89 | 90 | class UploadNotAllowed(Exception): 91 | """ 92 | This exception is raised if the upload was not allowed. You should catch 93 | it in your view code and display an appropriate message to the user. 94 | """ 95 | 96 | 97 | def tuple_from(*iters): 98 | return tuple(itertools.chain(*iters)) 99 | 100 | 101 | def extension(filename): 102 | ext = os.path.splitext(filename)[1] 103 | if ext == '': 104 | # add non-ascii filename support 105 | ext = os.path.splitext(filename)[0] 106 | if ext.startswith('.'): 107 | # os.path.splitext retains . separator 108 | ext = ext[1:] 109 | return ext 110 | 111 | 112 | def lowercase_ext(filename): 113 | """ 114 | This is a helper used by UploadSet.save to provide lowercase extensions for 115 | all processed files, to compare with configured extensions in the same 116 | case. 117 | 118 | .. versionchanged:: 0.1.4 119 | Filenames without extensions are no longer lowercased, only the 120 | extension is returned in lowercase, if an extension exists. 121 | 122 | :param filename: The filename to ensure has a lowercase extension. 123 | """ 124 | if '.' in filename: 125 | main, ext = os.path.splitext(filename) 126 | return main + ext.lower() 127 | # For consistency with os.path.splitext, 128 | # do not treat a filename without an extension as an extension. 129 | # That is, do not return filename.lower(). 130 | return filename 131 | 132 | 133 | def addslash(url): 134 | if url.endswith('/'): 135 | return url 136 | return url + '/' 137 | 138 | 139 | def patch_request_class(app, size=64 * 1024 * 1024): 140 | """ 141 | By default, Flask will accept uploads to an arbitrary size. While Werkzeug 142 | switches uploads from memory to a temporary file when they hit 500 KiB, 143 | it's still possible for someone to overload your disk space with a 144 | gigantic file. 145 | 146 | This patches the app's request class's 147 | `~werkzeug.BaseRequest.max_content_length` attribute so that any upload 148 | larger than the given size is rejected with an HTTP error. 149 | 150 | .. note:: 151 | 152 | In Flask 0.6, you can do this by setting the `MAX_CONTENT_LENGTH` 153 | setting, without patching the request class. To emulate this behavior, 154 | you can pass `None` as the size (you must pass it explicitly). That is 155 | the best way to call this function, as it won't break the Flask 0.6 156 | functionality if it exists. 157 | 158 | .. versionchanged:: 0.1.1 159 | 160 | :param app: The app to patch the request class of. 161 | :param size: The maximum size to accept, in bytes. The default is 64 MiB. 162 | If it is `None`, the app's `MAX_CONTENT_LENGTH` configuration 163 | setting will be used to patch. 164 | """ 165 | if size is None: 166 | if isinstance(app.request_class.__dict__['max_content_length'], 167 | property): 168 | return 169 | size = app.config.get('MAX_CONTENT_LENGTH') 170 | reqclass = app.request_class 171 | patched = type(reqclass.__name__, (reqclass,), 172 | {'max_content_length': size}) 173 | app.request_class = patched 174 | 175 | 176 | def config_for_set(uset, app, defaults=None): 177 | """ 178 | This is a helper function for `configure_uploads` that extracts the 179 | configuration for a single set. 180 | 181 | :param uset: The upload set. 182 | :param app: The app to load the configuration from. 183 | :param defaults: A dict with keys `url` and `dest` from the 184 | `UPLOADS_DEFAULT_DEST` and `DEFAULT_UPLOADS_URL` 185 | settings. 186 | """ 187 | config = app.config 188 | prefix = 'UPLOADED_%s_' % uset.name.upper() 189 | using_defaults = False 190 | if defaults is None: 191 | defaults = dict(dest=None, url=None) 192 | 193 | allow_extns = tuple(config.get(prefix + 'ALLOW', ())) 194 | deny_extns = tuple(config.get(prefix + 'DENY', ())) 195 | destination = config.get(prefix + 'DEST') 196 | base_url = config.get(prefix + 'URL') 197 | 198 | if destination is None: 199 | # the upload set's destination wasn't given 200 | if uset.default_dest: 201 | # use the "default_dest" callable 202 | destination = uset.default_dest(app) 203 | if destination is None: # still 204 | # use the default dest from the config 205 | if defaults['dest'] is not None: 206 | using_defaults = True 207 | destination = os.path.join(defaults['dest'], uset.name) 208 | else: 209 | raise RuntimeError("no destination for set %s" % uset.name) 210 | 211 | if base_url is None and using_defaults and defaults['url']: 212 | base_url = addslash(defaults['url']) + uset.name + '/' 213 | 214 | return UploadConfiguration(destination, base_url, allow_extns, deny_extns) 215 | 216 | 217 | def configure_uploads(app, upload_sets): 218 | """ 219 | Call this after the app has been configured. It will go through all the 220 | upload sets, get their configuration, and store the configuration on the 221 | app. It will also register the uploads module if it hasn't been set. This 222 | can be called multiple times with different upload sets. 223 | 224 | .. versionchanged:: 0.1.3 225 | The uploads module/blueprint will only be registered if it is needed 226 | to serve the upload sets. 227 | 228 | :param app: The `~flask.Flask` instance to get the configuration from. 229 | :param upload_sets: The `UploadSet` instances to configure. 230 | """ 231 | if isinstance(upload_sets, UploadSet): 232 | upload_sets = (upload_sets,) 233 | 234 | if not hasattr(app, 'upload_set_config'): 235 | app.upload_set_config = {} 236 | set_config = app.upload_set_config 237 | defaults = dict(dest=app.config.get('UPLOADS_DEFAULT_DEST'), 238 | url=app.config.get('UPLOADS_DEFAULT_URL')) 239 | 240 | for uset in upload_sets: 241 | config = config_for_set(uset, app, defaults) 242 | set_config[uset.name] = config 243 | 244 | should_serve = any(s.base_url is None for s in set_config.values()) 245 | if '_uploads' not in app.blueprints and should_serve: 246 | app.register_blueprint(uploads_mod) 247 | 248 | 249 | class All(object): 250 | """ 251 | This type can be used to allow all extensions. There is a predefined 252 | instance named `ALL`. 253 | """ 254 | def __contains__(self, item): 255 | return True 256 | 257 | 258 | #: This "contains" all items. You can use it to allow all extensions to be 259 | #: uploaded. 260 | ALL = All() 261 | 262 | 263 | class AllExcept(object): 264 | """ 265 | This can be used to allow all file types except certain ones. For example, 266 | to ban .exe and .iso files, pass:: 267 | 268 | AllExcept(('exe', 'iso')) 269 | 270 | to the `UploadSet` constructor as `extensions`. You can use any container, 271 | for example:: 272 | 273 | AllExcept(SCRIPTS + EXECUTABLES) 274 | """ 275 | def __init__(self, items): 276 | self.items = items 277 | 278 | def __contains__(self, item): 279 | return item not in self.items 280 | 281 | 282 | class UploadConfiguration(object): 283 | """ 284 | This holds the configuration for a single `UploadSet`. The constructor's 285 | arguments are also the attributes. 286 | 287 | :param destination: The directory to save files to. 288 | :param base_url: The URL (ending with a /) that files can be downloaded 289 | from. If this is `None`, Flask-Uploads will serve the 290 | files itself. 291 | :param allow: A list of extensions to allow, even if they're not in the 292 | `UploadSet` extensions list. 293 | :param deny: A list of extensions to deny, even if they are in the 294 | `UploadSet` extensions list. 295 | """ 296 | def __init__(self, destination, base_url=None, allow=(), deny=()): 297 | self.destination = destination 298 | self.base_url = base_url 299 | self.allow = allow 300 | self.deny = deny 301 | 302 | @property 303 | def tuple(self): 304 | return (self.destination, self.base_url, self.allow, self.deny) 305 | 306 | def __eq__(self, other): 307 | return self.tuple == other.tuple 308 | 309 | 310 | class UploadSet(object): 311 | """ 312 | This represents a single set of uploaded files. Each upload set is 313 | independent of the others. This can be reused across multiple application 314 | instances, as all configuration is stored on the application object itself 315 | and found with `flask.current_app`. 316 | 317 | :param name: The name of this upload set. It defaults to ``files``, but 318 | you can pick any alphanumeric name you want. (For simplicity, 319 | it's best to use a plural noun.) 320 | :param extensions: The extensions to allow uploading in this set. The 321 | easiest way to do this is to add together the extension 322 | presets (for example, ``TEXT + DOCUMENTS + IMAGES``). 323 | It can be overridden by the configuration with the 324 | `UPLOADED_X_ALLOW` and `UPLOADED_X_DENY` configuration 325 | parameters. The default is `DEFAULTS`. 326 | :param default_dest: If given, this should be a callable. If you call it 327 | with the app, it should return the default upload 328 | destination path for that app. 329 | """ 330 | def __init__(self, name='files', extensions=DEFAULTS, default_dest=None): 331 | if not name.isalnum(): 332 | raise ValueError("Name must be alphanumeric (no underscores)") 333 | self.name = name 334 | self.extensions = extensions 335 | self._config = None 336 | self.default_dest = default_dest 337 | 338 | @property 339 | def config(self): 340 | """ 341 | This gets the current configuration. By default, it looks up the 342 | current application and gets the configuration from there. But if you 343 | don't want to go to the full effort of setting an application, or it's 344 | otherwise outside of a request context, set the `_config` attribute to 345 | an `UploadConfiguration` instance, then set it back to `None` when 346 | you're done. 347 | """ 348 | if self._config is not None: 349 | return self._config 350 | try: 351 | return current_app.upload_set_config[self.name] 352 | except AttributeError: 353 | raise RuntimeError("cannot access configuration outside request") 354 | 355 | def url(self, filename): 356 | """ 357 | This function gets the URL a file uploaded to this set would be 358 | accessed at. It doesn't check whether said file exists. 359 | 360 | :param filename: The filename to return the URL for. 361 | """ 362 | base = self.config.base_url 363 | if base is None: 364 | return url_for('_uploads.uploaded_file', setname=self.name, 365 | filename=filename, _external=True) 366 | else: 367 | return base + filename 368 | 369 | def path(self, filename, folder=None): 370 | """ 371 | This returns the absolute path of a file uploaded to this set. It 372 | doesn't actually check whether said file exists. 373 | 374 | :param filename: The filename to return the path for. 375 | :param folder: The subfolder within the upload set previously used 376 | to save to. 377 | """ 378 | if folder is not None: 379 | target_folder = os.path.join(self.config.destination, folder) 380 | else: 381 | target_folder = self.config.destination 382 | return os.path.join(target_folder, filename) 383 | 384 | def file_allowed(self, storage, basename): 385 | """ 386 | This tells whether a file is allowed. It should return `True` if the 387 | given `werkzeug.FileStorage` object can be saved with the given 388 | basename, and `False` if it can't. The default implementation just 389 | checks the extension, so you can override this if you want. 390 | 391 | :param storage: The `werkzeug.FileStorage` to check. 392 | :param basename: The basename it will be saved under. 393 | """ 394 | return self.extension_allowed(extension(basename)) 395 | 396 | def extension_allowed(self, ext): 397 | """ 398 | This determines whether a specific extension is allowed. It is called 399 | by `file_allowed`, so if you override that but still want to check 400 | extensions, call back into this. 401 | 402 | :param ext: The extension to check, without the dot. 403 | """ 404 | return ((ext in self.config.allow) or 405 | (ext in self.extensions and ext not in self.config.deny)) 406 | 407 | def get_basename(self, filename): 408 | return lowercase_ext(secure_filename(filename)) 409 | 410 | def save(self, storage, folder=None, name=None): 411 | """ 412 | This saves a `werkzeug.FileStorage` into this upload set. If the 413 | upload is not allowed, an `UploadNotAllowed` error will be raised. 414 | Otherwise, the file will be saved and its name (including the folder) 415 | will be returned. 416 | 417 | :param storage: The uploaded file to save. 418 | :param folder: The subfolder within the upload set to save to. 419 | :param name: The name to save the file as. If it ends with a dot, the 420 | file's extension will be appended to the end. (If you 421 | are using `name`, you can include the folder in the 422 | `name` instead of explicitly using `folder`, i.e. 423 | ``uset.save(file, name="someguy/photo_123.")`` 424 | """ 425 | if not isinstance(storage, FileStorage): 426 | raise TypeError("storage must be a werkzeug.FileStorage") 427 | 428 | if folder is None and name is not None and "/" in name: 429 | folder, name = os.path.split(name) 430 | 431 | basename = self.get_basename(storage.filename) 432 | 433 | if not self.file_allowed(storage, basename): 434 | raise UploadNotAllowed() 435 | 436 | if name: 437 | if name.endswith('.'): 438 | basename = name + extension(basename) 439 | else: 440 | basename = name 441 | 442 | 443 | 444 | if folder: 445 | target_folder = os.path.join(self.config.destination, folder) 446 | else: 447 | target_folder = self.config.destination 448 | if not os.path.exists(target_folder): 449 | os.makedirs(target_folder) 450 | if os.path.exists(os.path.join(target_folder, basename)): 451 | basename = self.resolve_conflict(target_folder, basename) 452 | 453 | target = os.path.join(target_folder, basename) 454 | storage.save(target) 455 | if folder: 456 | return posixpath.join(folder, basename) 457 | else: 458 | return basename 459 | 460 | def resolve_conflict(self, target_folder, basename): 461 | """ 462 | If a file with the selected name already exists in the target folder, 463 | this method is called to resolve the conflict. It should return a new 464 | basename for the file. 465 | 466 | The default implementation splits the name and extension and adds a 467 | suffix to the name consisting of an underscore and a number, and tries 468 | that until it finds one that doesn't exist. 469 | 470 | :param target_folder: The absolute path to the target. 471 | :param basename: The file's original basename. 472 | """ 473 | name, ext = os.path.splitext(basename) 474 | count = 0 475 | while True: 476 | count = count + 1 477 | newname = '%s_%d%s' % (name, count, ext) 478 | if not os.path.exists(os.path.join(target_folder, newname)): 479 | return newname 480 | 481 | 482 | uploads_mod = Blueprint('_uploads', __name__, url_prefix='/_uploads') 483 | 484 | 485 | @uploads_mod.route('//') 486 | def uploaded_file(setname, filename): 487 | config = current_app.upload_set_config.get(setname) 488 | if config is None: 489 | abort(404) 490 | return send_from_directory(config.destination, filename) 491 | 492 | 493 | class TestingFileStorage(FileStorage): 494 | """ 495 | This is a helper for testing upload behavior in your application. You 496 | can manually create it, and its save method is overloaded to set `saved` 497 | to the name of the file it was saved to. All of these parameters are 498 | optional, so only bother setting the ones relevant to your application. 499 | 500 | :param stream: A stream. The default is an empty stream. 501 | :param filename: The filename uploaded from the client. The default is the 502 | stream's name. 503 | :param name: The name of the form field it was loaded from. The default is 504 | `None`. 505 | :param content_type: The content type it was uploaded as. The default is 506 | ``application/octet-stream``. 507 | :param content_length: How long it is. The default is -1. 508 | :param headers: Multipart headers as a `werkzeug.Headers`. The default is 509 | `None`. 510 | """ 511 | def __init__(self, stream=None, filename=None, name=None, 512 | content_type='application/octet-stream', content_length=-1, 513 | headers=None): 514 | FileStorage.__init__(self, stream, filename, name=name, 515 | content_type=content_type, content_length=content_length, 516 | headers=None) 517 | self.saved = None 518 | 519 | def save(self, dst, buffer_size=16384): 520 | """ 521 | This marks the file as saved by setting the `saved` attribute to the 522 | name of the file it was saved to. 523 | 524 | :param dst: The file to save to. 525 | :param buffer_size: Ignored. 526 | """ 527 | if isinstance(dst, string_types): 528 | self.saved = dst 529 | else: 530 | self.saved = dst.name 531 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Uploads 3 | ------------- 4 | Flask-Uploads provides flexible upload handling for Flask applications. It 5 | lets you divide your uploads into sets that the application user can publish 6 | separately. 7 | 8 | Links 9 | ````` 10 | * `documentation `_ 11 | * `development version 12 | `_ 13 | 14 | 15 | """ 16 | from setuptools import setup 17 | 18 | 19 | setup( 20 | name='Flask-Uploads', 21 | version='0.2.1', 22 | url='https://github.com/maxcountryman/flask-uploads', 23 | license='MIT', 24 | author='Matthew "LeafStorm" Frazier', 25 | author_email='leafstormrush@gmail.com', 26 | description='Flexible and efficient upload handling for Flask', 27 | long_description=__doc__, 28 | py_modules=['flask_uploads'], 29 | zip_safe=False, 30 | platforms='any', 31 | install_requires=[ 32 | 'Flask>=0.8.0' 33 | ], 34 | tests_require='nose', 35 | test_suite='nose.collector', 36 | classifiers=[ 37 | 'Development Status :: 4 - Beta', 38 | 'Environment :: Web Environment', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 44 | 'Topic :: Software Development :: Libraries :: Python Modules' 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests/test-uploads.py 4 | ===================== 5 | This is a Nose testing file for Flask-Uploads. 6 | 7 | NOTE: While Flask-Uploads will probably work on Windows, all the filenames in 8 | this testing file are in the POSIX style. So, no guarantees that the tests 9 | will pass. 10 | 11 | :copyright: 2010 Matthew "LeafStorm" Frazier 12 | :license: MIT/X11, see LICENSE for details 13 | """ 14 | from __future__ import with_statement 15 | import os.path 16 | from flask import Flask, url_for 17 | from flask_uploads import (UploadSet, UploadConfiguration, extension, 18 | lowercase_ext, TestingFileStorage, patch_request_class, configure_uploads, 19 | addslash, ALL, AllExcept) 20 | 21 | 22 | class TestMiscellaneous(object): 23 | def test_tfs(self): 24 | tfs = TestingFileStorage(filename='foo.bar') 25 | assert tfs.filename == 'foo.bar' 26 | assert tfs.name is None 27 | assert tfs.saved is None 28 | tfs.save('foo_bar.txt') 29 | assert tfs.saved == 'foo_bar.txt' 30 | 31 | def test_extension(self): 32 | assert extension('foo.txt') == 'txt' 33 | assert extension('foo') == '' 34 | assert extension('archive.tar.gz') == 'gz' 35 | assert extension('audio.m4a') == 'm4a' 36 | 37 | def test_lowercase_ext(self): 38 | assert lowercase_ext('foo.txt') == 'foo.txt' 39 | assert lowercase_ext('FOO.TXT') == 'FOO.txt' 40 | assert lowercase_ext('foo') == 'foo' 41 | assert lowercase_ext('FOO') == 'FOO' 42 | assert lowercase_ext('archive.tar.gz') == 'archive.tar.gz' 43 | assert lowercase_ext('ARCHIVE.TAR.GZ') == 'ARCHIVE.TAR.gz' 44 | assert lowercase_ext('audio.m4a') == 'audio.m4a' 45 | assert lowercase_ext('AUDIO.M4A') == 'AUDIO.m4a' 46 | 47 | def test_addslash(self): 48 | assert (addslash('http://localhost:4000') == 49 | 'http://localhost:4000/') 50 | assert (addslash('http://localhost/uploads') == 51 | 'http://localhost/uploads/') 52 | assert (addslash('http://localhost:4000/') == 53 | 'http://localhost:4000/') 54 | assert (addslash('http://localhost/uploads/') == 55 | 'http://localhost/uploads/') 56 | 57 | def test_custom_iterables(self): 58 | assert 'txt' in ALL 59 | assert 'exe' in ALL 60 | ax = AllExcept(['exe']) 61 | assert 'txt' in ax 62 | assert 'exe' not in ax 63 | 64 | 65 | Config = UploadConfiguration 66 | 67 | class TestConfiguration(object): 68 | def setup(self): 69 | self.app = Flask(__name__) 70 | 71 | def teardown(self): 72 | del self.app 73 | 74 | def configure(self, *sets, **options): 75 | self.app.config.update(options) 76 | configure_uploads(self.app, sets) 77 | return self.app.upload_set_config 78 | 79 | def test_manual(self): 80 | f, p = UploadSet('files'), UploadSet('photos') 81 | setconfig = self.configure(f, p, 82 | UPLOADED_FILES_DEST = '/var/files', 83 | UPLOADED_FILES_URL = 'http://localhost:6001/', 84 | UPLOADED_PHOTOS_DEST = '/mnt/photos', 85 | UPLOADED_PHOTOS_URL = 'http://localhost:6002/' 86 | ) 87 | fconf, pconf = setconfig['files'], setconfig['photos'] 88 | assert fconf == Config('/var/files', 'http://localhost:6001/') 89 | assert pconf == Config('/mnt/photos', 'http://localhost:6002/') 90 | 91 | def test_selfserve(self): 92 | f, p = UploadSet('files'), UploadSet('photos') 93 | setconfig = self.configure(f, p, 94 | UPLOADED_FILES_DEST = '/var/files', 95 | UPLOADED_PHOTOS_DEST = '/mnt/photos' 96 | ) 97 | fconf, pconf = setconfig['files'], setconfig['photos'] 98 | assert fconf == Config('/var/files', None) 99 | assert pconf == Config('/mnt/photos', None) 100 | 101 | def test_defaults(self): 102 | f, p = UploadSet('files'), UploadSet('photos') 103 | setconfig = self.configure(f, p, 104 | UPLOADS_DEFAULT_DEST = '/var/uploads', 105 | UPLOADS_DEFAULT_URL = 'http://localhost:6000/' 106 | ) 107 | fconf, pconf = setconfig['files'], setconfig['photos'] 108 | assert fconf == Config('/var/uploads/files', 109 | 'http://localhost:6000/files/') 110 | assert pconf == Config('/var/uploads/photos', 111 | 'http://localhost:6000/photos/') 112 | 113 | def test_default_selfserve(self): 114 | f, p = UploadSet('files'), UploadSet('photos') 115 | setconfig = self.configure(f, p, 116 | UPLOADS_DEFAULT_DEST = '/var/uploads' 117 | ) 118 | fconf, pconf = setconfig['files'], setconfig['photos'] 119 | assert fconf == Config('/var/uploads/files', None) 120 | assert pconf == Config('/var/uploads/photos', None) 121 | 122 | def test_mixed_defaults(self): 123 | f, p = UploadSet('files'), UploadSet('photos') 124 | setconfig = self.configure(f, p, 125 | UPLOADS_DEFAULT_DEST = '/var/uploads', 126 | UPLOADS_DEFAULT_URL = 'http://localhost:6001/', 127 | UPLOADED_PHOTOS_DEST = '/mnt/photos', 128 | UPLOADED_PHOTOS_URL = 'http://localhost:6002/' 129 | ) 130 | fconf, pconf = setconfig['files'], setconfig['photos'] 131 | assert fconf == Config('/var/uploads/files', 132 | 'http://localhost:6001/files/') 133 | assert pconf == Config('/mnt/photos', 'http://localhost:6002/') 134 | 135 | def test_defaultdest_callable(self): 136 | f = UploadSet('files', default_dest=lambda app: os.path.join( 137 | app.config['INSTANCE'], 'files' 138 | )) 139 | p = UploadSet('photos') 140 | setconfig = self.configure(f, p, 141 | INSTANCE = '/home/me/webapps/thisapp', 142 | UPLOADED_PHOTOS_DEST = '/mnt/photos', 143 | UPLOADED_PHOTOS_URL = 'http://localhost:6002/' 144 | ) 145 | fconf, pconf = setconfig['files'], setconfig['photos'] 146 | assert fconf == Config('/home/me/webapps/thisapp/files', None) 147 | assert pconf == Config('/mnt/photos', 'http://localhost:6002/') 148 | 149 | 150 | class TestPreconditions(object): 151 | def test_filenames(self): 152 | uset = UploadSet('files') 153 | uset._config = Config('/uploads') 154 | namepairs = ( 155 | ('foo.txt', True), 156 | ('boat.jpg', True), 157 | ('warez.exe', False) 158 | ) 159 | for name, result in namepairs: 160 | tfs = TestingFileStorage(filename=name) 161 | assert uset.file_allowed(tfs, name) is result 162 | 163 | def test_non_ascii_filename(self): 164 | uset = UploadSet('files') 165 | uset._config = Config('/uploads') 166 | tfs = TestingFileStorage(filename=u'天安门.jpg') 167 | res = uset.save(tfs) 168 | assert res == 'jpg' 169 | res = uset.save(tfs, name='secret.') 170 | assert res == 'secret.jpg' 171 | 172 | def test_default_extensions(self): 173 | uset = UploadSet('files') 174 | uset._config = Config('/uploads') 175 | extpairs = (('txt', True), ('jpg', True), ('exe', False)) 176 | for ext, result in extpairs: 177 | assert uset.extension_allowed(ext) is result 178 | 179 | 180 | class TestSaving(object): 181 | def setup(self): 182 | self.old_makedirs = os.makedirs 183 | os.makedirs = lambda v: None 184 | 185 | def teardown(self): 186 | os.makedirs = self.old_makedirs 187 | del self.old_makedirs 188 | 189 | def test_saved(self): 190 | uset = UploadSet('files') 191 | uset._config = Config('/uploads') 192 | tfs = TestingFileStorage(filename='foo.txt') 193 | res = uset.save(tfs) 194 | assert res == 'foo.txt' 195 | assert tfs.saved == '/uploads/foo.txt' 196 | 197 | def test_save_folders(self): 198 | uset = UploadSet('files') 199 | uset._config = Config('/uploads') 200 | tfs = TestingFileStorage(filename='foo.txt') 201 | res = uset.save(tfs, folder='someguy') 202 | assert res == 'someguy/foo.txt' 203 | assert tfs.saved == '/uploads/someguy/foo.txt' 204 | 205 | def test_save_named(self): 206 | uset = UploadSet('files') 207 | uset._config = Config('/uploads') 208 | tfs = TestingFileStorage(filename='foo.txt') 209 | res = uset.save(tfs, name='file_123.txt') 210 | assert res == 'file_123.txt' 211 | assert tfs.saved == '/uploads/file_123.txt' 212 | 213 | def test_save_namedext(self): 214 | uset = UploadSet('files') 215 | uset._config = Config('/uploads') 216 | tfs = TestingFileStorage(filename='boat.jpg') 217 | res = uset.save(tfs, name='photo_123.') 218 | assert res == 'photo_123.jpg' 219 | assert tfs.saved == '/uploads/photo_123.jpg' 220 | 221 | def test_folder_namedext(self): 222 | uset = UploadSet('files') 223 | uset._config = Config('/uploads') 224 | tfs = TestingFileStorage(filename='boat.jpg') 225 | res = uset.save(tfs, folder='someguy', name='photo_123.') 226 | assert res == 'someguy/photo_123.jpg' 227 | assert tfs.saved == '/uploads/someguy/photo_123.jpg' 228 | 229 | def test_implicit_folder(self): 230 | uset = UploadSet('files') 231 | uset._config = Config('/uploads') 232 | tfs = TestingFileStorage(filename='boat.jpg') 233 | res = uset.save(tfs, name='someguy/photo_123.') 234 | assert res == 'someguy/photo_123.jpg' 235 | assert tfs.saved == '/uploads/someguy/photo_123.jpg' 236 | 237 | def test_secured_filename(self): 238 | uset = UploadSet('files', ALL) 239 | uset._config = Config('/uploads') 240 | tfs1 = TestingFileStorage(filename='/etc/passwd') 241 | tfs2 = TestingFileStorage(filename='../../myapp.wsgi') 242 | res1 = uset.save(tfs1) 243 | assert res1 == 'etc_passwd' 244 | assert tfs1.saved == '/uploads/etc_passwd' 245 | res2 = uset.save(tfs2) 246 | assert res2 == 'myapp.wsgi' 247 | assert tfs2.saved == '/uploads/myapp.wsgi' 248 | 249 | 250 | class TestConflictResolution(object): 251 | def setup(self): 252 | self.extant_files = [] 253 | self.old_exists = os.path.exists 254 | os.path.exists = self.exists 255 | self.old_makedirs = os.makedirs 256 | os.makedirs = lambda v: None 257 | 258 | def teardown(self): 259 | os.path.exists = self.old_exists 260 | del self.extant_files, self.old_exists 261 | os.makedirs = self.old_makedirs 262 | del self.old_makedirs 263 | 264 | def extant(self, *files): 265 | self.extant_files.extend(files) 266 | 267 | def exists(self, fname): 268 | return fname in self.extant_files 269 | 270 | def test_self(self): 271 | assert not os.path.exists('/uploads/foo.txt') 272 | self.extant('/uploads/foo.txt') 273 | assert os.path.exists('/uploads/foo.txt') 274 | 275 | def test_conflict(self): 276 | uset = UploadSet('files') 277 | uset._config = Config('/uploads') 278 | tfs = TestingFileStorage(filename='foo.txt') 279 | self.extant('/uploads/foo.txt') 280 | res = uset.save(tfs) 281 | assert res == 'foo_1.txt' 282 | 283 | def test_multi_conflict(self): 284 | uset = UploadSet('files') 285 | uset._config = Config('/uploads') 286 | tfs = TestingFileStorage(filename='foo.txt') 287 | self.extant('/uploads/foo.txt', 288 | *('/uploads/foo_%d.txt' % n for n in range(1, 6))) 289 | res = uset.save(tfs) 290 | assert res == 'foo_6.txt' 291 | 292 | def test_conflict_without_extension(self): 293 | # Test case for issue #7. 294 | uset = UploadSet('files', extensions=('')) 295 | uset._config = Config('/uploads') 296 | tfs = TestingFileStorage(filename='foo') 297 | self.extant('/uploads/foo') 298 | res = uset.save(tfs) 299 | assert res == 'foo_1' 300 | 301 | 302 | class TestPathsAndURLs(object): 303 | def test_path(self): 304 | uset = UploadSet('files') 305 | uset._config = Config('/uploads') 306 | assert uset.path('foo.txt') == '/uploads/foo.txt' 307 | assert uset.path('someguy/foo.txt') == '/uploads/someguy/foo.txt' 308 | assert (uset.path('foo.txt', folder='someguy') == 309 | '/uploads/someguy/foo.txt') 310 | assert (uset.path('foo/bar.txt', folder='someguy') == 311 | '/uploads/someguy/foo/bar.txt') 312 | 313 | def test_url_generated(self): 314 | app = Flask(__name__) 315 | app.config.update( 316 | UPLOADED_FILES_DEST='/uploads' 317 | ) 318 | uset = UploadSet('files') 319 | configure_uploads(app, uset) 320 | with app.test_request_context(): 321 | url = uset.url('foo.txt') 322 | gen = url_for('_uploads.uploaded_file', setname='files', 323 | filename='foo.txt', _external=True) 324 | assert url == gen 325 | 326 | def test_url_based(self): 327 | app = Flask(__name__) 328 | app.config.update( 329 | UPLOADED_FILES_DEST='/uploads', 330 | UPLOADED_FILES_URL='http://localhost:5001/' 331 | ) 332 | uset = UploadSet('files') 333 | configure_uploads(app, uset) 334 | with app.test_request_context(): 335 | url = uset.url('foo.txt') 336 | assert url == 'http://localhost:5001/foo.txt' 337 | assert '_uploads' not in app.blueprints 338 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, pypy, py35, py36, py37, py38 3 | 4 | [testenv] 5 | deps = 6 | nose 7 | 8 | commands = nosetests [] 9 | --------------------------------------------------------------------------------