filemanager.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import logging
  2. import os
  3. import os.path as op
  4. import re
  5. import uuid
  6. from flask.globals import _request_ctx_stack
  7. from werkzeug.datastructures import FileStorage
  8. from werkzeug.utils import secure_filename
  9. from wtforms import ValidationError
  10. try:
  11. from flask import _app_ctx_stack
  12. except ImportError:
  13. _app_ctx_stack = None
  14. app_stack = _app_ctx_stack or _request_ctx_stack
  15. log = logging.getLogger(__name__)
  16. try:
  17. from PIL import Image, ImageOps
  18. except ImportError:
  19. Image = None
  20. ImageOps = None
  21. class FileManager(object):
  22. def __init__(
  23. self,
  24. base_path=None,
  25. relative_path="",
  26. namegen=None,
  27. allowed_extensions=None,
  28. permission=0o755,
  29. **kwargs
  30. ):
  31. ctx = app_stack.top
  32. if "UPLOAD_FOLDER" in ctx.app.config and not base_path:
  33. base_path = ctx.app.config["UPLOAD_FOLDER"]
  34. if not base_path:
  35. raise Exception("Config key UPLOAD_FOLDER is mandatory")
  36. self.base_path = base_path
  37. self.relative_path = relative_path
  38. self.namegen = namegen or uuid_namegen
  39. if not allowed_extensions and "FILE_ALLOWED_EXTENSIONS" in ctx.app.config:
  40. self.allowed_extensions = ctx.app.config["FILE_ALLOWED_EXTENSIONS"]
  41. else:
  42. self.allowed_extensions = allowed_extensions
  43. self.permission = permission
  44. self._should_delete = False
  45. def is_file_allowed(self, filename):
  46. if not self.allowed_extensions:
  47. return True
  48. return (
  49. "." in filename
  50. and filename.rsplit(".", 1)[1].lower() in self.allowed_extensions
  51. )
  52. def generate_name(self, obj, file_data):
  53. return self.namegen(file_data)
  54. def get_path(self, filename):
  55. if not self.base_path:
  56. raise ValueError("FileUploadField field requires base_path to be set.")
  57. return op.join(self.base_path, filename)
  58. def delete_file(self, filename):
  59. path = self.get_path(filename)
  60. if op.exists(path):
  61. os.remove(path)
  62. def save_file(self, data, filename):
  63. filename_ = secure_filename(filename)
  64. path = self.get_path(filename_)
  65. if not op.exists(op.dirname(path)):
  66. os.makedirs(os.path.dirname(path), self.permission)
  67. data.save(path)
  68. return filename_
  69. class ImageManager(FileManager):
  70. """
  71. Image Manager will manage your image files referenced on SQLAlchemy Model
  72. will save files on IMG_UPLOAD_FOLDER as <uuid>_sep_<filename>
  73. """
  74. keep_image_formats = ("PNG",)
  75. def __init__(
  76. self,
  77. base_path=None,
  78. relative_path=None,
  79. max_size=None,
  80. namegen=None,
  81. allowed_extensions=None,
  82. thumbgen=None,
  83. thumbnail_size=None,
  84. permission=0o755,
  85. **kwargs
  86. ):
  87. # Check if PIL is installed
  88. if Image is None:
  89. raise Exception("PIL library was not found")
  90. ctx = app_stack.top
  91. if "IMG_SIZE" in ctx.app.config and not max_size:
  92. self.max_size = ctx.app.config["IMG_SIZE"]
  93. if "IMG_UPLOAD_URL" in ctx.app.config and not relative_path:
  94. relative_path = ctx.app.config["IMG_UPLOAD_URL"]
  95. if not relative_path:
  96. raise Exception("Config key IMG_UPLOAD_URL is mandatory")
  97. if "IMG_UPLOAD_FOLDER" in ctx.app.config and not base_path:
  98. base_path = ctx.app.config["IMG_UPLOAD_FOLDER"]
  99. if not base_path:
  100. raise Exception("Config key IMG_UPLOAD_FOLDER is mandatory")
  101. self.thumbnail_fn = thumbgen or thumbgen_filename
  102. self.thumbnail_size = thumbnail_size
  103. self.image = None
  104. if not allowed_extensions:
  105. allowed_extensions = ("gif", "jpg", "jpeg", "png", "tiff")
  106. super(ImageManager, self).__init__(
  107. base_path=base_path,
  108. relative_path=relative_path,
  109. namegen=namegen,
  110. allowed_extensions=allowed_extensions,
  111. permission=permission,
  112. **kwargs
  113. )
  114. def get_url(self, filename):
  115. if isinstance(filename, FileStorage):
  116. return filename.filename
  117. return self.relative_path + filename
  118. def get_url_thumbnail(self, filename):
  119. if isinstance(filename, FileStorage):
  120. return filename.filename
  121. return self.relative_path + thumbgen_filename(filename)
  122. # Deletion
  123. def delete_file(self, filename):
  124. super(ImageManager, self).delete_file(filename)
  125. self.delete_thumbnail(filename)
  126. def delete_thumbnail(self, filename):
  127. path = self.get_path(self.thumbnail_fn(filename))
  128. if op.exists(path):
  129. os.remove(path)
  130. # Saving
  131. def save_file(self, data, filename, size=None, thumbnail_size=None):
  132. """
  133. Saves an image File
  134. :param data: FileStorage from Flask form upload field
  135. :param filename: Filename with full path
  136. """
  137. max_size = size or self.max_size
  138. thumbnail_size = thumbnail_size or self.thumbnail_size
  139. if data and isinstance(data, FileStorage):
  140. try:
  141. self.image = Image.open(data)
  142. except Exception as e:
  143. raise ValidationError("Invalid image: %s" % e)
  144. path = self.get_path(filename)
  145. # If Path does not exist, create it
  146. if not op.exists(op.dirname(path)):
  147. os.makedirs(os.path.dirname(path), self.permission)
  148. # Figure out format
  149. filename, format = self.get_save_format(filename, self.image)
  150. if self.image and (self.image.format != format or max_size):
  151. if max_size:
  152. image = self.resize(self.image, max_size)
  153. else:
  154. image = self.image
  155. self.save_image(image, self.get_path(filename), format)
  156. else:
  157. data.seek(0)
  158. data.save(path)
  159. self.save_thumbnail(data, filename, format, thumbnail_size)
  160. return filename
  161. def save_thumbnail(self, data, filename, format, thumbnail_size=None):
  162. thumbnail_size = thumbnail_size or self.thumbnail_size
  163. if self.image and thumbnail_size:
  164. path = self.get_path(self.thumbnail_fn(filename))
  165. self.save_image(self.resize(self.image, thumbnail_size), path, format)
  166. def resize(self, image, size):
  167. """
  168. Resizes the image
  169. :param image: The image object
  170. :param size: size is PIL tuple (width, height, force) ex: (200,100,True)
  171. """
  172. (width, height, force) = size
  173. if image.size[0] > width or image.size[1] > height:
  174. if force:
  175. return ImageOps.fit(self.image, (width, height), Image.LANCZOS)
  176. else:
  177. thumb = self.image.copy()
  178. thumb.thumbnail((width, height), Image.LANCZOS)
  179. return thumb
  180. return image
  181. def save_image(self, image, path, format="JPEG"):
  182. if image.mode not in ("RGB", "RGBA"):
  183. image = image.convert("RGBA")
  184. with open(path, "wb") as fp:
  185. image.save(fp, format)
  186. def get_save_format(self, filename, image):
  187. if image.format not in self.keep_image_formats:
  188. name, ext = op.splitext(filename)
  189. filename = "%s.jpg" % name
  190. return filename, "JPEG"
  191. return filename, image.format
  192. def uuid_namegen(file_data):
  193. return str(uuid.uuid1()) + "_sep_" + file_data.filename
  194. def get_file_original_name(name):
  195. """
  196. Use this function to get the user's original filename.
  197. Filename is concatenated with <UUID>_sep_<FILE NAME>, to avoid collisions.
  198. Use this function on your models on an additional function
  199. ::
  200. class ProjectFiles(Base):
  201. id = Column(Integer, primary_key=True)
  202. file = Column(FileColumn, nullable=False)
  203. def file_name(self):
  204. return get_file_original_name(str(self.file))
  205. :param name:
  206. The file name from model
  207. :return:
  208. Returns the user's original filename removes <UUID>_sep_
  209. """
  210. re_match = re.findall(".*_sep_(.*)", name)
  211. if re_match:
  212. return re_match[0]
  213. else:
  214. return "Not valid"
  215. def uuid_originalname(uuid_filename):
  216. return uuid_filename.split("_sep_")[1]
  217. def thumbgen_filename(filename):
  218. name, ext = op.splitext(filename)
  219. return "%s_thumb%s" % (name, ext)