Repository: frappe/frappe
Pull request: file-api: migrate file api
Migrated file doctype APIs to files.py for streamlined access

closes #2800 

adds changes to the way file api functions. For example:

### Pythonic Way

The old call to save a file would require one to do the following:

```py
f = save_file(filename, filedata, doctype, docname,
              folder, decode_base64, is_private, docfield)
```

The new save function functions as an object of the File class:

```py
_file = frappe.get_doc({"doctype": "File",
                        "file_name": filename,
                        "attached_to_doctype": doctype,
                        "attached_to_name": docname,
                        "attached_to_field": docfield,
                        "content": filedata,
                        "decode": decode_base64,
                        "folder": folder,
                        "is_private": is_private})
_file.save()
```

This allows easier manipulation on files using a single file object, as opposed to manually accessing files through separate function calls each time a file needs to be manipulated.

### REST API

To consume File as a part of the REST API, create a session. I'll use `requests` as an example:

```py
import requests

sess = requests.Session()
# login to the user account
s.post("http://sitename.whatever/",
       data={'cmd': 'login', 'pwd': 'password', "usr": "email_id"},
       headers={'accept': 'application/json'})
```

This will create a requests session which you can then use to `POST` or `GET` requests.

#### POST

After creating a session, you can `POST` a File using requests by doing:

```py
s.post("http://sitename.whatever/api/resource/File",
       data=json.dumps({
                "file_name": filename,
                "attached_to_doctype": doctype,
                "attached_to_name": docname,
                "attached_to_field": docfield,
                "content": data,
                "decode": boolean,
                "folder": folder,
                "is_private": boolean}),
       headers={'accept': 'application/json'})
```

#### GET

Similarly, you can  perform `GET` requests by doing:

```py
s.get("http://sitename.whatever/api/resource/File")
```

For more information about consuming the API, refer the [documentation][doc].

[doc]: http://frappe.io/docs/user/en/guides/integration


Problem statement:
Feature Idea: File manager API
## Introduction

Frappe apps, notably ERPnext, use an internal database to save attached files to documents. This system serves it's purpose well and allows many users to attach files that need to be available to other users. However, more advanced document management requires users to manually copy files to other file sharing services, such as Dropbox, Google drive and Sharepoint. Now, I know that a more advanced file manager is planned for ERPnext, which would alleviate some concerns, but I believe that ERPnext and Frappe would benefit from a file management API, that would allow files stored in other sharing services to be attached to documents.

## Main concept

I had envisioned a module similar to the paypal integration, where users could input API credentials to their own file management system, they could input a url to which the requests would be sent and some settings to permit customization, such as a button color (blue for dropbox) and a custom logo that would show up on the button. This method would allow more flexibility and open up ERPnext and Frappe to more companies that require a more advanced document management solution.

## API endpoints

There would need to be hooks on all the events of the file upload window, with the option to prevent saving the files in the ERPnext database, reducing redundant saving. There would need to be an event when the window opens to retrieve attached files according to an ID that would be provided by the external file manager.

### GET
When a user opens a document, a `GET` request should be made to the external api with the credentials and the location of the document, to retrieve the corresponding files.

### POST
When a user attaches a document to the upload window, a `POST` request would be made to the external API that would contain the `file`, the attached `tags`, the `location` of the document and the `user` that attached the file. As much information as possible about the file is essential, since the external API will be able to better classify the file that is being attached.

### DELETE
When the user removes a file from the last document it is attached to, a `DELETE` request will be made to the external API that would contain the file ID. There should be an option in the settings to retain the file when it is deleted to prevent this behaviour.

### UPDATE
When a user changes tags on a document, an `UPDATE` request will be made to the external API to update the tags. The request would only need to contain the file ID and the full list of tags not only the new ones.

## Settings
The settings page would be used to set up the external file manager API and would require the following fields: 
`API url`: the URL that requests will be made against. 
`API key`: the authentication key that will be used to authenticate requests against the file manager. 
`Signature`: a signature that will be used to sign requests and provide further authentication.
`Service Name`: the text that will be used on the button to attach files. 
`Service logo`: an optional logo that would be shown on the button. 
`Button Color`: a custom color for the button would be nice.

## Conclusion
I had attempted to create this module, as I find it would be quite useful for the majority of ERPnext users, but _I lack the python knowledge and understanding of the Frappe framework to create this_ intricate manipulation in the background processes of Frappe.

I think this module would add a significant amount of value to ERPnext, while not being extremely complex to implement, I hope that you agree! I have put *significant* effort in planning this module and am available at all times to provide insight to the person that will work on this.

Thanks for reading.

Hints from issue discussion:
This could be : `/api/file/{filename}` similar to `/api/resource/{doctype}`

Edit: Happy to help if you want to work on it.
I think that would be a good api endpoint. Like I mentioned, my python is not sufficiently advanced to work on this alone, but I could help someone more advanced get to the end result!

> On Mar 6, 2017, at 3:21 AM, Rushabh Mehta <notifications@github.com> wrote:
> 
> This could be : /api/file/{filename} similar to /api/resource/{doctype}
> 
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub, or mute the thread.
> 

Has anyone had the time to look at this issue? :v: 
Its good if possible to directly scan document with usb or network scanner  from ERPNEXT and attach to document .

Commit message: file-api: add save test for base64 encoded file

Signed-off-by: Chinmay Pai <chinmaydpai@gmail.com>

Patch to review:
diff --git a/frappe/client.py b/frappe/client.py
index 454b0a79e188..0be029599adf 100644
--- a/frappe/client.py
+++ b/frappe/client.py
@@ -9,7 +9,6 @@
 import json, os
 
 from six import iteritems, string_types, integer_types
