s3_3.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. from __future__ import unicode_literals
  2. import datetime
  3. import os
  4. import random
  5. import re
  6. import string
  7. import boto3
  8. from botocore.client import Config
  9. from botocore.exceptions import ClientError
  10. import frappe
  11. import magic
  12. class S3Operations(object):
  13. def __init__(self):
  14. """
  15. Function to initialise the aws settings from frappe S3 File attachment
  16. doctype.
  17. """
  18. self.s3_settings_doc = frappe.get_doc(
  19. 'S3 File Attachment',
  20. 'S3 File Attachment',
  21. )
  22. if (
  23. self.s3_settings_doc.aws_key and
  24. self.s3_settings_doc.aws_secret
  25. ):
  26. self.S3_CLIENT = boto3.client(
  27. 's3',
  28. aws_access_key_id=self.s3_settings_doc.aws_key,
  29. aws_secret_access_key=self.s3_settings_doc.aws_secret,
  30. region_name=self.s3_settings_doc.region_name,
  31. config=Config(signature_version='s3v4')
  32. )
  33. else:
  34. self.S3_CLIENT = boto3.client(
  35. 's3',
  36. region_name=self.s3_settings_doc.region_name,
  37. config=Config(signature_version='s3v4')
  38. )
  39. self.BUCKET = self.s3_settings_doc.bucket_name
  40. self.folder_name = self.s3_settings_doc.folder_name
  41. def strip_special_chars(self, file_name):
  42. """
  43. Strips file charachters which doesnt match the regex.
  44. """
  45. regex = re.compile('[^0-9a-zA-Z._-]')
  46. file_name = regex.sub('', file_name)
  47. return file_name
  48. def key_generator(self, file_name, parent_doctype, parent_name):
  49. """
  50. Generate keys for s3 objects uploaded with file name attached.
  51. """
  52. hook_cmd = frappe.get_hooks().get("s3_key_generator")
  53. if hook_cmd:
  54. try:
  55. k = frappe.get_attr(hook_cmd[0])(
  56. file_name=file_name,
  57. parent_doctype=parent_doctype,
  58. parent_name=parent_name
  59. )
  60. if k:
  61. return k.rstrip('/').lstrip('/')
  62. except:
  63. pass
  64. file_name = file_name.replace(' ', '_')
  65. file_name = self.strip_special_chars(file_name)
  66. key = ''.join(
  67. random.choice(
  68. string.ascii_uppercase + string.digits) for _ in range(8)
  69. )
  70. today = datetime.datetime.now()
  71. year = today.strftime("%Y")
  72. month = today.strftime("%m")
  73. day = today.strftime("%d")
  74. doc_path = None
  75. if not doc_path:
  76. if self.folder_name:
  77. final_key = self.folder_name + "/" + year + "/" + month + \
  78. "/" + day + "/" + parent_doctype + "/" + key + "_" + \
  79. file_name
  80. else:
  81. final_key = year + "/" + month + "/" + day + "/" + \
  82. parent_doctype + "/" + key + "_" + file_name
  83. return final_key
  84. else:
  85. final_key = doc_path + '/' + key + "_" + file_name
  86. return final_key
  87. def upload_files_to_s3_with_key(
  88. self, file_path, file_name, is_private, parent_doctype, parent_name
  89. ):
  90. """
  91. Uploads a new file to S3.
  92. Strips the file extension to set the content_type in metadata.
  93. """
  94. mime_type = magic.from_file(file_path, mime=True)
  95. key = self.key_generator(file_name, parent_doctype, parent_name)
  96. content_type = mime_type
  97. try:
  98. if is_private:
  99. self.S3_CLIENT.upload_file(
  100. file_path, self.BUCKET, key,
  101. ExtraArgs={
  102. "ContentType": content_type,
  103. "Metadata": {
  104. "ContentType": content_type,
  105. "file_name": file_name
  106. }
  107. }
  108. )
  109. else:
  110. self.S3_CLIENT.upload_file(
  111. file_path, self.BUCKET, key,
  112. ExtraArgs={
  113. "ContentType": content_type,
  114. "ACL": 'public-read',
  115. "Metadata": {
  116. "ContentType": content_type,
  117. }
  118. }
  119. )
  120. except boto3.exceptions.S3UploadFailedError:
  121. frappe.throw(frappe._("File Upload Failed. Please try again."))
  122. return key
  123. def delete_from_s3(self, key):
  124. """Delete file from s3"""
  125. self.s3_settings_doc = frappe.get_doc(
  126. 'S3 File Attachment',
  127. 'S3 File Attachment',
  128. )
  129. if self.s3_settings_doc.delete_file_from_cloud:
  130. s3_client = boto3.client(
  131. 's3',
  132. aws_access_key_id=self.s3_settings_doc.aws_key,
  133. aws_secret_access_key=self.s3_settings_doc.aws_secret,
  134. region_name=self.s3_settings_doc.region_name,
  135. config=Config(signature_version='s3v4')
  136. )
  137. try:
  138. s3_client.delete_object(
  139. Bucket=self.s3_settings_doc.bucket_name,
  140. Key=key
  141. )
  142. except ClientError:
  143. frappe.throw(frappe._("Access denied: Could not delete file"))
  144. def read_file_from_s3(self, key):
  145. """
  146. Function to read file from a s3 file.
  147. """
  148. return self.S3_CLIENT.get_object(Bucket=self.BUCKET, Key=key)
  149. def get_url(self, key, file_name=None):
  150. """
  151. Return url.
  152. :param bucket: s3 bucket name
  153. :param key: s3 object key
  154. """
  155. if self.s3_settings_doc.signed_url_expiry_time:
  156. self.signed_url_expiry_time = self.s3_settings_doc.signed_url_expiry_time # noqa
  157. else:
  158. self.signed_url_expiry_time = 120
  159. params = {
  160. 'Bucket': self.BUCKET,
  161. 'Key': key,
  162. }
  163. if file_name:
  164. params['ResponseContentDisposition'] = 'filename={}'.format(file_name)
  165. url = self.S3_CLIENT.generate_presigned_url(
  166. 'get_object',
  167. Params=params,
  168. ExpiresIn=self.signed_url_expiry_time,
  169. )
  170. return url
  171. @frappe.whitelist()
  172. def file_upload_to_s3(doc, method):
  173. """
  174. check and upload files to s3. the path check and
  175. """
  176. s3_upload = S3Operations()
  177. path = doc.file_url
  178. site_path = frappe.utils.get_site_path()
  179. parent_doctype = doc.attached_to_doctype
  180. parent_name = doc.attached_to_name
  181. ignore_s3_upload_for_doctype = frappe.local.conf.get('ignore_s3_upload_for_doctype') or ['Data Import']
  182. if parent_doctype not in ignore_s3_upload_for_doctype:
  183. if not doc.is_private:
  184. file_path = site_path + '/public' + path
  185. else:
  186. file_path = site_path + path
  187. key = s3_upload.upload_files_to_s3_with_key(
  188. file_path, doc.file_name,
  189. doc.is_private, parent_doctype,
  190. parent_name
  191. )
  192. if doc.is_private:
  193. method = "frappe_s3_attachment.controller.generate_file"
  194. file_url = """/api/method/{0}?key={1}&file_name={2}""".format(method, key, doc.file_name)
  195. else:
  196. file_url = '{}/{}/{}'.format(
  197. s3_upload.S3_CLIENT.meta.endpoint_url,
  198. s3_upload.BUCKET,
  199. key
  200. )
  201. os.remove(file_path)
  202. frappe.db.sql("""UPDATE `tabFile` SET file_url=%s, folder=%s,
  203. old_parent=%s, content_hash=%s WHERE name=%s""", (
  204. file_url, 'Home/Attachments', 'Home/Attachments', key, doc.name))
  205. doc.file_url = file_url
  206. if parent_doctype and frappe.get_meta(parent_doctype).get('image_field'):
  207. frappe.db.set_value(parent_doctype, parent_name, frappe.get_meta(parent_doctype).get('image_field'), file_url)
  208. frappe.db.commit()
  209. @frappe.whitelist()
  210. def generate_file(key=None, file_name=None):
  211. """
  212. Function to stream file from s3.
  213. """
  214. if key:
  215. s3_upload = S3Operations()
  216. signed_url = s3_upload.get_url(key, file_name)
  217. frappe.local.response["type"] = "redirect"
  218. frappe.local.response["location"] = signed_url
  219. else:
  220. frappe.local.response['body'] = "Key not found."
  221. return
  222. def upload_existing_files_s3(name, file_name):
  223. """
  224. Function to upload all existing files.
  225. """
  226. file_doc_name = frappe.db.get_value('File', {'name': name})
  227. if file_doc_name:
  228. doc = frappe.get_doc('File', name)
  229. s3_upload = S3Operations()
  230. path = doc.file_url
  231. site_path = frappe.utils.get_site_path()
  232. parent_doctype = doc.attached_to_doctype
  233. parent_name = doc.attached_to_name
  234. if not doc.is_private:
  235. file_path = site_path + '/public' + path
  236. else:
  237. file_path = site_path + path
  238. key = s3_upload.upload_files_to_s3_with_key(
  239. file_path, doc.file_name,
  240. doc.is_private, parent_doctype,
  241. parent_name
  242. )
  243. if doc.is_private:
  244. method = "frappe_s3_attachment.controller.generate_file"
  245. file_url = """/api/method/{0}?key={1}""".format(method, key)
  246. else:
  247. file_url = '{}/{}/{}'.format(
  248. s3_upload.S3_CLIENT.meta.endpoint_url,
  249. s3_upload.BUCKET,
  250. key
  251. )
  252. os.remove(file_path)
  253. doc = frappe.db.sql("""UPDATE `tabFile` SET file_url=%s, folder=%s,
  254. old_parent=%s, content_hash=%s WHERE name=%s""", (
  255. file_url, 'Home/Attachments', 'Home/Attachments', key, doc.name))
  256. frappe.db.commit()
  257. else:
  258. pass
  259. def s3_file_regex_match(file_url):
  260. """
  261. Match the public file regex match.
  262. """
  263. return re.match(
  264. r'^(https:|/api/method/frappe_s3_attachment.controller.generate_file)',
  265. file_url
  266. )
  267. @frappe.whitelist()
  268. def migrate_existing_files():
  269. """
  270. Function to migrate the existing files to s3.
  271. """
  272. # get_all_files_from_public_folder_and_upload_to_s3
  273. files_list = frappe.get_all(
  274. 'File',
  275. fields=['name', 'file_url', 'file_name']
  276. )
  277. for file in files_list:
  278. if file['file_url']:
  279. if not s3_file_regex_match(file['file_url']):
  280. upload_existing_files_s3(file['name'], file['file_name'])
  281. return True
  282. def delete_from_cloud(doc, method):
  283. """Delete file from s3"""
  284. s3 = S3Operations()
  285. s3.delete_from_s3(doc.content_hash)
  286. @frappe.whitelist()
  287. def ping():
  288. """
  289. Test function to check if api function work.
  290. """
  291. return "pong"