選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

460 行
13 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. import frappe
  4. import os, base64, re, json
  5. import hashlib
  6. import mimetypes
  7. import io
  8. from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint
  9. from frappe import _
  10. from frappe import conf
  11. from copy import copy
  12. from urllib.parse import unquote
  13. from frappe.utils.image import optimize_image
  14. class MaxFileSizeReachedError(frappe.ValidationError):
  15. pass
  16. def get_file_url(file_data_name):
  17. data = frappe.db.get_value("File", file_data_name, ["file_name", "file_url"], as_dict=True)
  18. return data.file_url or data.file_name
  19. def upload():
  20. # get record details
  21. dt = frappe.form_dict.doctype
  22. dn = frappe.form_dict.docname
  23. file_url = frappe.form_dict.file_url
  24. filename = frappe.form_dict.filename
  25. frappe.form_dict.is_private = cint(frappe.form_dict.is_private)
  26. if not filename and not file_url:
  27. frappe.msgprint(_("Please select a file or url"),
  28. raise_exception=True)
  29. file_doc = get_file_doc()
  30. comment = {}
  31. if dt and dn:
  32. comment = frappe.get_doc(dt, dn).add_comment("Attachment",
  33. _("added {0}").format("<a href='{file_url}' target='_blank'>{file_name}</a>{icon}".format(**{
  34. "icon": ' <i class="fa fa-lock text-warning"></i>' \
  35. if file_doc.is_private else "",
  36. "file_url": file_doc.file_url.replace("#", "%23") \
  37. if file_doc.file_name else file_doc.file_url,
  38. "file_name": file_doc.file_name or file_doc.file_url
  39. })))
  40. return {
  41. "name": file_doc.name,
  42. "file_name": file_doc.file_name,
  43. "file_url": file_doc.file_url,
  44. "is_private": file_doc.is_private,
  45. "comment": comment.as_dict() if comment else {}
  46. }
  47. def get_file_doc(dt=None, dn=None, folder=None, is_private=None, df=None):
  48. '''returns File object (Document) from given parameters or form_dict'''
  49. r = frappe.form_dict
  50. if dt is None: dt = r.doctype
  51. if dn is None: dn = r.docname
  52. if df is None: df = r.docfield
  53. if folder is None: folder = r.folder
  54. if is_private is None: is_private = r.is_private
  55. if r.filedata:
  56. file_doc = save_uploaded(dt, dn, folder, is_private, df)
  57. elif r.file_url:
  58. file_doc = save_url(r.file_url, r.filename, dt, dn, folder, is_private, df)
  59. return file_doc
  60. def save_uploaded(dt, dn, folder, is_private, df=None):
  61. fname, content = get_uploaded_content()
  62. if content:
  63. return save_file(fname, content, dt, dn, folder, is_private=is_private, df=df)
  64. else:
  65. raise Exception
  66. def save_url(file_url, filename, dt, dn, folder, is_private, df=None):
  67. # if not (file_url.startswith("http://") or file_url.startswith("https://")):
  68. # frappe.msgprint("URL must start with 'http://' or 'https://'")
  69. # return None, None
  70. file_url = unquote(file_url)
  71. file_size = frappe.form_dict.file_size
  72. f = frappe.get_doc({
  73. "doctype": "File",
  74. "file_url": file_url,
  75. "file_name": filename,
  76. "attached_to_doctype": dt,
  77. "attached_to_name": dn,
  78. "attached_to_field": df,
  79. "folder": folder,
  80. "file_size": file_size,
  81. "is_private": is_private
  82. })
  83. f.flags.ignore_permissions = True
  84. try:
  85. f.insert()
  86. except frappe.DuplicateEntryError:
  87. return frappe.get_doc("File", f.duplicate_entry)
  88. return f
  89. def get_uploaded_content():
  90. # should not be unicode when reading a file, hence using frappe.form
  91. if 'filedata' in frappe.form_dict:
  92. if "," in frappe.form_dict.filedata:
  93. frappe.form_dict.filedata = frappe.form_dict.filedata.rsplit(",", 1)[1]
  94. frappe.uploaded_content = base64.b64decode(frappe.form_dict.filedata)
  95. frappe.uploaded_filename = frappe.form_dict.filename
  96. return frappe.uploaded_filename, frappe.uploaded_content
  97. else:
  98. frappe.msgprint(_('No file attached'))
  99. return None, None
  100. def save_file(fname, content, dt, dn, folder=None, decode=False, is_private=0, df=None):
  101. if decode:
  102. if isinstance(content, str):
  103. content = content.encode("utf-8")
  104. if b"," in content:
  105. content = content.split(b",")[1]
  106. content = base64.b64decode(content)
  107. file_size = check_max_file_size(content)
  108. content_hash = get_content_hash(content)
  109. content_type = mimetypes.guess_type(fname)[0]
  110. fname = get_file_name(fname, content_hash[-6:])
  111. file_data = get_file_data_from_hash(content_hash, is_private=is_private)
  112. if not file_data:
  113. call_hook_method("before_write_file", file_size=file_size)
  114. write_file_method = get_hook_method('write_file', fallback=save_file_on_filesystem)
  115. file_data = write_file_method(fname, content, content_type=content_type, is_private=is_private)
  116. file_data = copy(file_data)
  117. file_data.update({
  118. "doctype": "File",
  119. "attached_to_doctype": dt,
  120. "attached_to_name": dn,
  121. "attached_to_field": df,
  122. "folder": folder,
  123. "file_size": file_size,
  124. "content_hash": content_hash,
  125. "is_private": is_private
  126. })
  127. f = frappe.get_doc(file_data)
  128. f.flags.ignore_permissions = True
  129. try:
  130. f.insert()
  131. except frappe.DuplicateEntryError:
  132. return frappe.get_doc("File", f.duplicate_entry)
  133. return f
  134. def get_file_data_from_hash(content_hash, is_private=0):
  135. for name in frappe.db.sql_list("select name from `tabFile` where content_hash=%s and is_private=%s", (content_hash, is_private)):
  136. b = frappe.get_doc('File', name)
  137. return {k: b.get(k) for k in frappe.get_hooks()['write_file_keys']}
  138. return False
  139. def save_file_on_filesystem(fname, content, content_type=None, is_private=0):
  140. fpath = write_file(content, fname, is_private)
  141. if is_private:
  142. file_url = "/private/files/{0}".format(fname)
  143. else:
  144. file_url = "/files/{0}".format(fname)
  145. return {
  146. 'file_name': os.path.basename(fpath),
  147. 'file_url': file_url
  148. }
  149. def get_max_file_size():
  150. return conf.get('max_file_size') or 10485760
  151. def check_max_file_size(content):
  152. max_file_size = get_max_file_size()
  153. file_size = len(content)
  154. if file_size > max_file_size:
  155. frappe.msgprint(_("File size exceeded the maximum allowed size of {0} MB").format(
  156. max_file_size / 1048576),
  157. raise_exception=MaxFileSizeReachedError)
  158. return file_size
  159. def write_file(content, fname, is_private=0):
  160. """write file to disk with a random name (to compare)"""
  161. file_path = get_files_path(is_private=is_private)
  162. # create directory (if not exists)
  163. frappe.create_folder(file_path)
  164. # write the file
  165. if isinstance(content, str):
  166. content = content.encode()
  167. with open(os.path.join(file_path.encode('utf-8'), fname.encode('utf-8')), 'wb+') as f:
  168. f.write(content)
  169. return get_files_path(fname, is_private=is_private)
  170. def remove_all(dt, dn, from_delete=False):
  171. """remove all files in a transaction"""
  172. try:
  173. for fid in frappe.db.sql_list("""select name from `tabFile` where
  174. attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
  175. remove_file(fid, dt, dn, from_delete)
  176. except Exception as e:
  177. if e.args[0]!=1054: raise # (temp till for patched)
  178. def remove_file_by_url(file_url, doctype=None, name=None):
  179. if doctype and name:
  180. fid = frappe.db.get_value("File", {"file_url": file_url,
  181. "attached_to_doctype": doctype, "attached_to_name": name})
  182. else:
  183. fid = frappe.db.get_value("File", {"file_url": file_url})
  184. if fid:
  185. return remove_file(fid)
  186. def remove_file(fid, attached_to_doctype=None, attached_to_name=None, from_delete=False):
  187. """Remove file and File entry"""
  188. file_name = None
  189. if not (attached_to_doctype and attached_to_name):
  190. attached = frappe.db.get_value("File", fid,
  191. ["attached_to_doctype", "attached_to_name", "file_name"])
  192. if attached:
  193. attached_to_doctype, attached_to_name, file_name = attached
  194. ignore_permissions, comment = False, None
  195. if attached_to_doctype and attached_to_name and not from_delete:
  196. doc = frappe.get_doc(attached_to_doctype, attached_to_name)
  197. ignore_permissions = doc.has_permission("write") or False
  198. if frappe.flags.in_web_form:
  199. ignore_permissions = True
  200. if not file_name:
  201. file_name = frappe.db.get_value("File", fid, "file_name")
  202. comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name))
  203. frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions)
  204. return comment
  205. def delete_file_data_content(doc, only_thumbnail=False):
  206. method = get_hook_method('delete_file_data_content', fallback=delete_file_from_filesystem)
  207. method(doc, only_thumbnail=only_thumbnail)
  208. def delete_file_from_filesystem(doc, only_thumbnail=False):
  209. """Delete file, thumbnail from File document"""
  210. if only_thumbnail:
  211. delete_file(doc.thumbnail_url)
  212. else:
  213. delete_file(doc.file_url)
  214. delete_file(doc.thumbnail_url)
  215. def delete_file(path):
  216. """Delete file from `public folder`"""
  217. if path:
  218. if ".." in path.split("/"):
  219. frappe.msgprint(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path))
  220. parts = os.path.split(path.strip("/"))
  221. if parts[0]=="files":
  222. path = frappe.utils.get_site_path("public", "files", parts[-1])
  223. else:
  224. path = frappe.utils.get_site_path("private", "files", parts[-1])
  225. path = encode(path)
  226. if os.path.exists(path):
  227. os.remove(path)
  228. def get_file(fname):
  229. """Returns [`file_name`, `content`] for given file name `fname`"""
  230. file_path = get_file_path(fname)
  231. # read the file
  232. with io.open(encode(file_path), mode='rb') as f:
  233. content = f.read()
  234. try:
  235. # for plain text files
  236. content = content.decode()
  237. except UnicodeDecodeError:
  238. # for .png, .jpg, etc
  239. pass
  240. return [file_path.rsplit("/", 1)[-1], content]
  241. def get_file_path(file_name):
  242. """Returns file path from given file name"""
  243. f = frappe.db.sql("""select file_url from `tabFile`
  244. where name=%s or file_name=%s""", (file_name, file_name))
  245. if f:
  246. file_name = f[0][0]
  247. file_path = file_name
  248. if "/" not in file_path:
  249. file_path = "/files/" + file_path
  250. if file_path.startswith("/private/files/"):
  251. file_path = get_files_path(*file_path.split("/private/files/", 1)[1].split("/"), is_private=1)
  252. elif file_path.startswith("/files/"):
  253. file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/"))
  254. else:
  255. frappe.throw(_("There is some problem with the file url: {0}").format(file_path))
  256. return file_path
  257. def get_content_hash(content):
  258. if isinstance(content, str):
  259. content = content.encode()
  260. return hashlib.md5(content).hexdigest()
  261. def get_file_name(fname, optional_suffix):
  262. # convert to unicode
  263. fname = cstr(fname)
  264. n_records = frappe.db.sql("select name from `tabFile` where file_name=%s", fname)
  265. if len(n_records) > 0 or os.path.exists(encode(get_files_path(fname))):
  266. f = fname.rsplit('.', 1)
  267. if len(f) == 1:
  268. partial, extn = f[0], ""
  269. else:
  270. partial, extn = f[0], "." + f[1]
  271. return '{partial}{suffix}{extn}'.format(partial=partial, extn=extn, suffix=optional_suffix)
  272. return fname
  273. @frappe.whitelist()
  274. def download_file(file_url):
  275. """
  276. Download file using token and REST API. Valid session or
  277. token is required to download private files.
  278. Method : GET
  279. Endpoint : frappe.utils.file_manager.download_file
  280. URL Params : file_name = /path/to/file relative to site path
  281. """
  282. file_doc = frappe.get_doc("File", {"file_url":file_url})
  283. file_doc.check_permission("read")
  284. path = os.path.join(get_files_path(), os.path.basename(file_url))
  285. with open(path, "rb") as fileobj:
  286. filedata = fileobj.read()
  287. frappe.local.response.filename = os.path.basename(file_url)
  288. frappe.local.response.filecontent = filedata
  289. frappe.local.response.type = "download"
  290. def extract_images_from_doc(doc, fieldname):
  291. content = doc.get(fieldname)
  292. content = extract_images_from_html(doc, content)
  293. if frappe.flags.has_dataurl:
  294. doc.set(fieldname, content)
  295. def extract_images_from_html(doc, content):
  296. frappe.flags.has_dataurl = False
  297. def _save_file(match):
  298. data = match.group(1)
  299. data = data.split("data:")[1]
  300. headers, content = data.split(",")
  301. mtype = headers.split(";")[0]
  302. if isinstance(content, str):
  303. content = content.encode("utf-8")
  304. if b"," in content:
  305. content = content.split(b",")[1]
  306. content = base64.b64decode(content)
  307. content = optimize_image(content, mtype)
  308. if "filename=" in headers:
  309. filename = headers.split("filename=")[-1]
  310. # decode filename
  311. if not isinstance(filename, str):
  312. filename = str(filename, 'utf-8')
  313. else:
  314. filename = get_random_filename(content_type=mtype)
  315. doctype = doc.parenttype if doc.parent else doc.doctype
  316. name = doc.parent or doc.name
  317. if doc.doctype == "Comment":
  318. doctype = doc.reference_doctype
  319. name = doc.reference_name
  320. # TODO fix this
  321. file_url = save_file(filename, content, doctype, name, decode=False).get("file_url")
  322. if not frappe.flags.has_dataurl:
  323. frappe.flags.has_dataurl = True
  324. return '<img src="{file_url}"'.format(file_url=file_url)
  325. if content:
  326. content = re.sub(r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content)
  327. return content
  328. def get_random_filename(extn=None, content_type=None):
  329. if extn:
  330. if not extn.startswith("."):
  331. extn = "." + extn
  332. elif content_type:
  333. extn = mimetypes.guess_extension(content_type)
  334. return random_string(7) + (extn or "")
  335. @frappe.whitelist(allow_guest=True)
  336. def validate_filename(filename):
  337. from frappe.utils import now_datetime
  338. timestamp = now_datetime().strftime(" %Y-%m-%d %H:%M:%S")
  339. fname = get_file_name(filename, timestamp)
  340. return fname
  341. @frappe.whitelist()
  342. def add_attachments(doctype, name, attachments):
  343. '''Add attachments to the given DocType'''
  344. if isinstance(attachments, str):
  345. attachments = json.loads(attachments)
  346. # loop through attachments
  347. files =[]
  348. for a in attachments:
  349. if isinstance(a, str):
  350. attach = frappe.db.get_value("File", {"name":a}, ["file_name", "file_url", "is_private"], as_dict=1)
  351. # save attachments to new doc
  352. f = save_url(attach.file_url, attach.file_name, doctype, name, "Home/Attachments", attach.is_private)
  353. files.append(f)
  354. return files