-from frappe.utils.file_manager import save_file
 
 '''
 Handle RESTful requests that are mapped to the `/api/resource` route.
@@ -351,10 +350,20 @@ def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder
 	if not doc.has_permission():
 		frappe.throw(_("Not permitted"), frappe.PermissionError)
 
-	f = save_file(filename, filedata, doctype, docname, folder, decode_base64, is_private, docfield)
+	_file = frappe.get_doc({
+		"doctype": "File",
+		"file_name": filename,
+		"attached_to_doctype": doctype,
+		"attached_to_name": docname,
+		"attached_to_field": docfield,
+		"folder": folder,
+		"is_private": is_private,
+		"content": filedata,
+		"decode": decode_base64})
+	_file.save()
 
 	if docfield and doctype:
-		doc.set(docfield, f.file_url)
+		doc.set(docfield, _file.file_url)
 		doc.save()
 
 	return f.as_dict()
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index d84257a5fc98..48c8dfcae1cb 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -10,7 +10,6 @@
 from frappe.core.utils import get_parent_doc
 from frappe.utils import (get_url, get_formatted_email, cint,
   validate_email_add, split_emails, time_diff_in_seconds, parse_addr, get_datetime)
-from frappe.utils.file_manager import get_file
 from frappe.email.queue import check_email_limit
 from frappe.utils.scheduler import log
 from frappe.email.email_body import get_message_id
@@ -281,7 +280,8 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
 				# is it a filename?
 				try:
 					# keep this for error handling
-					file = get_file(a)
+					_file = frappe.get_doc("File", {"file_name": a})
+					_file.get_content()
 					# these attachments will be attached on-demand
 					# and won't be stored in the message
 					doc.attachments.append({"fid": a})
@@ -393,8 +393,6 @@ def get_bcc(doc, recipients=None, fetched_from_email_account=False):
 
 def add_attachments(name, attachments):
 	'''Add attachments to the given Communiction'''
-	from frappe.utils.file_manager import save_url
-
 	# loop through attachments
 	for a in attachments:
 		if isinstance(a, string_types):
@@ -402,8 +400,14 @@ def add_attachments(name, attachments):
 				["file_name", "file_url", "is_private"], as_dict=1)
 
 			# save attachments to new doc
-			save_url(attach.file_url, attach.file_name, "Communication", name,
-				"Home/Attachments", attach.is_private)
+			_file = frappe.get_doc({
+				"doctype": "File",
+				"file_url": attach.file_url,
+				"attached_to_doctype": "Communication",
+				"attached_to_name": name,
+				"folder": "Home/Attachments"})
+			_file.save()
+
 
 def filter_email_list(doc, email_list, exclude, is_cc=False, is_bcc=False):
 	# temp variables
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index 299eb1de9dec..037724fae1de 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -12,7 +12,6 @@
 
 from frappe.utils.csvutils import getlink
 from frappe.utils.dateutils import parse_date
-from frappe.utils.file_manager import save_url
 
 from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_url_to_form
 from six import text_type, string_types
@@ -265,14 +264,22 @@ def attach_file_to_doc(doctype, docname, file_url):
 			# file is already attached
 			return
 
-		save_url(file_url, None, doctype, docname, "Home/Attachments", 0)
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_url": file_url,
+			"attached_to_name": docname,
+			"attached_to_doctype": doctype,
+			"attached_to_field": 0,
+			"folder": "Home/Attachments"})
+		_file.save()
+
 
 	# header
 	filename, file_extension = ['','']
 	if not rows:
-		from frappe.utils.file_manager import get_file # get_file_doc
-		fname, fcontent = get_file(data_import_doc.import_file)
-		filename, file_extension = os.path.splitext(fname)
+		_file = frappe.get_doc("File", {"file_url": data_import_doc.import_file})
+		fcontent = _file.get_content()
+		filename, file_extension = os.path.splitext(_file.file_name)
 
 		if file_extension == '.xlsx' and from_data_import == 'Yes':
 			from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
@@ -455,7 +462,6 @@ def publish_progress(achieved, reload=False):
 		if error_flag and data_import_doc.skip_errors and len(data) != len(data_rows_with_error):
 			import_status = "Partially Successful"
 			# write the file with the faulty row
-			from frappe.utils.file_manager import save_file
 			file_name = 'error_' + filename + file_extension
 			if file_extension == '.xlsx':
 				from frappe.utils.xlsxutils import make_xlsx
@@ -464,9 +470,15 @@ def publish_progress(achieved, reload=False):
 			else:
 				from frappe.utils.csvutils import to_csv
 				file_data = to_csv(data_rows_with_error)
-			error_data_file = save_file(file_name, file_data, "Data Import",
-				data_import_doc.name,  "Home/Attachments")
-			data_import_doc.error_file = error_data_file.file_url
+			_file = frappe.get_doc({
+				"doctype": "File",
+				"file_name": file_name,
+				"attached_to_doctype": "Data Import",
+				"attached_to_name": data_import_doc.name,
+				"folder": "Home/Attachments",
+				"content": file_data})
+			_file.save()
+			data_import_doc.error_file = _file.file_url
 
 		elif error_flag:
 			import_status = "Failed"
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index ed48cef0101a..7a45aed5d2c9 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -11,24 +11,36 @@
 import frappe
 import json
 import os
+import base64
+import re
+import hashlib
+import mimetypes
+import io
 import shutil
 import requests
 import requests.exceptions
-import mimetypes, imghdr
+import imghdr
 
-from frappe.utils.file_manager import delete_file_data_content, get_content_hash, get_random_filename
+from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint
 from frappe import _
+from frappe import conf
 from frappe.utils.nestedset import NestedSet
-from frappe.utils import strip, get_files_path
+from frappe.utils import strip
 from PIL import Image, ImageOps
 from six import StringIO, string_types
 from six.moves.urllib.parse import unquote
+from six import text_type, PY2
 import zipfile
 
+class MaxFileSizeReachedError(frappe.ValidationError):
+	pass
+
+
 class FolderNotEmpty(frappe.ValidationError): pass
 
 exclude_from_linked_with = True
 
+
 class File(NestedSet):
 	nsm_parent_field = 'folder'
 	no_feed_on_delete = True
@@ -36,6 +48,10 @@ class File(NestedSet):
 	def before_insert(self):
 		frappe.local.rollback_observers.append(self)
 		self.set_folder_name()
+		self.content = self.get("content", None)
+		self.decode = self.get("decode", False)
+		if self.content:
+			self.save_file(content=self.content, decode=self.decode)
 
 	def get_name_based_on_parent_folder(self):
 		path = get_breadcrumbs(self.folder)
@@ -69,10 +85,12 @@ def validate(self):
 		self.validate_folder()
 
 		if not self.flags.ignore_file_validate:
-			self.validate_file()
+			if not self.is_folder:
+				self.validate_file()
 			self.generate_content_hash()
 
 		self.set_folder_size()
+		self.validate_url()
 
 		if frappe.db.exists('File', {'name': self.name, 'is_folder': 0}):
 			old_file_url = self.file_url
@@ -104,7 +122,8 @@ def validate(self):
 							break
 					self.attached_to_field = field_name
 				if self.attached_to_field:
-					frappe.db.set_value(self.attached_to_doctype, self.attached_to_name, self.attached_to_field, self.file_url)
+					frappe.db.set_value(self.attached_to_doctype, self.attached_to_name,
+						self.attached_to_field, self.file_url)
 
 
 	def set_folder_size(self):
@@ -139,18 +158,16 @@ def set_folder_name(self):
 	def validate_folder(self):
 		if not self.is_home_folder and not self.folder and \
 			not self.flags.ignore_folder_validate:
-			frappe.throw(_("Folder is mandatory"))
+			self.folder = "Home"
 
 	def validate_file(self):
 		"""Validates existence of public file
 		TODO: validate for private file
 		"""
-		if (self.file_url or "").startswith("/files/"):
-			if not self.file_name:
-				self.file_name = self.file_url.split("/files/")[-1]
+		full_path = self.get_full_path()
 
-			if not os.path.exists(get_files_path(frappe.as_unicode(self.file_name.lstrip("/")))):
-				frappe.throw(_("File {0} does not exist").format(self.file_url), IOError)
+		if not os.path.exists(full_path):
+			frappe.throw(_("File {0} does not exist").format(self.file_url), IOError)
 
 	def validate_duplicate_entry(self):
 		if not self.flags.ignore_duplicate_entry_error and not self.is_folder:
@@ -165,7 +182,8 @@ def validate_duplicate_entry(self):
 					self.attached_to_name))
 			if len(n_records) > 0:
 				self.duplicate_entry = n_records[0][0]
-				frappe.throw(frappe._("Same file has already been attached to the record"), frappe.DuplicateEntryError)
+				frappe.throw(frappe._("Same file has already been attached to the record"),
+					frappe.DuplicateEntryError)
 
 	def generate_content_hash(self):
 		if self.content_hash or not self.file_url:
@@ -185,7 +203,7 @@ def on_trash(self):
 		self.check_folder_is_empty()
 		self.check_reference_doc_permission()
 		super(File, self).on_trash()
-		self.delete_file()
+		self.call_delete_file()
 
 	def make_thumbnail(self, set_as_thumbnail=True, width=300, height=300, suffix="small", crop=False):
 		if self.file_url:
@@ -246,19 +264,20 @@ def check_reference_doc_permission(self):
 				if not self.flags.ignore_permissions and \
 					not frappe.has_permission(self.attached_to_doctype,
 						"write", self.attached_to_name):
-					frappe.throw(frappe._("Cannot delete file as it belongs to {0} {1} for which you do not have permissions").format(self.attached_to_doctype, self.attached_to_name),
+					frappe.throw(frappe._(
+						"Cannot delete file as it belongs to {0} {1} for which you do not have permissions").format(
+						self.attached_to_doctype, self.attached_to_name),
 						frappe.PermissionError)
 			except frappe.DoesNotExistError:
 				pass
 
-	def delete_file(self):
+	def call_delete_file(self):
 		"""If file not attached to any other record, delete it"""
 		if self.file_name and self.content_hash and (not frappe.db.count("File",
 			{"content_hash": self.content_hash, "name": ["!=", self.name]})):
-				delete_file_data_content(self)
-
+				self.delete_file_data_content()
 		elif self.file_url:
-			delete_file_data_content(self, only_thumbnail=True)
+			self.delete_file_data_content(only_thumbnail=True)
 
 	def on_rollback(self):
 		self.flags.on_rollback = True
@@ -293,6 +312,248 @@ def unzip(self):
 
 		frappe.delete_doc('File', self.name)
 
+
+	def get_file_url(self):
+		data = frappe.db.get_value("File", self.file_data_name, ["file_name", "file_url"], as_dict=True)
+		return data.file_url or data.file_name
+
+
+	def upload(self):
+		# get record details
+		self.attached_to_doctype = frappe.form_dict.doctype
+		self.attached_to_name = frappe.form_dict.docname
+		self.attached_to_field = frappe.form_dict.docfield
+		self.file_url = frappe.form_dict.file_url
+		self.file_name = frappe.form_dict.filename
+		frappe.form_dict.is_private = cint(frappe.form_dict.is_private)
+
+		if not self.file_name and not self.file_url:
+			frappe.msgprint(_("Please select a file or url"),
+				raise_exception=True)
+
+		file_doc = self.get_file_doc()
+
+		comment = {}
+		if self.attached_to_doctype and self.attached_to_name:
+			comment = frappe.get_doc(self.attached_to_doctype, self.attached_to_name).add_comment("Attachment",
+			_	("added {0}").format("<a href='{file_url}' target='_blank'>{file_name}</a>{icon}".format(**{
+					"icon": ' <i class="fa fa-lock text-warning"></i>' \
+						if file_doc.is_private else "",
+					"file_url": file_doc.file_url.replace("#", "%23") \
+						if file_doc.file_name else file_doc.file_url,
+					"file_name": file_doc.file_name or file_doc.file_url
+				})))
+
+		return {
+			"name": file_doc.name,
+			"file_name": file_doc.file_name,
+			"file_url": file_doc.file_url,
+			"is_private": file_doc.is_private,
+			"comment": comment.as_dict() if comment else {}
+		}
+
+	def get_content(self):
+		"""Returns [`file_name`, `content`] for given file name `fname`"""
+		if self.get('content'):
+			return self.content
+		file_path = self.get_full_path()
+
+		# read the file
+		if PY2:
+			with open(encode(file_path)) as f:
+				content = f.read()
+		else:
+			with io.open(encode(file_path), mode='rb') as f:
+				content = f.read()
+				try:
+					# for plain text files
+					content = content.decode()
+				except UnicodeDecodeError:
+					# for .png, .jpg, etc
+					pass
+
+		return content
+
+	def get_full_path(self):
+		"""Returns file path from given file name"""
+
+		file_path = self.file_url or self.file_name
+
+		if "/" not in file_path:
+			file_path = "/files/" + file_path
+
+		if file_path.startswith("/private/files/"):
+			file_path = get_files_path(*file_path.split("/private/files/", 1)[1].split("/"), is_private=1)
+
+		elif file_path.startswith("/files/"):
+			file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/"))
+
+		else:
+			frappe.throw(_("There is some problem with the file url: {0}").format(file_path))
+
+		return file_path
+
+	def write_file(self):
+		"""write file to disk with a random name (to compare)"""
+		file_path = get_files_path(is_private=self.is_private)
+
+		# create directory (if not exists)
+		frappe.create_folder(file_path)
+		# write the file
+		self.content = self.get_content()
+		if isinstance(self.content, text_type):
+			self.content = self.content.encode()
+		with open(os.path.join(file_path.encode('utf-8'), self.file_name.encode('utf-8')), 'wb+') as f:
+			f.write(self.content)
+
+		return get_files_path(self.file_name, is_private=self.is_private)
+
+	def get_file_doc(self):
+		'''returns File object (Document) from given parameters or form_dict'''
+		r = frappe.form_dict
+
+		if self.file_url is None: self.file_url = r.file_url
+		if self.file_name is None: self.file_name = r.file_name
+		if self.attached_to_doctype is None: self.attached_to_doctype = r.doctype
+		if self.attached_to_name is None: self.attached_to_name = r.docname
+		if self.attached_to_field is None: self.attached_to_field = r.docfield
+		if self.folder is None: self.folder = r.folder
+		if self.is_private is None: self.is_private = r.is_private
+
+		if r.filedata:
+			file_doc = self.save_uploaded()
+
+		elif r.file_url:
+			file_doc = self.save()
+
+		return file_doc
+
+
+	def save_uploaded(self):
+		self.content = self.get_uploaded_content()
+		if self.content:
+			return self.save()
+		else:
+			raise Exception
+
+
+	def validate_url(self, df=None):
+		if self.file_url:
+			if not self.file_url.startswith(("http://", "https://", "/files/", "/private/files/")):
+				frappe.throw("URL must start with 'http://' or 'https://'")
+				return
+
+			self.file_url = unquote(self.file_url)
+			self.file_size = frappe.form_dict.file_size
+
+
+	def get_uploaded_content(self):
+		# should not be unicode when reading a file, hence using frappe.form
+		if 'filedata' in frappe.form_dict:
+			if "," in frappe.form_dict.filedata:
+				frappe.form_dict.filedata = frappe.form_dict.filedata.rsplit(",", 1)[1]
+			frappe.uploaded_content = base64.b64decode(frappe.form_dict.filedata)
+			return frappe.uploaded_content
+		elif self.content:
+			return self.content
+		frappe.msgprint(_('No file attached'))
+		return None
+
+
+	def save_file(self, content=None, decode=False):
+		file_exists = False
+		self.content = content
+		if decode:
+			if isinstance(content, text_type):
+				self.content = content.encode("utf-8")
+
+			if b"," in self.content:
+				self.content = self.content.split(b",")[1]
+			self.content = base64.b64decode(self.content)
+
+		if not self.is_private:
+			self.is_private = 0
+		self.file_size = self.check_max_file_size()
+		self.content_hash = get_content_hash(self.content)
+		self.content_type = mimetypes.guess_type(self.file_name)[0]
+
+		_file = frappe.get_value("File", {"content_hash": self.content_hash}, ["file_url"])
+		if _file:
+			self.file_url  = _file
+			file_exists = True
+
+		if not file_exists:
+			if os.path.exists(encode(get_files_path(self.file_name))):
+				self.file_name = get_file_name(self.file_name, self.content_hash[-6:])
+
+			call_hook_method("before_write_file", file_size=self.file_size)
+			write_file_method = get_hook_method('write_file')
+			if write_file_method:
+				return write_file_method(self)
+			return self.save_file_on_filesystem()
+
+
+	def save_file_on_filesystem(self):
+		fpath = self.write_file()
+
+		if self.is_private:
+			self.file_url = "/private/files/{0}".format(self.file_name)
+		else:
+			self.file_url = "/files/{0}".format(self.file_name)
+
+		return {
+			'file_name': os.path.basename(fpath),
+			'file_url': self.file_url
+		}
+
+	def get_file_data_from_hash(self):
+		for name in frappe.db.sql_list("select name from `tabFile` where content_hash=%s and is_private=%s",
+			(self.content_hash, self.is_private)):
+			b = frappe.get_doc('File', name)
+			return {k: b.get(k) for k in frappe.get_hooks()['write_file_keys']}
+		return False
+
+
+	def check_max_file_size(self):
+		max_file_size = get_max_file_size()
+		file_size = len(self.content)
+
+		if file_size > max_file_size:
+			frappe.msgprint(_("File size exceeded the maximum allowed size of {0} MB").format(
+				max_file_size / 1048576),
+				raise_exception=MaxFileSizeReachedError)
+
+		return file_size
+
+
+	def delete_file_data_content(self, only_thumbnail=False):
+		method = get_hook_method('delete_file_data_content')
+		if method:
+			method(self, only_thumbnail=only_thumbnail)
+		else:
+			self.delete_file_from_filesystem(only_thumbnail=only_thumbnail)
+
+
+	def delete_file_from_filesystem(self, only_thumbnail=False):
+		"""Delete file, thumbnail from File document"""
+		if only_thumbnail:
+			delete_file(self.thumbnail_url)
+		else:
+			delete_file(self.file_url)
+			delete_file(self.thumbnail_url)
+
+
+	def check_file_permission(self):
+		for file in frappe.get_all("File", filters={"file_url": self.file_url, "is_private": 1},
+			fields=["name", "attached_to_doctype", "attached_to_name"]):
+
+			if (frappe.has_permission("File", ptype="read", doc=file.name)
+				or frappe.has_permission(file.attached_to_doctype, ptype="read", doc=file.attached_to_name)):
+				return True
+
+		raise frappe.PermissionError
+
+
 def on_doctype_update():
 	frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"])
 	frappe.db.add_index("File", ["lft", "rgt"])
@@ -334,6 +595,7 @@ def create_new_folder(file_name, folder):
 
 @frappe.whitelist()
 def move_file(file_list, new_parent, old_parent):
+
 	if isinstance(file_list, string_types):
 		file_list = json.loads(file_list)
 
@@ -421,14 +683,171 @@ def get_web_image(file_url):
 
 	return image, filename, extn
 
-def check_file_permission(file_url):
-	for file in frappe.get_all("File", filters={"file_url": file_url, "is_private": 1}, fields=["name", "attached_to_doctype", "attached_to_name"]):
 
-		if (frappe.has_permission("File", ptype="read", doc=file.name)
-			or frappe.has_permission(file.attached_to_doctype, ptype="read", doc=file.attached_to_name)):
-			return True
+def delete_file(path):
+	"""Delete file from `public folder`"""
+	if path:
+		if ".." in path.split("/"):
+			frappe.msgprint(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path))
+
+		parts = os.path.split(path.strip("/"))
+		if parts[0]=="files":
+			path = frappe.utils.get_site_path("public", "files", parts[-1])
+
+		else:
+			path = frappe.utils.get_site_path("private", "files", parts[-1])
+
+		path = encode(path)
+		if os.path.exists(path):
+			os.remove(path)
+
+
+def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_delete=False):
+	"""Remove file and File entry"""
+	file_name = None
+	if not (attached_to_doctype and attached_to_name):
+		attached = frappe.db.get_value("File", fid,
+			["attached_to_doctype", "attached_to_name", "file_name"])
+		if attached:
+			attached_to_doctype, attached_to_name, file_name = attached
+
+	ignore_permissions, comment = False, None
+	if attached_to_doctype and attached_to_name and not from_delete:
+		doc = frappe.get_doc(attached_to_doctype, attached_to_name)
+		ignore_permissions = doc.has_permission("write") or False
+		if frappe.flags.in_web_form:
+			ignore_permissions = True
+		if not file_name:
+			file_name = frappe.db.get_value("File", fid, "file_name")
+		comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name))
+		frappe.delete_doc("File", fid, ignore_permissions=ignore_permissions)
+
+	return comment
+
+
+def get_max_file_size():
+	return conf.get('max_file_size') or 10485760
+
+
+def remove_all(dt, dn, from_delete=False):
+	"""remove all files in a transaction"""
+	try:
+		for fid in frappe.db.sql_list("""select name from `tabFile` where
+			attached_to_doctype=%s and attached_to_name=%s""", (dt, dn)):
+			remove_file(fid=fid, attached_to_doctype=dt, attached_to_name=dn, from_delete=from_delete)
+	except Exception as e:
+		if e.args[0]!=1054: raise # (temp till for patched)
+
+
+def remove_file_by_url(file_url, doctype=None, name=None):
+	if doctype and name:
+		fid = frappe.db.get_value("File", {
+			"file_url": file_url,
+			"attached_to_doctype": doctype,
+			"attached_to_name": name})
+	else:
+		fid = frappe.db.get_value("File", {"file_url": file_url})
+
+	if fid:
+		return remove_file(fid=fid)
+
+
+def get_content_hash(content):
+	if isinstance(content, text_type):
+		content = content.encode()
+	return hashlib.md5(content).hexdigest()
+
+
+def get_file_name(fname, optional_suffix):
+	# convert to unicode
+	fname = cstr(fname)
+
+	f = fname.rsplit('.', 1)
+	if len(f) == 1:
+		partial, extn = f[0], ""
+	else:
+		partial, extn = f[0], "." + f[1]
+	return '{partial}{suffix}{extn}'.format(partial=partial, extn=extn, suffix=optional_suffix)
+
+
+@frappe.whitelist()
+def download_file(file_url):
+	"""
+	Download file using token and REST API. Valid session or
+	token is required to download private files.
+
+	Method : GET
+	Endpoint : frappe.core.doctype.file.file.download_file
+	URL Params : file_name = /path/to/file relative to site path
+	"""
+	file_doc = frappe.get_doc("File", {"file_url": file_url})
+	file_doc.check_permission("read")
+	path = os.path.join(get_files_path(), os.path.basename(file_url))
+
+	with open(path, "rb") as fileobj:
+		filedata = fileobj.read()
+	frappe.local.response.filename = os.path.basename(file_url)
+	frappe.local.response.filecontent = filedata
+	frappe.local.response.type = "download"
+
+def extract_images_from_doc(doc, fieldname):
+	content = doc.get(fieldname)
+	content = extract_images_from_html(doc, content)
+	if frappe.flags.has_dataurl:
+		doc.set(fieldname, content)
+
+
+def extract_images_from_html(doc, content):
+	frappe.flags.has_dataurl = False
+
+	def _save_file(match):
+		data = match.group(1)
+		data = data.split("data:")[1]
+		headers, content = data.split(",")
+
+		if "filename=" in headers:
+			filename = headers.split("filename=")[-1]
+
+			# decode filename
+			if not isinstance(filename, text_type):
+				filename = text_type(filename, 'utf-8')
+		else:
+			mtype = headers.split(";")[0]
+			filename = get_random_filename(content_type=mtype)
+
+		doctype = doc.parenttype if doc.parent else doc.doctype
+		name = doc.parent or doc.name
+
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": filename,
+			"attached_to_doctype": doctype,
+			"attached_to_name": name,
+			"content": content,
+			"decode": True})
+		_file.save()
+		file_url = _file.file_url
+		if not frappe.flags.has_dataurl:
+			frappe.flags.has_dataurl = True
+
+		return '<img src="{file_url}"'.format(file_url=file_url)
+
+	if content:
+		content = re.sub('<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content)
+
+	return content
+
+
+def get_random_filename(extn=None, content_type=None):
+	if extn:
+		if not extn.startswith("."):
+			extn = "." + extn
+
+	elif content_type:
+		extn = mimetypes.guess_extension(content_type)
+
+	return random_string(7) + (extn or "")
 
-	raise frappe.PermissionError
 
 @frappe.whitelist()
 def unzip_file(name):
@@ -436,6 +855,7 @@ def unzip_file(name):
 	file_obj = frappe.get_doc('File', name)
 	file_obj.unzip()
 
+
 @frappe.whitelist()
 def get_attached_images(doctype, names):
 	'''get list of image urls attached in form
