├── .gitignore ├── LICENSE ├── README.rst ├── flask_admin_s3_upload.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | flask-admin-s3-upload 2 | ===================== 3 | 4 | Field types for allowing file and image uploads to Amazon S3 (as well as default local storage) in Flask-Admin. 5 | 6 | 7 | Example 8 | ------- 9 | 10 | For a complete, working Flask app that demonstrates flask-admin-s3-upload in action, have a look at `flask-s3-save-example `_. 11 | 12 | 13 | Usage 14 | ----- 15 | 16 | Use with a Flask-Admin ModelView by overriding field types, and by passing in special arguments to those fields: 17 | 18 | .. code-block:: python 19 | 20 | from flask.ext.admin.contrib.sqla import ModelView 21 | 22 | class MyView(ModelView): 23 | form_overrides = dict( 24 | some_image=S3ImageUploadField, 25 | some_file=S3FileUploadField) 26 | 27 | form_args = dict( 28 | some_image=dict( 29 | base_path='/some/folder/static', 30 | relative_path='some_image/', 31 | url_relative_path='uploads/', 32 | namegen=your_namegen_func_here, 33 | storage_type_field='some_image_storage_type', 34 | bucket_name_field='some_image_storage_bucket_name', 35 | ), 36 | some_file=dict( 37 | base_path='/some/folder/static', 38 | relative_path='some_file/', 39 | namegen=your_namegen_func_here, 40 | allowed_extensions=('pdf', 'txt'), 41 | storage_type_field='some_file_storage_type', 42 | bucket_name_field='some_file_storage_bucket_name', 43 | )) 44 | 45 | def scaffold_form(self): 46 | # Note: assuming that we have Flask-S3 config values to pass 47 | # to fields below. Flask-S3 is not required, you can pass 48 | # values from elsewhere if you want. 49 | from flask import current_app as app 50 | 51 | form_class = super(MyView, self).scaffold_form() 52 | static_root_parent = '/some/folder' 53 | 54 | if app.config['USE_S3']: 55 | form_class.some_image.kwargs['storage_type'] = 's3' 56 | form_class.some_file.kwargs['storage_type'] = 's3' 57 | 58 | form_class.some_image.kwargs['bucket_name'] = app.config['S3_BUCKET_NAME'] 59 | form_class.some_image.kwargs['access_key_id'] = app.config['AWS_ACCESS_KEY_ID'] 60 | form_class.some_image.kwargs['access_key_secret'] = app.config['AWS_SECRET_ACCESS_KEY'] 61 | form_class.some_image.kwargs['static_root_parent'] = static_root_parent 62 | 63 | form_class.some_file.kwargs['bucket_name'] = app.config['S3_BUCKET_NAME'] 64 | form_class.some_file.kwargs['access_key_id'] = app.config['AWS_ACCESS_KEY_ID'] 65 | form_class.some_file.kwargs['access_key_secret'] = app.config['AWS_SECRET_ACCESS_KEY'] 66 | form_class.some_file.kwargs['static_root_parent'] = static_root_parent 67 | 68 | return form_class 69 | -------------------------------------------------------------------------------- /flask_admin_s3_upload.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.4' 2 | 3 | 4 | try: 5 | from PIL import Image, ImageOps 6 | except ImportError: 7 | Image = None 8 | ImageOps = None 9 | 10 | from io import BytesIO 11 | import os 12 | import os.path as op 13 | import re 14 | 15 | from boto.s3.connection import S3Connection 16 | from boto.exception import S3ResponseError 17 | from boto.s3.key import Key 18 | from werkzeug.datastructures import FileStorage 19 | 20 | from wtforms import ValidationError 21 | 22 | from flask_admin.form.upload import FileUploadField, ImageUploadInput, \ 23 | thumbgen_filename 24 | from flask_admin._compat import urljoin 25 | 26 | from url_for_s3 import url_for_s3 27 | 28 | 29 | class S3FileUploadField(FileUploadField): 30 | """ 31 | Inherits from flask-admin FileUploadField, to allow file uploading 32 | to Amazon S3 (as well as the default local storage). 33 | """ 34 | 35 | def __init__(self, label=None, validators=None, storage_type=None, 36 | bucket_name=None, access_key_id=None, 37 | access_key_secret=None, acl='public-read', 38 | storage_type_field=None, bucket_name_field=None, 39 | static_root_parent=None, **kwargs): 40 | super(S3FileUploadField, self).__init__(label, validators, **kwargs) 41 | 42 | if storage_type and (storage_type != 's3'): 43 | raise ValueError( 44 | 'Storage type "%s" is invalid, the only supported storage type' 45 | ' (apart from default local storage) is s3.' % storage_type 46 | ) 47 | 48 | self.storage_type = storage_type 49 | self.bucket_name = bucket_name 50 | self.access_key_id = access_key_id 51 | self.access_key_secret = access_key_secret 52 | self.acl = acl 53 | self.storage_type_field = storage_type_field 54 | self.bucket_name_field = bucket_name_field 55 | self.static_root_parent = static_root_parent 56 | 57 | def populate_obj(self, obj, name): 58 | field = getattr(obj, name, None) 59 | if field: 60 | # If field should be deleted, clean it up 61 | if self._should_delete: 62 | self._delete_file(field, obj) 63 | setattr(obj, name, '') 64 | 65 | if self.storage_type_field: 66 | setattr(obj, self.storage_type_field, '') 67 | if self.bucket_name_field: 68 | setattr(obj, self.bucket_name_field, '') 69 | 70 | return 71 | 72 | if (self.data and isinstance(self.data, FileStorage) 73 | and self.data.filename): 74 | if field: 75 | self._delete_file(field, obj) 76 | 77 | filename = self.generate_name(obj, self.data) 78 | temp_file = BytesIO() 79 | self.data.save(temp_file) 80 | filename = self._save_file(temp_file, filename) 81 | # update filename of FileStorage to our validated name 82 | self.data.filename = filename 83 | 84 | setattr(obj, name, filename) 85 | 86 | if self.storage_type == 's3': 87 | if self.storage_type_field: 88 | setattr(obj, self.storage_type_field, self.storage_type) 89 | if self.bucket_name_field: 90 | setattr(obj, self.bucket_name_field, self.bucket_name) 91 | else: 92 | if self.storage_type_field: 93 | setattr(obj, self.storage_type_field, '') 94 | if self.bucket_name_field: 95 | setattr(obj, self.bucket_name_field, '') 96 | 97 | def _get_s3_path(self, filename): 98 | if not self.static_root_parent: 99 | raise ValueError('S3FileUploadField field requires ' 100 | 'static_root_parent to be set.') 101 | 102 | return re.sub('^\/', '', self._get_path(filename).replace( 103 | self.static_root_parent, '')) 104 | 105 | def _delete_file(self, filename, obj): 106 | storage_type = getattr(obj, self.storage_type_field, '') 107 | bucket_name = getattr(obj, self.bucket_name_field, '') 108 | 109 | if not (storage_type and bucket_name): 110 | return super(S3FileUploadField, self)._delete_file(filename) 111 | 112 | if storage_type != 's3': 113 | raise ValueError( 114 | 'Storage type "%s" is invalid, the only supported storage type' 115 | ' (apart from default local storage) is s3.' % storage_type) 116 | 117 | conn = S3Connection(self.access_key_id, self.access_key_secret) 118 | bucket = conn.get_bucket(bucket_name) 119 | 120 | path = self._get_s3_path(filename) 121 | k = Key(bucket) 122 | k.key = path 123 | 124 | try: 125 | bucket.delete_key(k) 126 | except S3ResponseError: 127 | pass 128 | 129 | def _save_file_local(self, temp_file, filename): 130 | path = self._get_path(filename) 131 | if not op.exists(op.dirname(path)): 132 | os.makedirs(os.path.dirname(path), self.permission | 0o111) 133 | 134 | fd = open(path, 'wb') 135 | 136 | # Thanks to: 137 | # http://stackoverflow.com/a/3253276/2066849 138 | temp_file.seek(0) 139 | t = temp_file.read(1048576) 140 | while t: 141 | fd.write(t) 142 | t = temp_file.read(1048576) 143 | 144 | fd.close() 145 | 146 | return filename 147 | 148 | def _save_file(self, temp_file, filename): 149 | if not (self.storage_type and self.bucket_name): 150 | return self._save_file_local(temp_file, filename) 151 | 152 | if self.storage_type != 's3': 153 | raise ValueError( 154 | 'Storage type "%s" is invalid, the only supported storage type' 155 | ' (apart from default local storage) is s3.' 156 | % self.storage_type) 157 | 158 | conn = S3Connection(self.access_key_id, self.access_key_secret) 159 | bucket = conn.get_bucket(self.bucket_name) 160 | 161 | path = self._get_s3_path(filename) 162 | k = bucket.new_key(path) 163 | k.set_contents_from_string(temp_file.getvalue()) 164 | k.set_acl(self.acl) 165 | 166 | return filename 167 | 168 | 169 | class S3ImageUploadInput(ImageUploadInput): 170 | """ 171 | Inherits from flask-admin ImageUploadInput, to render images 172 | uploaded to Amazon S3 (as well as the default local storage). 173 | """ 174 | 175 | def get_url(self, field): 176 | if op.isfile(op.join(field.base_path, field.data)): 177 | return super(S3ImageUploadInput, self).get_url(field) 178 | 179 | if field.thumbnail_size: 180 | filename = field.thumbnail_fn(field.data) 181 | else: 182 | filename = field.data 183 | 184 | if field.url_relative_path: 185 | filename = urljoin(field.url_relative_path, filename) 186 | 187 | return url_for_s3(field.endpoint, bucket_name=field.bucket_name, 188 | filename=filename) 189 | 190 | 191 | class S3ImageUploadField(S3FileUploadField): 192 | """ 193 | Revised version of flask-admin ImageUploadField, to allow image 194 | uploading to Amazon S3 (as well as the default local storage). 195 | 196 | Based loosely on code from: 197 | http://stackoverflow.com/a/29178240/2066849 198 | """ 199 | 200 | widget = S3ImageUploadInput() 201 | 202 | keep_image_formats = ('PNG',) 203 | 204 | def __init__(self, label=None, validators=None, 205 | max_size=None, thumbgen=None, thumbnail_size=None, 206 | url_relative_path=None, endpoint='static', 207 | **kwargs): 208 | # Check if PIL is installed 209 | if Image is None: 210 | raise ImportError('PIL library was not found') 211 | 212 | self.max_size = max_size 213 | self.thumbnail_fn = thumbgen or thumbgen_filename 214 | self.thumbnail_size = thumbnail_size 215 | self.endpoint = endpoint 216 | self.image = None 217 | self.url_relative_path = url_relative_path 218 | 219 | if (not ('allowed_extensions' in kwargs) 220 | or not kwargs['allowed_extensions']): 221 | kwargs['allowed_extensions'] = \ 222 | ('gif', 'jpg', 'jpeg', 'png', 'tiff') 223 | 224 | super(S3ImageUploadField, self).__init__(label, validators, 225 | **kwargs) 226 | 227 | def pre_validate(self, form): 228 | super(S3ImageUploadField, self).pre_validate(form) 229 | 230 | if (self.data and 231 | isinstance(self.data, FileStorage) and 232 | self.data.filename): 233 | try: 234 | self.image = Image.open(self.data) 235 | except Exception as e: 236 | raise ValidationError('Invalid image: %s' % e) 237 | 238 | # Deletion 239 | def _delete_file(self, filename, obj): 240 | storage_type = getattr(obj, self.storage_type_field, '') 241 | bucket_name = getattr(obj, self.bucket_name_field, '') 242 | 243 | super(S3ImageUploadField, self)._delete_file(filename, obj) 244 | 245 | self._delete_thumbnail(filename, storage_type, bucket_name) 246 | 247 | def _delete_thumbnail_local(self, filename): 248 | path = self._get_path(self.thumbnail_fn(filename)) 249 | 250 | if op.exists(path): 251 | os.remove(path) 252 | 253 | def _delete_thumbnail(self, filename, storage_type, bucket_name): 254 | if not (storage_type and bucket_name): 255 | self._delete_thumbnail_local(filename) 256 | return 257 | 258 | if storage_type != 's3': 259 | raise ValueError( 260 | 'Storage type "%s" is invalid, the only supported storage type' 261 | ' (apart from default local storage) is s3.' % storage_type) 262 | 263 | conn = S3Connection(self.access_key_id, self.access_key_secret) 264 | bucket = conn.get_bucket(bucket_name) 265 | 266 | path = self._get_s3_path(self.thumbnail_fn(filename)) 267 | k = Key(bucket) 268 | k.key = path 269 | 270 | try: 271 | bucket.delete_key(k) 272 | except S3ResponseError: 273 | pass 274 | 275 | # Saving 276 | def _save_file(self, temp_file, filename): 277 | if self.storage_type and (self.storage_type != 's3'): 278 | raise ValueError( 279 | 'Storage type "%s" is invalid, the only supported storage type' 280 | ' (apart from default local storage) is s3.' % ( 281 | self.storage_type,)) 282 | 283 | # Figure out format 284 | filename, format = self._get_save_format(filename, self.image) 285 | 286 | if self.image: # and (self.image.format != format or self.max_size): 287 | if self.max_size: 288 | image = self._resize(self.image, self.max_size) 289 | else: 290 | image = self.image 291 | 292 | temp_file = BytesIO() 293 | self._save_image(image, temp_file, format) 294 | 295 | super(S3ImageUploadField, self)._save_file(temp_file, filename) 296 | temp_file_thumbnail = BytesIO() 297 | self._save_thumbnail(temp_file_thumbnail, filename, format) 298 | 299 | return filename 300 | 301 | def _save_thumbnail(self, temp_file, filename, format): 302 | if self.image and self.thumbnail_size: 303 | self._save_image(self._resize(self.image, self.thumbnail_size), 304 | temp_file, 305 | format) 306 | 307 | super(S3ImageUploadField, self)._save_file( 308 | temp_file, self.thumbnail_fn(filename)) 309 | 310 | def _resize(self, image, size): 311 | (width, height, force) = size 312 | 313 | if image.size[0] > width or image.size[1] > height: 314 | if force: 315 | return ImageOps.fit(self.image, (width, height), 316 | Image.ANTIALIAS) 317 | else: 318 | thumb = self.image.copy() 319 | thumb.thumbnail((width, height), Image.ANTIALIAS) 320 | return thumb 321 | 322 | return image 323 | 324 | def _save_image(self, image, temp_file, format='JPEG'): 325 | if image.mode not in ('RGB', 'RGBA'): 326 | image = image.convert('RGBA') 327 | 328 | image.save(temp_file, format) 329 | 330 | def _get_save_format(self, filename, image): 331 | if image.format not in self.keep_image_formats: 332 | name, ext = op.splitext(filename) 333 | filename = '%s.jpg' % name 334 | return filename, 'JPEG' 335 | 336 | return filename, image.format 337 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import setuptools 4 | 5 | module_path = os.path.join(os.path.dirname(__file__), 'flask_admin_s3_upload.py') 6 | version_line = [line for line in open(module_path) 7 | if line.startswith('__version__')][0] 8 | 9 | __version__ = version_line.split('__version__ = ')[-1][1:][:-2] 10 | 11 | setuptools.setup( 12 | name="flask-admin-s3-upload", 13 | version=__version__, 14 | url="https://github.com/Jaza/flask-admin-s3-upload", 15 | 16 | author="Jeremy Epstein", 17 | author_email="jazepstein@gmail.com", 18 | 19 | description="Field types for allowing file and image uploads to Amazon S3 (as well as default local storage) in Flask-Admin.", 20 | long_description=open('README.rst').read(), 21 | 22 | py_modules=['flask_admin_s3_upload'], 23 | zip_safe=False, 24 | platforms='any', 25 | 26 | install_requires=['Flask-Admin', 'url-for-s3', 'boto'], 27 | 28 | classifiers=[ 29 | 'Development Status :: 2 - Pre-Alpha', 30 | 'Environment :: Web Environment', 31 | 'Intended Audience :: Developers', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.3', 38 | ], 39 | ) 40 | --------------------------------------------------------------------------------