upload.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. import os
  2. import os.path as op
  3. from werkzeug import secure_filename
  4. from werkzeug.datastructures import FileStorage
  5. from wtforms import ValidationError, fields
  6. from wtforms.widgets import HTMLString, html_params
  7. try:
  8. from wtforms.fields.core import _unset_value as unset_value
  9. except ImportError:
  10. from wtforms.utils import unset_value
  11. from flask_admin.babel import gettext
  12. from flask_admin.helpers import get_url
  13. from flask_admin._compat import string_types, urljoin
  14. try:
  15. from PIL import Image, ImageOps
  16. except ImportError:
  17. Image = None
  18. ImageOps = None
  19. __all__ = ['FileUploadInput', 'FileUploadField',
  20. 'ImageUploadInput', 'ImageUploadField',
  21. 'namegen_filename', 'thumbgen_filename']
  22. # Widgets
  23. class FileUploadInput(object):
  24. """
  25. Renders a file input chooser field.
  26. You can customize `empty_template` and `data_template` members to customize
  27. look and feel.
  28. """
  29. empty_template = ('<input %(file)s>')
  30. data_template = ('<div>'
  31. ' <input %(text)s>'
  32. ' <input type="checkbox" name="%(marker)s">Delete</input>'
  33. '</div>'
  34. '<input %(file)s>')
  35. def __call__(self, field, **kwargs):
  36. kwargs.setdefault('id', field.id)
  37. kwargs.setdefault('name', field.name)
  38. template = self.data_template if field.data else self.empty_template
  39. if field.errors:
  40. template = self.empty_template
  41. if field.data and isinstance(field.data, FileStorage):
  42. value = field.data.filename
  43. else:
  44. value = field.data or ''
  45. return HTMLString(template % {
  46. 'text': html_params(type='text',
  47. readonly='readonly',
  48. value=value,
  49. name=field.name),
  50. 'file': html_params(type='file',
  51. value=value,
  52. **kwargs),
  53. 'marker': '_%s-delete' % field.name
  54. })
  55. class ImageUploadInput(object):
  56. """
  57. Renders a image input chooser field.
  58. You can customize `empty_template` and `data_template` members to customize
  59. look and feel.
  60. """
  61. empty_template = ('<input %(file)s>')
  62. data_template = ('<div class="image-thumbnail">'
  63. ' <img %(image)s>'
  64. ' <input type="checkbox" name="%(marker)s">Delete</input>'
  65. ' <input %(text)s>'
  66. '</div>'
  67. '<input %(file)s>')
  68. def __call__(self, field, **kwargs):
  69. kwargs.setdefault('id', field.id)
  70. kwargs.setdefault('name', field.name)
  71. args = {
  72. 'text': html_params(type='hidden',
  73. value=field.data,
  74. name=field.name),
  75. 'file': html_params(type='file',
  76. **kwargs),
  77. 'marker': '_%s-delete' % field.name
  78. }
  79. if field.data and isinstance(field.data, string_types):
  80. url = self.get_url(field)
  81. args['image'] = html_params(src=url)
  82. template = self.data_template
  83. else:
  84. template = self.empty_template
  85. return HTMLString(template % args)
  86. def get_url(self, field):
  87. if field.thumbnail_size:
  88. filename = field.thumbnail_fn(field.data)
  89. else:
  90. filename = field.data
  91. if field.url_relative_path:
  92. filename = urljoin(field.url_relative_path, filename)
  93. return get_url(field.endpoint, filename=filename)
  94. # Fields
  95. class FileUploadField(fields.StringField):
  96. """
  97. Customizable file-upload field.
  98. Saves file to configured path, handles updates and deletions. Inherits from `StringField`,
  99. resulting filename will be stored as string.
  100. """
  101. widget = FileUploadInput()
  102. def __init__(self, label=None, validators=None,
  103. base_path=None, relative_path=None,
  104. namegen=None, allowed_extensions=None,
  105. permission=0o666, allow_overwrite=True,
  106. **kwargs):
  107. """
  108. Constructor.
  109. :param label:
  110. Display label
  111. :param validators:
  112. Validators
  113. :param base_path:
  114. Absolute path to the directory which will store files
  115. :param relative_path:
  116. Relative path from the directory. Will be prepended to the file name for uploaded files.
  117. Flask-Admin uses `urlparse.urljoin` to generate resulting filename, so make sure you have
  118. trailing slash.
  119. :param namegen:
  120. Function that will generate filename from the model and uploaded file object.
  121. Please note, that model is "dirty" model object, before it was committed to database.
  122. For example::
  123. import os.path as op
  124. def prefix_name(obj, file_data):
  125. parts = op.splitext(file_data.filename)
  126. return secure_filename('file-%s%s' % parts)
  127. class MyForm(BaseForm):
  128. upload = FileUploadField('File', namegen=prefix_name)
  129. :param allowed_extensions:
  130. List of allowed extensions. If not provided, will allow any file.
  131. :param allow_overwrite:
  132. Whether to overwrite existing files in upload directory. Defaults to `True`.
  133. .. versionadded:: 1.1.1
  134. The `allow_overwrite` parameter was added.
  135. """
  136. self.base_path = base_path
  137. self.relative_path = relative_path
  138. self.namegen = namegen or namegen_filename
  139. self.allowed_extensions = allowed_extensions
  140. self.permission = permission
  141. self._allow_overwrite = allow_overwrite
  142. self._should_delete = False
  143. super(FileUploadField, self).__init__(label, validators, **kwargs)
  144. def is_file_allowed(self, filename):
  145. """
  146. Check if file extension is allowed.
  147. :param filename:
  148. File name to check
  149. """
  150. if not self.allowed_extensions:
  151. return True
  152. return ('.' in filename and
  153. filename.rsplit('.', 1)[1].lower() in
  154. map(lambda x: x.lower(), self.allowed_extensions))
  155. def _is_uploaded_file(self, data):
  156. return (data and isinstance(data, FileStorage) and data.filename)
  157. def pre_validate(self, form):
  158. if self._is_uploaded_file(self.data) and not self.is_file_allowed(self.data.filename):
  159. raise ValidationError(gettext('Invalid file extension'))
  160. # Handle overwriting existing content
  161. if not self._is_uploaded_file(self.data):
  162. return
  163. if not self._allow_overwrite and os.path.exists(self._get_path(self.data.filename)):
  164. raise ValidationError(gettext('File "%s" already exists.' % self.data.filename))
  165. def process(self, formdata, data=unset_value):
  166. if formdata:
  167. marker = '_%s-delete' % self.name
  168. if marker in formdata:
  169. self._should_delete = True
  170. return super(FileUploadField, self).process(formdata, data)
  171. def process_formdata(self, valuelist):
  172. if self._should_delete:
  173. self.data = None
  174. elif valuelist:
  175. for data in valuelist:
  176. if self._is_uploaded_file(data):
  177. self.data = data
  178. break
  179. def populate_obj(self, obj, name):
  180. field = getattr(obj, name, None)
  181. if field:
  182. # If field should be deleted, clean it up
  183. if self._should_delete:
  184. self._delete_file(field)
  185. setattr(obj, name, None)
  186. return
  187. if self._is_uploaded_file(self.data):
  188. if field:
  189. self._delete_file(field)
  190. filename = self.generate_name(obj, self.data)
  191. filename = self._save_file(self.data, filename)
  192. # update filename of FileStorage to our validated name
  193. self.data.filename = filename
  194. setattr(obj, name, filename)
  195. def generate_name(self, obj, file_data):
  196. filename = self.namegen(obj, file_data)
  197. if not self.relative_path:
  198. return filename
  199. return urljoin(self.relative_path, filename)
  200. def _get_path(self, filename):
  201. if not self.base_path:
  202. raise ValueError('FileUploadField field requires base_path to be set.')
  203. if callable(self.base_path):
  204. return op.join(self.base_path(), filename)
  205. return op.join(self.base_path, filename)
  206. def _delete_file(self, filename):
  207. path = self._get_path(filename)
  208. if op.exists(path):
  209. os.remove(path)
  210. def _save_file(self, data, filename):
  211. path = self._get_path(filename)
  212. if not op.exists(op.dirname(path)):
  213. os.makedirs(os.path.dirname(path), self.permission | 0o111)
  214. if (self._allow_overwrite is False) and os.path.exists(path):
  215. raise ValueError(gettext('File "%s" already exists.' % path))
  216. data.save(path)
  217. return filename
  218. class ImageUploadField(FileUploadField):
  219. """
  220. Image upload field.
  221. Does image validation, thumbnail generation, updating and deleting images.
  222. Requires PIL (or Pillow) to be installed.
  223. """
  224. widget = ImageUploadInput()
  225. keep_image_formats = ('PNG',)
  226. """
  227. If field detects that uploaded image is not in this list, it will save image
  228. as PNG.
  229. """
  230. def __init__(self, label=None, validators=None,
  231. base_path=None, relative_path=None,
  232. namegen=None, allowed_extensions=None,
  233. max_size=None,
  234. thumbgen=None, thumbnail_size=None,
  235. permission=0o666,
  236. url_relative_path=None, endpoint='static',
  237. **kwargs):
  238. """
  239. Constructor.
  240. :param label:
  241. Display label
  242. :param validators:
  243. Validators
  244. :param base_path:
  245. Absolute path to the directory which will store files
  246. :param relative_path:
  247. Relative path from the directory. Will be prepended to the file name for uploaded files.
  248. Flask-Admin uses `urlparse.urljoin` to generate resulting filename, so make sure you have
  249. trailing slash.
  250. :param namegen:
  251. Function that will generate filename from the model and uploaded file object.
  252. Please note, that model is "dirty" model object, before it was committed to database.
  253. For example::
  254. import os.path as op
  255. def prefix_name(obj, file_data):
  256. parts = op.splitext(file_data.filename)
  257. return secure_filename('file-%s%s' % parts)
  258. class MyForm(BaseForm):
  259. upload = FileUploadField('File', namegen=prefix_name)
  260. :param allowed_extensions:
  261. List of allowed extensions. If not provided, then gif, jpg, jpeg, png and tiff will be allowed.
  262. :param max_size:
  263. Tuple of (width, height, force) or None. If provided, Flask-Admin will
  264. resize image to the desired size.
  265. Width and height is in pixels. If `force` is set to `True`, will try to fit image into dimensions and
  266. keep aspect ratio, otherwise will just resize to target size.
  267. :param thumbgen:
  268. Thumbnail filename generation function. All thumbnails will be saved as JPEG files,
  269. so there's no need to keep original file extension.
  270. For example::
  271. import os.path as op
  272. def thumb_name(filename):
  273. name, _ = op.splitext(filename)
  274. return secure_filename('%s-thumb.jpg' % name)
  275. class MyForm(BaseForm):
  276. upload = ImageUploadField('File', thumbgen=thumb_name)
  277. :param thumbnail_size:
  278. Tuple or (width, height, force) values. If not provided, thumbnail won't be created.
  279. Width and height is in pixels. If `force` is set to `True`, will try to fit image into dimensions and
  280. keep aspect ratio, otherwise will just resize to target size.
  281. :param url_relative_path:
  282. Relative path from the root of the static directory URL. Only gets used when generating
  283. preview image URLs.
  284. For example, your model might store just file names (`relative_path` set to `None`), but
  285. `base_path` is pointing to subdirectory.
  286. :param endpoint:
  287. Static endpoint for images. Used by widget to display previews. Defaults to 'static'.
  288. """
  289. # Check if PIL is installed
  290. if Image is None:
  291. raise ImportError('PIL library was not found')
  292. self.max_size = max_size
  293. self.thumbnail_fn = thumbgen or thumbgen_filename
  294. self.thumbnail_size = thumbnail_size
  295. self.endpoint = endpoint
  296. self.image = None
  297. self.url_relative_path = url_relative_path
  298. if not allowed_extensions:
  299. allowed_extensions = ('gif', 'jpg', 'jpeg', 'png', 'tiff')
  300. super(ImageUploadField, self).__init__(label, validators,
  301. base_path=base_path,
  302. relative_path=relative_path,
  303. namegen=namegen,
  304. allowed_extensions=allowed_extensions,
  305. permission=permission,
  306. **kwargs)
  307. def pre_validate(self, form):
  308. super(ImageUploadField, self).pre_validate(form)
  309. if self._is_uploaded_file(self.data):
  310. try:
  311. self.image = Image.open(self.data)
  312. except Exception as e:
  313. raise ValidationError('Invalid image: %s' % e)
  314. # Deletion
  315. def _delete_file(self, filename):
  316. super(ImageUploadField, self)._delete_file(filename)
  317. self._delete_thumbnail(filename)
  318. def _delete_thumbnail(self, filename):
  319. path = self._get_path(self.thumbnail_fn(filename))
  320. if op.exists(path):
  321. os.remove(path)
  322. # Saving
  323. def _save_file(self, data, filename):
  324. path = self._get_path(filename)
  325. if not op.exists(op.dirname(path)):
  326. os.makedirs(os.path.dirname(path), self.permission | 0o111)
  327. # Figure out format
  328. filename, format = self._get_save_format(filename, self.image)
  329. if self.image and (self.image.format != format or self.max_size):
  330. if self.max_size:
  331. image = self._resize(self.image, self.max_size)
  332. else:
  333. image = self.image
  334. self._save_image(image, self._get_path(filename), format)
  335. else:
  336. data.seek(0)
  337. data.save(self._get_path(filename))
  338. self._save_thumbnail(data, filename, format)
  339. return filename
  340. def _save_thumbnail(self, data, filename, format):
  341. if self.image and self.thumbnail_size:
  342. path = self._get_path(self.thumbnail_fn(filename))
  343. self._save_image(self._resize(self.image, self.thumbnail_size),
  344. path,
  345. format)
  346. def _resize(self, image, size):
  347. (width, height, force) = size
  348. if image.size[0] > width or image.size[1] > height:
  349. if force:
  350. return ImageOps.fit(self.image, (width, height), Image.ANTIALIAS)
  351. else:
  352. thumb = self.image.copy()
  353. thumb.thumbnail((width, height), Image.ANTIALIAS)
  354. return thumb
  355. return image
  356. def _save_image(self, image, path, format='JPEG'):
  357. if image.mode not in ('RGB', 'RGBA'):
  358. image = image.convert('RGBA')
  359. with open(path, 'wb') as fp:
  360. image.save(fp, format)
  361. def _get_save_format(self, filename, image):
  362. if image.format not in self.keep_image_formats:
  363. name, ext = op.splitext(filename)
  364. filename = '%s.jpg' % name
  365. return filename, 'JPEG'
  366. return filename, image.format
  367. # Helpers
  368. def namegen_filename(obj, file_data):
  369. """
  370. Generate secure filename for uploaded file.
  371. """
  372. return secure_filename(file_data.filename)
  373. def thumbgen_filename(filename):
  374. """
  375. Generate thumbnail name from filename.
  376. """
  377. name, ext = op.splitext(filename)
  378. return '%s_thumb%s' % (name, ext)