@@ -456,3 +876,11 @@ def get_attached_images(doctype, names):
 		out[i.docname].append(i.file_url)
 
 	return out
+
+
+@frappe.whitelist()
+def validate_filename(filename):
+	from frappe.utils import now_datetime
+	timestamp = now_datetime().strftime(" %Y-%m-%d %H:%M:%S")
+	fname = get_file_name(filename, timestamp)
+	return fname
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 05dc677df63f..dd1a7e7718c9 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -3,33 +3,191 @@
 # See license.txt
 from __future__ import unicode_literals
 
+import base64
 import frappe
+import os
 import unittest
-from frappe.utils.file_manager import save_file, get_files_path
 from frappe import _
 from frappe.core.doctype.file.file import move_file
+from frappe.utils import get_files_path
 # test_records = frappe.get_test_records('File')
 
+test_content1 = 'Hello'
+test_content2 = 'Hello World'
+
+
+def make_test_doc():
+	d = frappe.new_doc('ToDo')
+	d.description = 'Test'
+	d.save()
+	return d.doctype, d.name
+
+
+class TestSimpleFile(unittest.TestCase):
+
+
+	def setUp(self):
+		self.attached_to_doctype, self.attached_to_docname = make_test_doc()
+		self.test_content = test_content1
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "test1.txt",
+			"attached_to_doctype": self.attached_to_doctype,
+			"attached_to_name": self.attached_to_docname,
+			"content": self.test_content})
+		_file.save()
+		self.saved_file_url = _file.file_url
+
+
+	def test_save(self):
+		_file = frappe.get_doc("File", {"file_url": self.saved_file_url})
+		content = _file.get_content()
+		self.assertEqual(content, self.test_content)
+
+
+	def tearDown(self):
+		# File gets deleted on rollback, so blank
+		pass
+
+
+class TestBase64File(unittest.TestCase):
+
+
+	def setUp(self):
+		self.attached_to_doctype, self.attached_to_docname = make_test_doc()
+		self.test_content = base64.b64encode(test_content1.encode('utf-8'))
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "test_base64.txt",
+			"attached_to_doctype": self.attached_to_doctype,
+			"attached_to_docname": self.attached_to_docname,
+			"content": self.test_content,
+			"decode": True})
+		_file.save()
+		self.saved_file_url = _file.file_url
+
+
+	def test_saved_content(self):
+		_file = frappe.get_doc("File", {"file_url": self.saved_file_url})
+		content = _file.get_content()
+		self.assertEqual(content, test_content1)
+
+
+	def tearDown(self):
+		# File gets deleted on rollback, so blank
+		pass
+
+
+class TestSameFileName(unittest.TestCase):
+
+
+	def setUp(self):
+		self.attached_to_doctype, self.attached_to_docname = make_test_doc()
+		self.test_content1 = test_content1
+		self.test_content2 = test_content2
+		_file1 = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "testing.txt",
+			"attached_to_doctype": self.attached_to_doctype,
+			"attached_to_name": self.attached_to_docname,
+			"content": self.test_content1})
+		_file1.save()
+		_file2 = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "testing.txt",
+			"attached_to_doctype": self.attached_to_doctype,
+			"attached_to_name": self.attached_to_docname,
+			"content": self.test_content2})
+		_file2.save()
+		self.saved_file_url1 = _file1.file_url
+		self.saved_file_url2 = _file2.file_url
+
+
+	def test_saved_content(self):
+		_file = frappe.get_doc("File", {"file_url": self.saved_file_url1})
+		content1 = _file.get_content()
+		self.assertEqual(content1, self.test_content1)
+		_file = frappe.get_doc("File", {"file_url": self.saved_file_url2})
+		content2 = _file.get_content()
+		self.assertEqual(content2, self.test_content2)
+
+
+	def tearDown(self):
+		# File gets deleted on rollback, so blank
+		pass
+
+
+class TestSameContent(unittest.TestCase):
+
+
+	def setUp(self):
+		self.attached_to_doctype1, self.attached_to_docname1 = make_test_doc()
+		self.attached_to_doctype2, self.attached_to_docname2 = make_test_doc()
+		self.test_content1 = test_content1
+		self.test_content2 = test_content1
+		self.orig_filename = 'hello.txt'
+		self.dup_filename = 'hello2.txt'
+		_file1 = frappe.get_doc({
+			"doctype": "File",
+			"file_name": self.orig_filename,
+			"attached_to_doctype": self.attached_to_doctype1,
+			"attached_to_name": self.attached_to_docname1,
+			"content": self.test_content1})
+		_file1.save()
+
+		_file2 = frappe.get_doc({
+			"doctype": "File",
+			"file_name": self.dup_filename,
+			"attached_to_doctype": self.attached_to_doctype2,
+			"attached_to_name": self.attached_to_docname2,
+			"content": self.test_content2})
+
+		_file2.save()
+
+
+	def test_saved_content(self):
+		self.assertFalse(os.path.exists(get_files_path(self.dup_filename)))
+
+
+	def tearDown(self):
+		# File gets deleted on rollback, so blank
+		pass
+
+
 class TestFile(unittest.TestCase):
+
+
 	def setUp(self):
 		self.delete_test_data()
 		self.upload_file()
 
+
 	def tearDown(self):
 		try:
 			frappe.get_doc("File", {"file_name": "file_copy.txt"}).delete()
 		except frappe.DoesNotExistError:
 			pass
 
+
 	def delete_test_data(self):
 		for f in frappe.db.sql('''select name, file_name from tabFile where
 			is_home_folder = 0 and is_attachments_folder = 0 order by rgt-lft asc'''):
 			frappe.delete_doc("File", f[0])
 
+
 	def upload_file(self):
-		self.saved_file = save_file('file_copy.txt', "Testing file copy example.",\
-			 "", "", self.get_folder("Test Folder 1", "Home").name)
-		self.saved_filename = get_files_path(self.saved_file.file_name)
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "file_copy.txt",
+			"attached_to_name": "",
+			"attached_to_doctype": "",
+			"folder": self.get_folder("Test Folder 1", "Home").name,
+			"content": "Testing file copy example."})
+		_file.save()
+		self.saved_folder = _file.folder
+		self.saved_name = _file.name
+		self.saved_filename = get_files_path(_file.file_name)
+
 
 	def get_folder(self, folder_name, parent_folder="Home"):
 		return frappe.get_doc({
@@ -39,30 +197,39 @@ def get_folder(self, folder_name, parent_folder="Home"):
 			"folder": _(parent_folder)
 		}).insert()
 
+
 	def tests_after_upload(self):
-		self.assertEqual(self.saved_file.folder, _("Home/Test Folder 1"))
+		self.assertEqual(self.saved_folder, _("Home/Test Folder 1"))
 
 		folder_size = frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size")
-		saved_file_size = frappe.db.get_value("File", self.saved_file.name, "file_size")
+		saved_file_size = frappe.db.get_value("File", self.saved_name, "file_size")
 
 		self.assertEqual(folder_size, saved_file_size)
 
+
 	def test_file_copy(self):
 		folder = self.get_folder("Test Folder 2", "Home")
 
-		file = frappe.get_doc("File", {"file_name":"file_copy.txt"})
+		file = frappe.get_doc("File", {"file_name": "file_copy.txt"})
 		move_file([{"name": file.name}], folder.name, file.folder)
-		file = frappe.get_doc("File", {"file_name":"file_copy.txt"})
+		file = frappe.get_doc("File", {"file_name": "file_copy.txt"})
 
 		self.assertEqual(_("Home/Test Folder 2"), file.folder)
 		self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 2"), "file_size"), file.file_size)
 		self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size"), 0)
 
+
 	def test_folder_copy(self):
 		folder = self.get_folder("Test Folder 2", "Home")
 		folder = self.get_folder("Test Folder 3", "Home/Test Folder 2")
-
-		self.saved_file = save_file('folder_copy.txt', "Testing folder copy example.", "", "", folder.name)
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "folder_copy.txt",
+			"attached_to_name": "",
+			"attached_to_doctype": "",
+			"folder": folder.name,
+			"content": "Testing folder copy example"})
+		_file.save()
 
 		move_file([{"name": folder.name}], 'Home/Test Folder 1', folder.folder)
 
@@ -75,29 +242,39 @@ def test_folder_copy(self):
 		self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size"), file.file_size)
 		self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 2"), "file_size"), 0)
 
-	def test_non_parent_folder(self):
+
+	def test_default_folder(self):
 		d = frappe.get_doc({
 			"doctype": "File",
 			"file_name": _("Test_Folder"),
 			"is_folder": 1
 		})
+		d.save()
+		self.assertEqual(d.folder, "Home")
 
-		self.assertRaises(frappe.ValidationError, d.save)
 
 	def test_on_delete(self):
-		file = frappe.get_doc("File", {"file_name":"file_copy.txt"})
+		file = frappe.get_doc("File", {"file_name": "file_copy.txt"})
 		file.delete()
 
 		self.assertEqual(frappe.db.get_value("File", _("Home/Test Folder 1"), "file_size"), 0)
 
 		folder = self.get_folder("Test Folder 3", "Home/Test Folder 1")
-		self.saved_file = save_file('folder_copy.txt', "Testing folder copy example.", "", "", folder.name)
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "folder_copy.txt",
+			"attached_to_name": "",
+			"attached_to_doctype": "",
+			"folder": folder.name,
+			"content": "Testing folder copy example"})
+		_file.save()
 
 		folder = frappe.get_doc("File", "Home/Test Folder 1/Test Folder 3")
 		self.assertRaises(frappe.ValidationError, folder.delete)
 
+
 	def test_file_upload_limit(self):
-		from frappe.utils.file_manager import MaxFileSizeReachedError
+		from frappe.core.doctype.file.file import MaxFileSizeReachedError
 		from frappe.limits import update_limits, clear_limit
 		from frappe import _dict
 
@@ -114,8 +291,15 @@ def test_file_upload_limit(self):
 		# Rebuild the frappe.local.conf to take up the changes from site_config
 		frappe.local.conf = _dict(frappe.get_site_config())
 
-		self.assertRaises(MaxFileSizeReachedError, save_file, '_test_max_space.txt',
-			'This files test for max space usage', "", "", self.get_folder("Test Folder 2", "Home").name)
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_name": "_test_max_space.txt",
+			"attached_to_name": "",
+			"attached_to_doctype": "",
+			"folder": self.get_folder("Test Folder 2", "Home").name,
+			"content": "This file tests for max space usage"})
+		self.assertRaises(MaxFileSizeReachedError,
+			_file.save)
 
 		# Scrub the site_config and rebuild frappe.local.conf
 		clear_limit("space")
diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py
index 3d8481c696fb..78afb3c1830b 100644
--- a/frappe/core/doctype/prepared_report/prepared_report.py
+++ b/frappe/core/doctype/prepared_report/prepared_report.py
@@ -11,10 +11,10 @@
 from frappe.model.document import Document
 from frappe.utils.background_jobs import enqueue
 from frappe.desk.query_report import generate_report_result, get_columns_dict
-from frappe.utils.file_manager import save_file, remove_all
+from frappe.core.doctype.file.file import remove_all
 from frappe.utils.csvutils import to_csv, read_csv_content_from_attached_file
 from frappe.desk.form.load import get_attachments
-from frappe.utils.file_manager import download_file
+from frappe.core.doctype.file.file import download_file
 
 
 class PreparedReport(Document):
@@ -61,15 +61,15 @@ def create_csv_file(columns, data, dt, dn):
 	csv_filename = '{0}.csv'.format(frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M"))
 	rows = [tuple(columns)] + data
 	encoded = base64.b64encode(frappe.safe_encode(to_csv(rows)))
-	# Call save_file function to upload and attach the file
-	save_file(
-		fname=csv_filename,
-		content=encoded,
-		dt=dt,
-		dn=dn,
-		folder=None,
-		decode=True,
-		is_private=False)
+	# Call save() file function to upload and attach the file
+	_file = frappe.get_doc({
+		"doctype": "File",
+		"file_name": csv_filename,
+		"attached_to_doctype": dt,
+		"attached_to_name": dn,
+		"content": encoded,
+		"decode": True})
+	_file.save()
 
 
 @frappe.whitelist()
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index a787e597fa6a..9f321cca0e3a 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -6,6 +6,7 @@
 import frappe.desk.form.meta
 import frappe.desk.form.load
 from frappe.utils.html_utils import clean_email_html
+from frappe.core.doctype.file.file import remove_file
 
 from frappe import _
 from six import string_types
@@ -13,9 +14,9 @@
 @frappe.whitelist()
 def remove_attach():
 	"""remove attachment"""
-	import frappe.utils.file_manager
 	fid = frappe.form_dict.get('fid')
-	return frappe.utils.file_manager.remove_file(fid)
+	return remove_file(fid=fid)
+
 
 @frappe.whitelist()
 def validate_link():
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index eef203ab04d4..82f44ee24951 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -7,7 +7,6 @@
 from frappe.utils import strip, cint
 from frappe.translate import (set_default_language, get_dict, send_translations)
 from frappe.geo.country_info import get_country_info
-from frappe.utils.file_manager import save_file
 from frappe.utils.password import update_password
 from werkzeug.useragents import UserAgent
 from . import install_fixtures
@@ -187,7 +186,15 @@ def update_user_name(args):
 		attach_user = args.get("attach_user").split(",")
 		if len(attach_user)==3:
 			filename, filetype, content = attach_user
-			fileurl = save_file(filename, content, "User", args.get("name"), decode=True).file_url
+			_file = frappe.get_doc({
+				"doctype": "File",
+				"file_name": filename,
+				"attached_to_doctype": "User",
+				"attached_to_name": args.get("name"),
+				"content": content,
+				"decode": True})
+			_file.save()
+			fileurl = _file.file_url
 			frappe.db.set_value("User", args.get("name"), "user_image", fileurl)
 
 	if args.get('name'):
diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py
index 6fa7ff30ed87..7cc1345a4267 100644
--- a/frappe/email/doctype/email_account/test_email_account.py
+++ b/frappe/email/doctype/email_account/test_email_account.py
@@ -8,7 +8,6 @@
 
 from frappe.core.doctype.communication.email import make
 from frappe.desk.form.load import get_attachments
-from frappe.utils.file_manager import delete_file_from_filesystem
 from frappe.email.doctype.email_account.email_account import notify_unreplied
 from datetime import datetime, timedelta
 
@@ -55,7 +54,7 @@ def test_incoming_with_attach(self):
 		frappe.db.sql("DELETE FROM `tabCommunication` WHERE sender='test_sender@example.com'")
 		existing_file = frappe.get_doc({'doctype': 'File', 'file_name': 'erpnext-conf-14.png'})
 		frappe.delete_doc("File", existing_file.name)
-		delete_file_from_filesystem(existing_file)
+		existing_file.delete_file_from_filesystem()
 
 		with open(os.path.join(os.path.dirname(__file__), "test_mails", "incoming-2.raw"), "r") as testfile:
 			test_mails = [testfile.read()]
@@ -73,7 +72,8 @@ def test_incoming_with_attach(self):
 		# cleanup
 		existing_file = frappe.get_doc({'doctype': 'File', 'file_name': 'erpnext-conf-14.png'})
 		frappe.delete_doc("File", existing_file.name)
-		delete_file_from_filesystem(existing_file)
+		existing_file.delete_file_from_filesystem()
+
 
 	def test_incoming_attached_email_from_outlook_plain_text_only(self):
 		frappe.db.sql("delete from tabCommunication where sender='test_sender@example.com'")
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index 8ab7ae5c857e..7cb7be1945b0 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -144,12 +144,12 @@ def set_message(self, message, mime_type='text/html', as_attachment=0, filename=
 
 	def attach_file(self, n):
 		"""attach a file from the `FileData` table"""
-		from frappe.utils.file_manager import get_file
-		res = get_file(n)
-		if not res:
+		_file = frappe.get_doc("File", {"file_name": n})
+		content = _file.get_content()
+		if not content:
 			return
 
-		self.add_attachment(res[0], res[1])
+		self.add_attachment(_file.file_name, content)
 
 	def add_attachment(self, fname, fcontent, content_type=None,
 		parent=None, content_id=None, inline=False):
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index d299658878ab..621a81e2cb27 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -11,7 +11,6 @@
 from frappe.utils.verified_command import get_signed_params, verify_request
 from html2text import html2text
 from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint
-from frappe.utils.file_manager import get_file
 from rq.timeouts import JobTimeoutException
 from frappe.utils.scheduler import log
 from six import text_type, string_types
@@ -531,9 +530,10 @@ def prepare_message(email, recipient, recipients_list):
 
 		fid = attachment.get("fid")
 		if fid:
-			fname, fcontent = get_file(fid)
+			_file = frappe.get_doc("File", {"file_name": fid})
+			fcontent = _file.get_content()
 			attachment.update({
-				'fname': fname,
+				'fname': _file.file_name,
 				'fcontent': fcontent,
 				'parent': msg_obj
 			})
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index f77e8a80d9b6..ffee8dc17af9 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -13,7 +13,7 @@
 from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now,
 	cint, cstr, strip, markdown, parse_addr)
 from frappe.utils.scheduler import log
-from frappe.utils.file_manager import get_random_filename, save_file, MaxFileSizeReachedError
+from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError
 
 class EmailSizeExceededError(frappe.ValidationError): pass
 class EmailTimeoutError(frappe.ValidationError): pass
@@ -523,12 +523,18 @@ def save_attachments_in_doc(self, doc):
 
 		for attachment in self.attachments:
 			try:
-				file_data = save_file(attachment['fname'], attachment['fcontent'],
-					doc.doctype, doc.name, is_private=1)
-				saved_attachments.append(file_data)
+				_file = frappe.get_doc({
+					"doctype": "File",
+					"file_name": attachment['fname'],
+					"attached_to_doctype": doc.doctype,
+					"attached_to_name": doc.name,
+					"is_private": 1,
+					"content": attachment['fcontent']})
+				_file.save()
+				saved_attachments.append(_file)
 
 				if attachment['fname'] in self.cid_map:
-					self.cid_map[file_data.name] = self.cid_map[attachment['fname']]
+					self.cid_map[_file.name] = self.cid_map[attachment['fname']]
 
 			except MaxFileSizeReachedError:
 				# WARNING: bypass max file size exception
diff --git a/frappe/handler.py b/frappe/handler.py
index 16138edf621d..7a040871ad04 100755
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -6,7 +6,6 @@
 from frappe import _
 import frappe.utils
 import frappe.sessions
-import frappe.utils.file_manager
 import frappe.desk.form.run_method
 from frappe.utils.response import build_response
 from werkzeug.wrappers import Response
@@ -111,7 +110,18 @@ def uploadfile():
 	try:
 		if frappe.form_dict.get('from_form'):
 			try:
-				ret = frappe.utils.file_manager.upload()
+				ret = frappe.get_doc({
+					"doctype": "File",
+					"attached_to_name": frappe.form_dict.docname,
+					"attached_to_doctype": frappe.form_dict.doctype,
+					"attached_to_field": frappe.form_dict.docfield,
+					"file_url": frappe.form_dict.file_url,
+					"file_name": frappe.form_dict.filename,
+					"is_private": frappe.utils.cint(frappe.form_dict.is_private),
+					"content": frappe.form_dict.filedata,
+					"decode": True
+				})
+				ret.save()
 			except frappe.DuplicateEntryError:
 				# ignore pass
 				ret = None
diff --git a/frappe/integrations/doctype/gsuite_templates/gsuite_templates.py b/frappe/integrations/doctype/gsuite_templates/gsuite_templates.py
index ff85ac839890..83f379ee29e0 100644
--- a/frappe/integrations/doctype/gsuite_templates/gsuite_templates.py
+++ b/frappe/integrations/doctype/gsuite_templates/gsuite_templates.py
@@ -7,7 +7,6 @@
 from frappe import _
 from frappe.model.document import Document
 from frappe.integrations.doctype.gsuite_settings.gsuite_settings import run_gsuite_script
-from frappe.utils.file_manager import save_url
 
 class GSuiteTemplates(Document):
 	pass
@@ -25,7 +24,16 @@ def create_gsuite_doc(doctype, docname, gs_template=None):
 
 	r = run_gsuite_script('new', filename, templ.template_id, templ.destination_id, json_data)
 
-	filedata = save_url(r['url'], filename, doctype, docname, "Home/Attachments", True)
+	_file = frappe.get_doc({
+		"doctype": "File",
+		"file_url": r['url'],
+		"file_name": filename,
+		"attached_to_doctype": doctype,
+		"attached_to_name": docname,
+		"attached_to_field": True,
+		"folder": "Home/Attachments"})
+	_file.save()
+
 	comment = frappe.get_doc(doctype, docname).add_comment("Attachment",
 		_("added {0}").format("<a href='{file_url}' target='_blank'>{file_name}</a>{icon}".format(**{
 			"icon": ' <i class="fa fa-lock text-warning"></i>' if filedata.is_private else "",
diff --git a/frappe/limits.py b/frappe/limits.py
index 93e0e05ee056..fe800e3fe5be 100755
--- a/frappe/limits.py
+++ b/frappe/limits.py
@@ -173,7 +173,7 @@ def clear_limit(key):
 
 def validate_space_limit(file_size):
 	"""Stop from writing file if max space limit is reached"""
-	from frappe.utils.file_manager import MaxFileSizeReachedError
+	from frappe.core.doctype.file.file import MaxFileSizeReachedError
 
 	limits = get_limits()
 	if not limits.space:
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index aa6e3099151d..449bfa0262a5 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -774,7 +774,7 @@ def cast(self, value, df):
 		return cast_fieldtype(df.fieldtype, value)
 
 	def _extract_images_from_text_editor(self):
-		from frappe.utils.file_manager import extract_images_from_doc
+		from frappe.core.doctype.file.file import extract_images_from_doc
 		if self.doctype != "DocType":
 			for df in self.meta.get("fields", {"fieldtype": ('=', "Text Editor")}):
 				extract_images_from_doc(self, df.fieldname)
diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py
index 34c5da61abac..2b8a74764301 100644
--- a/frappe/model/delete_doc.py
+++ b/frappe/model/delete_doc.py
@@ -7,7 +7,7 @@
 import frappe.model.meta
 from frappe.model.dynamic_links import get_dynamic_link_map
 import frappe.defaults
-from frappe.utils.file_manager import remove_all
+from frappe.core.doctype.file.file import remove_all
 from frappe.utils.password import delete_all_passwords_for
 from frappe import _
 from frappe.model.naming import revert_series_if_last
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 9442e87eada4..d98dbcc5232d 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -14,7 +14,6 @@
 import hashlib, json
 from frappe.model import optional_fields
 from frappe.model.workflow import validate_workflow
-from frappe.utils.file_manager import save_url
 from frappe.utils.global_search import update_global_search
 from frappe.integrations.doctype.webhook import run_webhooks
 
@@ -322,7 +321,15 @@ def copy_attachments_from_amended_from(self):
 		for attach_item in get_attachments(self.doctype, self.amended_from):
 
 			#save attachments to new doc
-			save_url(attach_item.file_url, attach_item.file_name, self.doctype, self.name, "Home/Attachments", attach_item.is_private)
+			_file = frappe.get_doc({
+				"doctype": "File",
+				"file_url": attach_item.file_url,
+				"file_name": attach_item.file_name,
+				"attached_to_name": self.name,
+				"attached_to_doctype": self.doctype,
+				"folder": "Home/Attachments"})
+			_file.save()
+
 
 	def update_children(self):
 		'''update child tables'''
diff --git a/frappe/patches/v4_0/file_manager_hooks.py b/frappe/patches/v4_0/file_manager_hooks.py
index 60686b1e0e50..6be3b2512405 100644
--- a/frappe/patches/v4_0/file_manager_hooks.py
+++ b/frappe/patches/v4_0/file_manager_hooks.py
@@ -6,7 +6,7 @@
 import frappe
 import os
 from frappe.utils import get_files_path
-from frappe.utils.file_manager import get_content_hash, get_file
+from frappe.core.doctype.file.file import get_content_hash
 
 
 def execute():
@@ -22,7 +22,8 @@ def execute():
 		else:
 			b.file_url = os.path.normpath('/files/' + old_file_name)
 		try:
-			_file_name, content = get_file(name)
+			_file = frappe.get_doc("File", {"file_name": name})
+			content = _file.get_content()
 			b.content_hash = get_content_hash(content)
 		except IOError:
 			print('Warning: Error processing ', name)
diff --git a/frappe/patches/v4_1/file_manager_fix.py b/frappe/patches/v4_1/file_manager_fix.py
index 1fced61799f6..cd30c9417731 100644
--- a/frappe/patches/v4_1/file_manager_fix.py
+++ b/frappe/patches/v4_1/file_manager_fix.py
@@ -5,7 +5,7 @@
 
 import frappe
 import os
-from frappe.utils.file_manager import get_content_hash, get_file, get_file_name
+from frappe.core.doctype.file.file import get_content_hash, get_file_name
 from frappe.utils import get_files_path, get_site_path
 
 # The files missed by the previous patch might have been replaced with new files
@@ -36,7 +36,8 @@ def execute():
 		else:
 			b.file_url = os.path.normpath('/files/' + old_file_name)
 		try:
-			_file_name, content = get_file(name)
+			_file = frappe.get_doc("File", {"file_name": name})
+			content = _file.get_content()
 			b.content_hash = get_content_hash(content)
 		except IOError:
 			print('Warning: Error processing ', name)
diff --git a/frappe/printing/doctype/print_format/print_format.py b/frappe/printing/doctype/print_format/print_format.py
index ea66665572a0..8f25ab7cb898 100644
--- a/frappe/printing/doctype/print_format/print_format.py
+++ b/frappe/printing/doctype/print_format/print_format.py
@@ -31,7 +31,7 @@ def validate(self):
 			validate_template(self.html)
 
 	def extract_images(self):
-		from frappe.utils.file_manager import extract_images_from_html
+		from frappe.core.doctype.file.file import extract_images_from_html
 		if self.format_data:
 			data = json.loads(self.format_data)
 			for df in data:
diff --git a/frappe/public/js/frappe/upload.js b/frappe/public/js/frappe/upload.js
index bdc8b4131656..ca2ed264edbf 100644
--- a/frappe/public/js/frappe/upload.js
+++ b/frappe/public/js/frappe/upload.js
@@ -317,7 +317,7 @@ frappe.upload = {
 		} else {
 			args.file_size = fileobj.size;
 			frappe.call({
-				method: 'frappe.utils.file_manager.validate_filename',
+				method: 'frappe.core.doctype.file.file.validate_filename',
 				args: {"filename": args.filename},
 				callback: function(r) {
 					args.filename = r.message;
diff --git a/frappe/twofactor.py b/frappe/twofactor.py
index 03d23744ddc9..33bade873c9c 100644
--- a/frappe/twofactor.py
+++ b/frappe/twofactor.py
@@ -337,13 +337,19 @@ def get_qr_svg_code(totp_uri):
 
 def qrcode_as_png(user, totp_uri):
 	'''Save temporary Qrcode to server.'''
-	from frappe.utils.file_manager import save_file
 	folder = create_barcode_folder()
 	png_file_name = '{}.png'.format(frappe.generate_hash(length=20))
-	file_obj = save_file(png_file_name, png_file_name, 'User', user, folder=folder)
+	_file = frappe.get_doc({
+		"doctype": "File",
+		"file_name": png_file_name,
+		"attached_to_doctype": 'User',
+		"attached_to_name": user,
+		"folder": folder,
+		"content": png_file_name})
+	_file.save()
 	frappe.db.commit()
-	file_url = get_url(file_obj.file_url)
-	file_path = os.path.join(frappe.get_site_path('public', 'files'), file_obj.file_name)
+	file_url = get_url(_file.file_url)
+	file_path = os.path.join(frappe.get_site_path('public', 'files'), _file.file_name)
 	url = qrcreate(totp_uri)
 	with open(file_path, 'w') as png_file:
 		url.png(png_file, scale=8, module_color=[0, 0, 0, 180], background=[0xff, 0xff, 0xcc])
diff --git a/frappe/utils/csvutils.py b/frappe/utils/csvutils.py
index 7476eac0c349..72b03ae9d006 100644
--- a/frappe/utils/csvutils.py
+++ b/frappe/utils/csvutils.py
@@ -15,8 +15,8 @@ def read_csv_content_from_uploaded_file(ignore_encoding=False):
 		with open(frappe.uploaded_file, "r") as upfile:
 			fcontent = upfile.read()
 	else:
-		from frappe.utils.file_manager import get_uploaded_content
-		fname, fcontent = get_uploaded_content()
+		_file = frappe.new_doc("File")
+		fcontent = _file.get_uploaded_content()
 	return read_csv_content(fcontent, ignore_encoding)
 
 def read_csv_content_from_attached_file(doc):
@@ -30,8 +30,8 @@ def read_csv_content_from_attached_file(doc):
 		raise Exception
 
 	try:
-		from frappe.utils.file_manager import get_file
-		fname, fcontent = get_file(fileid)
+		_file = frappe.get_doc("File", {"file_name": fileid})
+		fcontent = _file.get_content()
 		return read_csv_content(fcontent, frappe.form_dict.get('ignore_encoding_errors'))
 	except Exception:
 		frappe.throw(_("Unable to open attached file. Did you export it as CSV?"), title=_('Invalid CSV Format'))
diff --git a/frappe/utils/response.py b/frappe/utils/response.py
index 78eb88cee1ec..284d1fbc2527 100644
--- a/frappe/utils/response.py
+++ b/frappe/utils/response.py
@@ -17,7 +17,6 @@
 from werkzeug.wsgi import wrap_file
 from werkzeug.wrappers import Response
 from werkzeug.exceptions import NotFound, Forbidden
-from frappe.core.doctype.file.file import check_file_permission
 from frappe.website.render import render
 from frappe.utils import cint
 from six import text_type
@@ -147,7 +146,8 @@ def download_backup(path):
 def download_private_file(path):
 	"""Checks permissions and sends back private file"""
 	try:
-		check_file_permission(path)
+		_file = frappe.get_doc("File", {"file_url": path})
+		_file.check_file_permission()
 
 	except frappe.PermissionError:
 		raise Forbidden(_("You don't have permission to access this file"))
diff --git a/frappe/utils/xlsxutils.py b/frappe/utils/xlsxutils.py
index 880726c16ee2..4354f56d218f 100644
--- a/frappe/utils/xlsxutils.py
+++ b/frappe/utils/xlsxutils.py
@@ -70,8 +70,8 @@ def handle_html(data):
 
 def read_xlsx_file_from_attached_file(file_id=None, fcontent=None, filepath=None):
 	if file_id:
-		from frappe.utils.file_manager import get_file_path
-		filename = get_file_path(file_id)
+		_file = frappe.get_doc("File", {"file_name": file_id})
+		filename = _file.get_full_path()
 	elif fcontent:
 		from io import BytesIO
 		filename = BytesIO(fcontent)
diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py
index b1d5708444b8..d2ffed70465a 100644
--- a/frappe/website/doctype/web_form/web_form.py
+++ b/frappe/website/doctype/web_form/web_form.py
@@ -6,10 +6,10 @@
 from frappe.website.website_generator import WebsiteGenerator
 from frappe import _, scrub
 from frappe.utils import cstr
-from frappe.utils.file_manager import save_file, remove_file_by_url
 from frappe.website.utils import get_comment_list
 from frappe.custom.doctype.customize_form.customize_form import docfield_properties
-from frappe.utils.file_manager import get_max_file_size
+from frappe.core.doctype.file.file import get_max_file_size
+from frappe.core.doctype.file.file import remove_file_by_url
 from frappe.modules.utils import export_module_json, get_doc_module
 from six.moves.urllib.parse import urlencode
 from frappe.integrations.utils import get_payment_gateway_controller
@@ -416,22 +416,29 @@ def accept(web_form, data, for_payment=False):
 
 			# remove earlier attached file (if exists)
 			if doc.get(fieldname):
-				remove_file_by_url(doc.get(fieldname), doc.doctype, doc.name)
+				remove_file_by_url(doc.get(fieldname), doctype=doc.doctype, name=doc.name)
 
 			# save new file
 			filename, dataurl = filedata.split(',', 1)
-			filedoc = save_file(filename, dataurl,
-				doc.doctype, doc.name, decode=True)
+			_file = frappe.get_doc({
+				"doctype": "File",
+				"file_name": filename,
+				"attached_to_doctype": doc.doctype,
+				"attached_to_name": doc.name,
+				"content": dataurl,
+				"decode": True})
+			_file.save()
 
 			# update values
-			doc.set(fieldname, filedoc.file_url)
+			doc.set(fieldname, _file.file_url)
 
 		doc.save(ignore_permissions = True)
 
 	if files_to_delete:
 		for f in files_to_delete:
 			if f:
-				remove_file_by_url(f, doc.doctype, doc.name)
+				remove_file_by_url(doc.get(fieldname), doctype=doc.doctype, name=doc.name)
+
 
 	frappe.flags.web_form_doc = doc
 


Code review comments:
diff_hunk: @@ -265,14 +264,22 @@ def attach_file_to_doc(doctype, docname, file_url):
 			# file is already attached
 			return
 
-		save_url(file_url, None, doctype, docname, "Home/Attachments", 0)
+		_file = frappe.get_doc({
+			"doctype": "File",
+			"file_url": file_url,
+			"attached_to_name": docname,
+			"attached_to_doctype": doctype,
+			"attached_to_field": 0,
+			"folder": "Home/Attachments"})
+		_file.save()
+
 
 	# header
 	filename, file_extension = ['','']
 	if not rows:
-		from frappe.utils.file_manager import get_file # get_file_doc
-		fname, fcontent = get_file(data_import_doc.import_file)
-		filename, file_extension = os.path.splitext(fname)
+		_file = frappe.get_doc("File", {"file_url": data_import_doc.import_file})
+		fcontent = _file.get_content()
+		filename, file_extension = os.path.splitext(_file.file_name)
path: frappe/core/doctype/data_import/importer.py
text: @user1:
`file.get_extension()`

diff_hunk: @@ -6,16 +6,17 @@
 import frappe.desk.form.meta
 import frappe.desk.form.load
 from frappe.utils.html_utils import clean_email_html
+from frappe.core.doctype.file.file import remove_file
 
 from frappe import _
 from six import string_types
 
 @frappe.whitelist()
 def remove_attach():
 	"""remove attachment"""
-	import frappe.utils.file_manager
 	fid = frappe.form_dict.get('fid')
-	return frappe.utils.file_manager.remove_file(fid)
+	return remove_file(fid=fid)
path: frappe/desk/form/utils.py
text: @user1:
`frappe.delete_doc('File', fid)`

diff_hunk: @@ -55,7 +54,7 @@ def test_incoming_with_attach(self):
 		frappe.db.sql("DELETE FROM `tabCommunication` WHERE sender='test_sender@example.com'")
 		existing_file = frappe.get_doc({'doctype': 'File', 'file_name': 'erpnext-conf-14.png'})
 		frappe.delete_doc("File", existing_file.name)
-		delete_file_from_filesystem(existing_file)
+		existing_file.delete_file_from_filesystem()
path: frappe/email/doctype/email_account/test_email_account.py
text: @user1:
frappe.delete_doc should handle this