diff --git a/drive/api/embed.py b/drive/api/embed.py
index 57223a188..9fe1b85dc 100644
--- a/drive/api/embed.py
+++ b/drive/api/embed.py
@@ -28,7 +28,7 @@ def get_file_content(embed_name: str, parent_entity_name: str):
drive_entity = frappe.get_value(
"Drive File",
parent_entity_name,
- ["document", "title", "mime_type", "file_size", "owner", "path", "team"],
+ ["document", "file_name", "mime_type", "file_size", "owner", "path", "team"],
as_dict=1,
)
@@ -41,7 +41,7 @@ def get_file_content(embed_name: str, parent_entity_name: str):
embed = frappe._dict(
path=str(
Path(
- get_home_folder(embed.team)["path"],
+ get_home_folder(embed.team)["file_url"],
"embeds",
embed_name,
)
diff --git a/drive/api/files.py b/drive/api/files.py
index b8791b28d..65c45642d 100644
--- a/drive/api/files.py
+++ b/drive/api/files.py
@@ -1,13 +1,13 @@
import json
-import os
import re
-from datetime import date, timedelta
+from datetime import timedelta
from io import BytesIO
from pathlib import Path
import frappe
import jwt
-import magic
+
+# import magic
import mimemapper
from pypika import Order
from werkzeug.utils import secure_filename, send_file
@@ -21,29 +21,32 @@
get_file_type,
get_home_folder,
update_file_size,
- get_default_team,
+ get_new_file_name,
+ validate_filename,
+ get_upload_path,
)
from drive.utils.api import prettify_file
-from drive.utils.files import FileManager
+from drive.utils.files import FileManager, sanitize_url, get_s3_key, get_s3_url
from .permissions import get_teams, user_has_permission
+FORBIDDEN_DOWNLOAD_TYPES = ["Folder", "Link", "Document"]
+
@frappe.whitelist(allow_guest=True)
@default_team
def upload_file(
team: str,
total_file_size: int = 0,
- last_modified: int = None,
+ file_modified: int = None,
fullpath: str = None,
parent: str = None,
- transfer: int = 0,
embed: int = 0,
):
"""
Accept chunked file contents via a multipart upload.
Store the file on disk, and insert a corresponding DriveFile doc.
- Works with normal uploads, transfers, and embeds.
+ Works with normal uploads, and embeds.
:return: DriveFile doc once the entire file has been uploaded
"""
checks = frappe.get_hooks("validate_drive_upload")
@@ -58,11 +61,11 @@ def upload_file(
if not user_has_permission(parent, "upload"):
frappe.throw("Ask the folder owner for upload access.", frappe.PermissionError)
- team = frappe.db.get_value("Drive File", parent, "team")
+ team = frappe.db.get_value("File", parent, "team")
if fullpath:
parent = ensure_path(team, fullpath, parent)
- # Support non-chunked uploads too
+ # Support both chunked and non-chunked uploads
if frappe.form_dict.chunk_index:
current_chunk = int(frappe.form_dict.chunk_index)
total_chunks = int(frappe.form_dict.total_chunk_count)
@@ -73,9 +76,9 @@ def upload_file(
total_chunks = 1
file = frappe.request.files["file"]
- title = get_new_title(file.filename, parent) if not transfer else file.filename
+ file_name = get_new_file_name(file.filename, parent)
upload_session = frappe.form_dict.uuid
- temp_path = get_upload_path(home_folder["path"], f"{upload_session}_{secure_filename(title)}")
+ temp_path = get_upload_path(sanitize_url(home_folder["file_url"]), f"{upload_session}_{secure_filename(file_name)}")
with temp_path.open("ab") as f:
f.seek(offset)
f.write(file.stream.read())
@@ -92,30 +95,26 @@ def upload_file(
if mime_type is None:
mime_type = magic.from_buffer(open(temp_path, "rb").read(2048), mime=True)
+ file_type = get_file_type(mime_type)
manager = FileManager()
- # Create DB record
- if transfer:
- entity = frappe.get_doc({"doctype": "Drive Transfer", "title": title, "file_size": file_size})
- entity.insert()
- entity.path = str(
- Path(home_folder["path"]) / (entity.name if manager.flat else Path(".transfers") / entity.title)
- )
- entity.save()
- drive_file = frappe._dict(**entity.as_dict(), team=team, parent=parent)
- else:
- drive_file = create_drive_file(
- team,
- title,
- parent,
- mime_type,
- lambda entity: manager.get_disk_path(entity, home_folder, embed),
- file_size,
- int(last_modified) / 1000 if last_modified else None,
- )
+ drive_file = create_drive_file(
+ team,
+ file_name,
+ parent,
+ file_type,
+ lambda file: "/" + str(manager.get_disk_path(file, home_folder, embed)),
+ mime_type,
+ file_size,
+ int(file_modified) / 1000 if file_modified else None,
+ )
# Upload and update parent folder size
- manager.upload_file(temp_path, drive_file, not embed and not transfer)
+ manager.upload_file(temp_path, drive_file, not embed)
+ # Change path to be s3 compatible
+ if manager.s3_enabled:
+ drive_file.file_url = get_s3_url(get_s3_key(drive_file.file_url))
+ drive_file.save()
try:
update_file_size(parent, file_size)
@@ -123,37 +122,20 @@ def upload_file(
# Find a cleaner way to handle folder sizes as multiple simultaneous uploads will break this
pass
- if transfer:
- frappe.publish_realtime("transfer-add", {"file": drive_file})
- elif not embed:
- frappe.publish_realtime("list-add", {"file": prettify_file(drive_file.as_dict())})
+ frappe.publish_realtime("list-add", {"file": prettify_file(drive_file.as_dict())})
return drive_file
@frappe.whitelist(allow_guest=True)
def get_thumbnail(entity_name: str):
- drive_file = frappe.get_value(
- "Drive File",
- entity_name,
- [
- "is_group",
- "path",
- "title",
- "mime_type",
- "file_size",
- "owner",
- "team",
- "document",
- "name",
- ],
- as_dict=1,
- )
- if not drive_file or drive_file.is_group or drive_file.is_link:
- return
- if user_has_permission(drive_file, "read") is False:
+ drive_file = frappe.get_cached_doc("File", entity_name)
+ if not drive_file or drive_file.is_folder:
return
+ if not user_has_permission(drive_file, "read"):
+ frappe.throw("No permission", frappe.PermissionError)
+
thumbnail_data = None
if frappe.cache().exists(entity_name):
try:
@@ -163,13 +145,13 @@ def get_thumbnail(entity_name: str):
if not thumbnail_data:
manager = FileManager()
try:
- if drive_file.mime_type.startswith("text"):
+ if drive_file.file_type == "Markdown":
with manager.get_file(drive_file) as f:
thumbnail_data = f.read()[:1000].decode("utf-8").replace("\n", "
")
- elif drive_file.mime_type == "frappe_doc":
- html = frappe.get_value("Drive Document", drive_file.document, "raw_content")
+ elif drive_file.file_type == "Document":
+ html = frappe.get_value("Writer Document", drive_file.details_docname, "raw_content")
thumbnail_data = html[:1000] if html else ""
- elif drive_file.mime_type == "frappe/slides":
+ elif drive_file.file_type == "Presentation":
# Use this until the thumbnail method is whitelisted
thumbnails = frappe.call(
"slides.slides.doctype.presentation.presentation.get_slide_thumbnails",
@@ -202,7 +184,7 @@ def get_thumbnail(entity_name: str):
@frappe.whitelist()
@default_team
-def create_presentation(team: str, title: str = "Untitled", parent: str | None = None):
+def create_presentation(team: str, file_name: str = "Untitled", parent: str | None = None):
home_directory = get_home_folder(team)
parent = parent or home_directory.name
team = frappe.db.get_value("Drive File", parent, "team")
@@ -214,14 +196,14 @@ def create_presentation(team: str, title: str = "Untitled", parent: str | None =
try:
r = frappe.call(
"slides.slides.doctype.presentation.presentation.create_presentation",
- title=title,
+ title=file_name,
theme="1mjgj61m8j",
)
except BaseException as e:
print("Couldn't create", e)
entity = create_drive_file(
team,
- title,
+ file_name,
parent,
"frappe/slides",
lambda _: r.name,
@@ -231,128 +213,39 @@ def create_presentation(team: str, title: str = "Untitled", parent: str | None =
@frappe.whitelist()
@default_team
-def create_document_entity(team: str, title: str | None = None, parent: str | None = None):
- home_directory = get_home_folder(team)
- parent = parent or home_directory.name
- parent_doc = frappe.get_cached_doc("Drive File", parent)
- team = frappe.db.get_value("Drive File", parent, "team")
- if not title:
- title = get_new_title("Untitled Document", parent)
-
- if not user_has_permission(parent, "upload"):
- frappe.throw(
- "Cannot access folder due to insufficient permissions",
- frappe.PermissionError,
- )
- drive_doc = frappe.new_doc("Drive Document")
- drive_doc.title = title
- drive_doc.settings = '{"collab": true}'
- drive_doc.save()
-
- manager = FileManager()
- path = manager.create_folder(
- frappe._dict(
- {
- "title": title,
- "parent_path": Path(parent_doc.path or ""),
- "team": team,
- "parent_entity": parent_doc.name,
- }
- ),
- home_directory,
- )
- manager.create_folder(
- frappe._dict(
- {
- "title": ".embeds",
- "team": team,
- "parent_path": path,
- }
- ),
- home_directory,
- )
-
- entity = create_drive_file(
- team,
- title,
- parent,
- "frappe_doc",
- lambda _: path,
- document=drive_doc.name,
- )
- return entity
-
-
-def get_upload_path(team_path, file_name):
- uploads_path = Path(frappe.get_site_path("private/files"), team_path, ".uploads")
- if not os.path.exists(uploads_path):
- uploads_path = Path(frappe.get_site_path("private/files"), team_path, ".uploads")
- uploads_path.mkdir()
- return uploads_path / file_name
-
-
-@frappe.whitelist()
-@default_team
-def create_folder(team: str, title: str, parent: str | None = None):
- """
- Create a new folder.
-
- :param title: Folder name
- :param parent: Document-name of the parent folder. Defaults to the user directory
- :raises PermissionError: If the user does not have write access to the specified parent folder
- :raises FileExistsError: If a folder with the same name already exists in the specified parent folder
- :return: DriveEntity doc of the new folder
- """
+def create_folder(team: str, file_name: str, parent: str | None = None):
home_folder = get_home_folder(team)
parent = parent or home_folder.name
- team = frappe.db.get_value("Drive File", parent, "team")
+ team = frappe.db.get_value("File", parent, "team")
- parent_doc = frappe.get_doc("Drive File", parent)
+ parent_doc = frappe.get_doc("File", parent)
if not user_has_permission(parent_doc, "upload"):
frappe.throw(
"You don't have permissions for this.",
frappe.PermissionError,
)
- entity_exists = frappe.db.exists(
- {
- "doctype": "Drive File",
- "parent_entity": parent,
- "is_group": 1,
- "title": title,
- "is_active": 1,
- }
- )
-
- if entity_exists:
- suggested_name = get_new_title(title, parent, folder=True)
- frappe.throw(
- f"Folder '{title}' already exists.\n Suggested: {suggested_name}",
- FileExistsError,
- )
+ validate_filename(file_name, parent, "Folder", error=f"Folder '{file_name}' already exists.")
manager = FileManager()
path = manager.create_folder(
frappe._dict(
{
- "title": title,
+ "file_name": file_name,
"team": team,
- "parent_path": Path(parent_doc.path or ""),
+ "parent_path": Path(parent_doc.file_url or ""),
}
),
home_folder,
)
- drive_file = create_drive_file(
+ return create_drive_file(
team,
- title,
+ file_name,
parent,
- "folder",
- lambda _: path,
- is_group=True,
+ "Folder",
+ path,
)
- return drive_file
-
def ensure_path(team, fullpath, parent=None):
"""
@@ -368,13 +261,13 @@ def ensure_path(team, fullpath, parent=None):
for folder in parts[:-1]:
exists = frappe.db.get_value(
- "Drive File",
+ "File",
{
- "title": folder,
- "is_group": 1,
- "is_active": 1,
+ "file_name": folder,
+ "is_folder": 1,
+ "status": 1,
"team": team,
- "parent_entity": current_parent,
+ "folder": current_parent,
},
"name",
)
@@ -389,7 +282,7 @@ def ensure_path(team, fullpath, parent=None):
@frappe.whitelist()
@default_team
-def create_link(team: str, title: str, link: str, parent: str | None = None):
+def create_link(team: str, file_name: str, link: str, parent: str | None = None):
home_folder = get_home_folder(team)
parent = parent or home_folder.name
@@ -398,33 +291,19 @@ def create_link(team: str, title: str, link: str, parent: str | None = None):
"Cannot create link due to insufficient permissions.",
frappe.PermissionError,
)
- entity_exists = frappe.db.exists(
- {
- "doctype": "Drive File",
- "parent_entity": parent,
- "is_group": 1,
- "title": title,
- "is_active": 1,
- }
- )
- if entity_exists:
- suggested_name = get_new_title(title, parent, folder=True)
- frappe.throw(
- f"File '{title}' already exists.\n Suggested: {suggested_name}",
- FileExistsError,
- )
+ validate_filename(file_name, parent, "Link", error=f"Link '{file_name}' already exists.")
drive_file = frappe.get_doc(
{
- "doctype": "Drive File",
- "title": title,
+ "doctype": "File",
+ "file_name": file_name,
"team": team,
- "path": link,
- "is_link": 1,
- "mime_type": "link/unknown",
- "_modified": frappe.utils.now_datetime(),
- "parent_entity": parent,
+ "file_url": link,
+ "file_type": "Link",
+ "file_modified": frappe.utils.now_datetime(),
+ "folder": parent,
+ "is_drive_file": 1,
}
)
drive_file.insert()
@@ -445,9 +324,7 @@ def create_auth_token(entity_name: str):
@frappe.whitelist(allow_guest=True)
-def get_file_content(
- entity_name: str, trigger_download: bool = False, jwt_token: str | None = None, transfer: bool = False
-):
+def get_file_content(entity_name: str, trigger_download: bool = False, jwt_token: str | None = None):
"""
Stream file content and optionally trigger download
@@ -471,54 +348,43 @@ def get_file_content(
elif not user_has_permission(entity_name, "read"):
raise frappe.PermissionError("You do not have permission to view this file")
- trigger_download = int(trigger_download)
- if transfer:
- transfer = frappe.get_doc("Drive Transfer", entity_name)
- drive_file = frappe._dict(**transfer.as_dict(), team=get_default_team())
- else:
- drive_file = frappe.get_value(
- "Drive File",
- {"name": entity_name},
- [
- "is_group",
- "team",
- "is_link",
- "path",
- "title",
- "mime_type",
- "is_active",
- "document",
- ],
- as_dict=1,
- )
- if not drive_file or drive_file.is_group or drive_file.is_link or (not transfer and drive_file.is_active != 1):
- frappe.throw("Not found", frappe.NotFound)
+ file = frappe.get_value(
+ "File",
+ {"name": entity_name},
+ [
+ "file_name",
+ "file_type",
+ "status",
+ "file_url",
+ "is_drive_file",
+ ],
+ as_dict=1,
+ )
+
+ if file.file_type == "Document" or not file.is_drive_file:
+ frappe.local.response["type"] = "redirect"
+ frappe.local.response["location"] = "/drive/w/" + file.name if file.is_drive_file else file.file_url
+ return
+
+ if not file or file.file_type in FORBIDDEN_DOWNLOAD_TYPES or file.status != 1:
+ frappe.throw("Not found", frappe.DoesNotExistError)
- return get_file_internal(drive_file, trigger_download)
+ return get_file_internal(file, trigger_download)
def get_file_internal(file, trigger_download=0):
- if (
- not trigger_download
- and get_file_type(file.as_dict() if file.as_dict else dict(file)) == "Video"
- and frappe.request.headers.get("Range")
- ):
+ if not trigger_download and file.file_type == "Video" and frappe.request.headers.get("Range"):
return stream_file_content(file.name)
- if file.document:
- frappe.local.response["type"] = "redirect"
- frappe.local.response["location"] = "/drive/w/" + file.name
- return
- else:
- manager = FileManager()
- return send_file(
- manager.get_file(file),
- mimetype=file.mime_type,
- as_attachment=trigger_download,
- conditional=True,
- max_age=3600,
- download_name=file.title,
- environ=frappe.request.environ,
- )
+
+ manager = FileManager()
+ return send_file(
+ manager.get_file(file),
+ as_attachment=trigger_download,
+ conditional=True,
+ max_age=3600,
+ download_name=file.file_name,
+ environ=frappe.request.environ,
+ )
@frappe.whitelist(allow_guest=True)
@@ -532,9 +398,16 @@ def stream_file_content(entity_name: str):
range_header = frappe.request.headers.get("Range")
if not range_header:
return get_file_content(entity_name)
- entity = frappe.get_doc("Drive File", entity_name)
+ entity = frappe.get_doc("File", entity_name)
if not user_has_permission(entity, "read"):
raise frappe.PermissionError("You do not have permission to view this file")
+
+ if not entity.is_drive_file:
+ # frappe.local.response = frappe.utils.response.download_private_file(entity.file_url)
+ frappe.local.response["type"] = "redirect"
+ frappe.local.response["location"] = entity.file_url
+ return
+
size = entity.file_size
byte1, byte2 = 0, None
@@ -560,7 +433,7 @@ def stream_file_content(entity_name: str):
if manager.s3_enabled:
data = manager.get_file(entity, f"bytes={byte1}-{byte1 + length - 1}")
else:
- with manager.open_file(entity.path) as f:
+ with manager.open_file(entity.file_url) as f:
f.seek(byte1)
data = f.read(length)
@@ -570,7 +443,7 @@ def stream_file_content(entity_name: str):
@frappe.whitelist()
-def set_favourite(entities: list[str] | None = None, clear_all: bool = False):
+def set_favourite(entities: list | None = None, clear_all: bool = False):
"""
Favouite or unfavourite DriveEntities for specified user
@@ -622,10 +495,10 @@ def remove_or_restore(entity_names: list[str] | str):
frappe.throw(f"Expected list but got {type(entity_names)}", ValueError)
manager = FileManager()
- def depth_zero_toggle_is_active(doc):
+ def depth_zero_toggle_status(doc):
if not user_has_permission(doc, "write"):
raise frappe.PermissionError("You do not have permission to remove this file")
- if doc.is_active:
+ if doc.status:
flag = 0
manager.move_to_trash(doc)
else:
@@ -635,14 +508,14 @@ def depth_zero_toggle_is_active(doc):
manager.restore(doc)
flag = 1
- doc.is_active = flag
- doc._modified = frappe.utils.now_datetime()
+ doc.status = flag
+ doc.file_modified = frappe.utils.now_datetime()
# Only update parent folder size if parent exists (not root level)
- if doc.parent_entity:
- folder_size = frappe.db.get_value("Drive File", doc.parent_entity, "file_size") or 0
+ if doc.folder:
+ folder_size = frappe.db.get_value("File", doc.folder, "file_size") or 0
frappe.db.set_value(
- "Drive File",
- doc.parent_entity,
+ "File",
+ doc.folder,
"file_size",
folder_size + doc.file_size * (1 if flag else -1),
)
@@ -650,34 +523,32 @@ def depth_zero_toggle_is_active(doc):
doc.save()
for entity in entity_names:
- depth_zero_toggle_is_active(frappe.get_doc("Drive File", entity))
+ depth_zero_toggle_status(frappe.get_doc("File", entity))
@frappe.whitelist()
def delete_entities(entity_names: list[str] | None = None, clear_all: bool = False):
if clear_all:
- entity_names = frappe.db.get_list("Drive File", {"is_active": 0, "owner": frappe.session.user}, pluck="name")
+ entity_names = frappe.db.get_list("File", {"status": 0, "owner": frappe.session.user}, pluck="name")
elif isinstance(entity_names, str):
entity_names = json.loads(entity_names)
elif not isinstance(entity_names, list) or not entity_names:
frappe.throw(f"Expected non-empty list but got {type(entity_names)}", ValueError)
for entity in entity_names:
- frappe.get_doc("Drive File", entity).permanent_delete()
+ frappe.get_doc("File", entity).permanent_delete()
@frappe.whitelist()
def rename(entity_name: str, new_title: str):
- drive_file = frappe.get_doc("Drive File", entity_name)
- if not drive_file:
- frappe.throw("Entity does not exist", ValueError)
+ drive_file = frappe.get_doc("File", entity_name)
return drive_file.rename(new_title)
# Will be replaced after new JS composables refactor
@frappe.whitelist()
def update_access(entity_name: str, method: str, **kwargs):
- drive_file = frappe.get_doc("Drive File", entity_name)
+ drive_file = frappe.get_doc("File", entity_name)
kwargs.pop("cmd")
if not drive_file:
frappe.throw("Entity does not exist", ValueError)
@@ -698,8 +569,7 @@ def remove_recents(entity_names: list[str] | None = [], clear_all: bool = False)
"""
if clear_all:
return frappe.db.delete("Drive Entity Log", {"user": frappe.session.user})
-
- if not isinstance(entity_names, list):
+ elif not isinstance(entity_names, list):
frappe.throw(f"Expected list but got {type(entity_names)}", ValueError)
for entity in entity_names:
@@ -716,36 +586,14 @@ def remove_recents(entity_names: list[str] | None = [], clear_all: bool = False)
@frappe.whitelist()
@default_team
-def does_entity_exist(name: str | None = None, parent_entity: str | None = None, team: str | None = None):
- if not parent_entity:
+def does_entity_exist(name: str | None = None, folder: str | None = None, team: str | None = None):
+ if not folder:
home_folder = get_home_folder(team)
- parent_entity = home_folder.name
- result = frappe.db.exists("Drive File", {"parent_entity": parent_entity, "title": name})
+ folder = home_folder.name
+ result = frappe.db.exists("File", {"folder": folder, "file_name": name})
return result
-def auto_delete_from_trash():
- days_before = (date.today() - timedelta(days=30)).isoformat()
- result = frappe.db.get_all(
- "Drive File",
- filters={"is_active": 0, "last_modified": ["<", days_before]},
- fields=["name"],
- )
- delete_entities(result)
-
-
-def clear_deleted_files():
- days_before = (date.today() + timedelta(days=30)).isoformat()
- result = frappe.db.get_all(
- "Drive File",
- filters={"is_active": -1, "modified": ["<", days_before]},
- fields=["name"],
- )
- for entity in result:
- doc = frappe.get_doc("Drive File", entity, ignore_permissions=True)
- doc.delete()
-
-
@frappe.whitelist()
@default_team
def move(entity_names: list[str], new_parent: str | None = None, team: str | None = None):
@@ -763,12 +611,12 @@ def move(entity_names: list[str], new_parent: str | None = None, team: str | Non
frappe.throw(f"Expected a non-empty list but got {type(entity_names)}", ValueError)
for entity in entity_names:
- doc = frappe.get_doc("Drive File", entity)
+ doc = frappe.get_doc("File", entity)
res = doc.move(new_parent, team)
- if not res["parent_entity"]:
- title, personal = frappe.db.get_value("Drive Team", res["team"], ["title", "personal"])
- res["title"] = "Home" if personal else title
+ if not res["folder"]:
+ file_name, personal = frappe.db.get_value("Drive Team", res["team"], ["file_name", "personal"])
+ res["file_name"] = "Home" if personal else file_name
return res
@@ -784,10 +632,8 @@ def search(query: str):
result = frappe.db.sql(
"""
SELECT `tabDrive File`.name,
- `tabDrive File`.title,
- `tabDrive File`.is_group,
- `tabDrive File`.is_link,
- `tabDrive File`.mime_type,
+ `tabDrive File`.file_name,
+ `tabDrive File`.file_type,
`tabDrive File`.document,
`tabDrive File`.color,
`tabUser`.name AS user_name,
@@ -796,16 +642,14 @@ def search(query: str):
FROM `tabDrive File`
LEFT JOIN `tabUser` ON `tabDrive File`.`owner` = `tabUser`.`name`
WHERE `tabDrive File`.team IN %(teams)s
- AND `tabDrive File`.`is_active` = 1
- AND `tabDrive File`.`parent_entity` <> ''
- AND MATCH(title) AGAINST (%(text)s IN BOOLEAN MODE)
+ AND `tabDrive File`.`status` = 1
+ AND `tabDrive File`.`folder` <> ''
+ AND MATCH(file_name) AGAINST (%(text)s IN BOOLEAN MODE)
GROUP BY `tabDrive File`.`name`
""",
values={"teams": teams, "text": text},
as_dict=1,
)
- for r in result:
- r["file_type"] = get_file_type(r)
return result
except Exception as e:
frappe.log_error(frappe.get_traceback(), "Frappe Drive Search Error")
@@ -814,54 +658,21 @@ def search(query: str):
@frappe.whitelist(allow_guest=True)
def translate_old_name(old_name: str):
- return frappe.get_value("Drive File", {"old_name": old_name}, "name")
-
-
-@frappe.whitelist()
-def get_new_title(title: str, parent_name: str, folder: bool = False, entity: str | None = None):
- """
- Returns new title for an entity if same title exists for another entity at the same level
-
- :param entity_title: Title of entity to be renamed (if at all)
- :param parent_entity: Parent entity of entity to be renamed (if at all)
- :return: String with new title
- """
- entity_title, entity_ext = os.path.splitext(title)
-
- filters = {
- "is_active": 1,
- "parent_entity": parent_name,
- "title": ["like", f"{entity_title}%{entity_ext}"],
- }
-
- if folder:
- filters["is_group"] = 1
-
- sibling_entity_titles = frappe.db.get_list(
- "Drive File",
- filters=filters,
- fields=["title", "name"],
- )
- if (
- not sibling_entity_titles
- or (sibling_entity_titles[0].name == entity)
- or not any(k["title"] == title for k in sibling_entity_titles)
- ):
- return title
- return f"{entity_title} ({len(sibling_entity_titles)}){entity_ext}"
+ return frappe.get_value("File", {"old_name": old_name}, "name")
@frappe.whitelist(allow_guest=True)
def get_entity_type(entity_name: str):
+ if not user_has_permission(entity_name, "read"):
+ frappe.throw("You do not have permission to view this file.", frappe.PermissionError)
+
entity = frappe.db.get_value(
- "Drive File",
- {"is_active": 1, "name": entity_name},
- ["team", "name", "mime_type", "is_group", "doc"],
+ "File",
+ {"status": 1, "name": entity_name},
+ ["name", "file_type"],
as_dict=1,
)
- if entity.doc or entity.mime_type == "text/markdown":
- entity["type"] = "document"
- elif entity.is_group:
+ if entity.file_type == "Folder":
entity["type"] = "folder"
else:
entity["type"] = "file"
@@ -875,12 +686,26 @@ def get_root_folder(team: str):
return get_home_folder(team)
-def auto_delete_transfers():
- from frappe.utils import now_datetime, add_to_date
+@frappe.whitelist(allow_guest=True)
+def redirect_to_original(file_id: str):
+ """
+ Redirect Drive attachments to original files
+ """
+ file = frappe.get_cached_doc("File", file_id)
+ if not user_has_permission(file_id, "read"):
+ frappe.throw("You do not have permission to view this file.", frappe.PermissionError)
+ if not file.details_doctype == "File":
+ frappe.throw("This is not an attachment", ValueError)
- one_hour_ago = add_to_date(now_datetime(), hours=-1)
+ frappe.local.response["type"] = "redirect"
+ frappe.local.response["location"] = "/drive/g/" + file.details_docname
- transfers = frappe.get_all("Drive Transfer", filters={"creation": ["<", one_hour_ago]}, pluck="name")
- for name in transfers:
- frappe.delete_doc("Drive Transfer", name)
+@frappe.whitelist()
+def get_docs_attached_to(file_name: str):
+ file = frappe.get_doc("File", file_name)
+ return frappe.get_list(
+ "File",
+ filters={"attached_to_doctype": ["is", "set"], "file_url": file.file_url},
+ fields=["attached_to_doctype", "attached_to_name"],
+ )
diff --git a/drive/api/integration.py b/drive/api/integration.py
index 99c6f60fc..ad74002ae 100644
--- a/drive/api/integration.py
+++ b/drive/api/integration.py
@@ -6,7 +6,7 @@ def presentation(doc, event):
if file:
if event == "on_update":
- frappe.get_doc("Drive File", file).rename(doc.title)
+ frappe.get_doc("Drive File", file).rename(doc.file_name)
if event == "on_trash":
print("gone, boom boom")
frappe.get_doc("Drive File", file).permanent_delete()
diff --git a/drive/api/list.py b/drive/api/list.py
index 63a065be6..d896ad03c 100644
--- a/drive/api/list.py
+++ b/drive/api/list.py
@@ -1,17 +1,19 @@
import json
+from collections import Counter
import frappe
-from pypika import Criterion, CustomFunction, Order
+from pypika import Criterion, CustomFunction, Order, Query
from pypika import functions as fn
+from frappe.core.doctype.file.file import get_permission_query_conditions as ff_get_permission_query_conditions
-from drive.utils import MIME_LIST_MAP, default_team, get_file_type, get_home_folder
-from drive.utils.api import get_default_access
-from .permissions import ENTITY_FIELDS, get_user_access
+from drive.utils import MIME_LIST_MAP, default_team, get_home_folder, FILE_FIELDS, map_ff_to_drive_type
+from drive.utils.api import get_default_access
+from .permissions import get_user_access, user_has_permission
DriveUser = frappe.qb.DocType("User")
UserGroupMember = frappe.qb.DocType("User Group Member")
-DriveFile = frappe.qb.DocType("Drive File")
+DriveFile = frappe.qb.DocType("File")
DrivePermission = frappe.qb.DocType("Drive Permission")
Team = frappe.qb.DocType("Drive Team")
TeamMember = frappe.qb.DocType("Drive Team Member")
@@ -22,96 +24,307 @@
Binary = CustomFunction("BINARY", ["expression"])
+# Helper Functions for Filters
+def _apply_shared_filter(query, shared_type):
+ """
+ Filters query to show shared files based on shared_type parameter.
+ - "with": files shared with current user
+ - "public": publicly shared files
+ - False/None: all files with permission join
+ """
+ user = frappe.session.user if frappe.session.user != "Guest" else ""
+ cond = (DrivePermission.entity == DriveFile.name) & (DrivePermission.user == user)
+
+ if shared_type == "with":
+ return query.right_join(DrivePermission).on(cond)
+ elif shared_type == "public":
+ cond = (DrivePermission.entity == DriveFile.name) & (DrivePermission.user == "")
+ return query.right_join(DrivePermission).on(cond)
+ else:
+ return query.left_join(DrivePermission).on(cond)
+
+
+def _apply_tags_filter(query, tag_list):
+ """
+ Filters files by tags using OR logic (matches any tag).
+ """
+ if not tag_list:
+ return query
+
+ tag_list = json.loads(tag_list) if isinstance(tag_list, str) else tag_list
+ query = query.left_join(DriveEntityTag).on(DriveEntityTag.parent == DriveFile.name)
+ tag_list_criterion = [DriveEntityTag.tag == tag for tag in tag_list]
+ return query.where(Criterion.any(tag_list_criterion))
+
+
+def _apply_file_kinds_filter(query, file_kinds):
+ """
+ Filters files by kind/mime type.
+ """
+ file_kinds = json.loads(file_kinds) if isinstance(file_kinds, str) else file_kinds
+ if not file_kinds:
+ return query
+
+ mime_types = []
+ for kind in file_kinds:
+ mime_types.extend(MIME_LIST_MAP.get(kind, []))
+
+ criterion = [DriveFile.mime_type == mime_type for mime_type in mime_types]
+ if "Folder" in file_kinds:
+ criterion.append(DriveFile.is_folder == 1)
+
+ return query.where(Criterion.any(criterion))
+
+
+# Data Aggregation Functions
+def _get_children_count(files):
+ """
+ Returns a dict mapping folder names to their child count.
+ """
+ if not files:
+ return {}
+ query = (
+ frappe.qb.from_(DriveFile)
+ .where((DriveFile.folder.isin([k["name"] for k in files])) & (DriveFile.status == 1))
+ .groupby(DriveFile.folder)
+ .select(DriveFile.folder, fn.Count("*").as_("child_count"))
+ )
+ return dict(query.run())
+
+
+def _get_share_count(team=None):
+ """
+ Returns a dict mapping file names to their share count.
+ Counts shares with individual users (excludes team and public shares).
+ """
+ query = (
+ frappe.qb.from_(DriveFile)
+ .right_join(DrivePermission)
+ .on(DrivePermission.entity == DriveFile.name)
+ .where((DrivePermission.user != "") & (DrivePermission.user != "$TEAM"))
+ .select(DriveFile.name, fn.Count("*").as_("share_count"))
+ .groupby(DriveFile.name)
+ )
+ return dict(query.run())
+
+
+def _get_public_files():
+ """
+ Returns a set of file names that are publicly shared.
+ """
+ query = frappe.qb.from_(DrivePermission).where(DrivePermission.user == "").select(DrivePermission.entity)
+ return set(k[0] for k in query.run())
+
+
+def _get_team_files():
+ """
+ Returns a set of file names shared with team.
+ """
+ query = frappe.qb.from_(DrivePermission).where(DrivePermission.team == 1).select(DrivePermission.entity)
+ return set(k[0] for k in query.run())
+
+
+def _get_basic_query(search):
+ query = frappe.qb.from_(DriveFile).where((DriveFile.status == 1) | (DriveFile.is_drive_file == 0))
+ if search:
+ query = query.where(DriveFile.file_name.like(f"%{search}%"))
+ return query
+
+
@frappe.whitelist(allow_guest=True)
@default_team
def files(
team: str,
entity_name: str | None = None,
- order_by: str = "modified 1",
- is_active: bool = True,
- limit: int = 20,
- cursor: str | None = None,
- favourites_only: bool = False,
- recents_only: bool = False,
- shared: str | None = None,
+ order_by: str = "modified",
+ ascending: bool = True,
tag_list: list[str] | str = [],
file_kinds: list[str] | str = [],
- folders: bool = False,
- only_parent: bool = True,
search: str = None,
):
- field, ascending = order_by.replace("modified", "_modified").split(" ")
-
- all_teams = False
+ """
+ Returns all active files in a folder.
+ """
if team == "all":
- all_teams = True
team = None
+ if not entity_name:
+ if team:
+ entity_name = get_home_folder(team)["name"]
+ else:
+ frappe.throw("You must provide a folder to query", ValueError)
- if not entity_name and team:
- entity_name = get_home_folder(team)["name"]
+ entity = frappe.get_doc("File", entity_name)
+ if team and not team == entity.team and entity.is_drive_file:
+ frappe.throw("Given team doesn't match the file's team", ValueError)
- user = frappe.session.user if frappe.session.user != "Guest" else ""
- if entity_name:
- entity = frappe.get_doc("Drive File", entity_name)
- # Verify that entity exists and is part of the team
- if not entity:
- frappe.throw(
- f"Not found ({entity_name}) ",
- frappe.exceptions.PageDoesNotExistError,
- )
-
- if not team == entity.team:
- team = entity.team
-
- # Verify that folder is public or that they have access
- user_access = get_user_access(entity, user)
-
- if not user_access["read"]:
- frappe.throw(
- f"You don't have access.",
- frappe.exceptions.PermissionError,
- )
-
- query = frappe.qb.from_(DriveFile).where(DriveFile.is_active == is_active)
- if shared:
- if shared == "by" or shared == "with":
- cond = (DrivePermission.entity == DriveFile.name) & (
- (DrivePermission.user if shared == "with" else DrivePermission.owner) == frappe.session.user
- )
- elif shared == "public":
- cond = (DrivePermission.entity == DriveFile.name) & (DrivePermission.user == "")
- # if shared == "with":
- # teams = get_teams()
- # cond |= (DrivePermission.team == 1) & (DrivePermission.user.isin(teams))
- query = query.right_join(DrivePermission).on(cond)
- else:
- query = query.left_join(DrivePermission).on(
- (DrivePermission.entity == DriveFile.name) & (DrivePermission.user == user)
+ if not user_has_permission(entity, "read"):
+ frappe.throw(
+ f"You don't have access.",
+ frappe.exceptions.PermissionError,
)
- query = query.select(*ENTITY_FIELDS, DrivePermission.user.as_("shared_team")).where(
- fn.Coalesce(DrivePermission.read, 1).as_("read") == 1
+ query = _get_basic_query(search).where(DriveFile.folder == entity_name)
+
+ return get_query_data(
+ query,
+ team=team,
+ tag_list=tag_list,
+ file_kinds=file_kinds,
+ entity_name=entity_name,
+ order_by=order_by,
+ ascending=ascending,
+ )
+
+
+@frappe.whitelist()
+@default_team
+def shared(
+ team: str,
+ shared_type: str = "with",
+ order_by: str = "modified",
+ ascending: bool = True,
+ tag_list: list[str] | str = [],
+ file_kinds: list[str] | str = [],
+ search: str = None,
+):
+ """
+ Returns shared files based on shared_type parameter.
+ - "with": files shared with current user
+ - "public": publicly shared files
+ """
+ query = _get_basic_query(search)
+
+ return get_query_data(
+ query,
+ shared_type=shared_type,
+ tag_list=tag_list,
+ file_kinds=file_kinds,
+ team=team,
+ order_by=order_by,
+ ascending=ascending,
+ )
+
+
+@frappe.whitelist()
+@default_team
+def favourites(
+ team: str,
+ order_by: str = "modified",
+ ascending: bool = True,
+ tag_list: list[str] | str = [],
+ file_kinds: list[str] | str = [],
+ search: str = None,
+):
+ """
+ Returns all files marked as favourite by the current user.
+ """
+ query = _get_basic_query(search)
+
+ return get_query_data(
+ query,
+ favourites_only=True,
+ tag_list=tag_list,
+ file_kinds=file_kinds,
+ team=team,
+ order_by=order_by,
+ ascending=ascending,
)
- # Cursor pagination
- if cursor:
- query = query.where((Binary(DriveFile[field]) > cursor if ascending else field < cursor)).limit(limit)
- # Cleaner way?
- if only_parent and (not recents_only and not favourites_only and not shared):
- query = query.where(DriveFile.parent_entity == entity_name)
- elif not all_teams:
- query = query.where((DriveFile.team == team) & (DriveFile.parent_entity != ""))
+@frappe.whitelist()
+@default_team
+def recents(
+ team: str,
+ order_by: str = "modified",
+ ascending: bool = True,
+ tag_list: list[str] | str = [],
+ file_kinds: list[str] | str = [],
+ search: str = None,
+):
+ """
+ Returns all files marked recently by the current user.
+ """
+ query = _get_basic_query(search)
+
+ return get_query_data(
+ query,
+ recents_only=True,
+ tag_list=tag_list,
+ file_kinds=file_kinds,
+ team=team,
+ order_by=order_by,
+ ascending=ascending,
+ )
+
+
+@frappe.whitelist()
+@default_team
+def trash(
+ team: str,
+ order_by: str = "modified",
+ ascending: bool = True,
+ tag_list: list[str] | str = [],
+ file_kinds: list[str] | str = [],
+ search: str = None,
+):
+ """
+ Returns all deleted files (trash) for the current user.
+ """
+ query = (
+ frappe.qb.from_(DriveFile)
+ .where((DriveFile.status == 0) & (DriveFile.is_drive_file == 1))
+ .where(DriveFile.owner == frappe.session.user)
+ )
+ if search:
+ query = query.where(DriveFile.file_name.like(f"%{search}%"))
+
+ return get_query_data(
+ query,
+ team=team,
+ tag_list=tag_list,
+ file_kinds=file_kinds,
+ order_by=order_by,
+ ascending=ascending,
+ )
+
- # Get favourites data (only that, if applicable)
+def get_query_data(
+ query,
+ favourites_only=False,
+ recents_only=False,
+ tag_list=[],
+ file_kinds=[],
+ team=None,
+ entity_name=None,
+ shared_type=None,
+ order_by="modified",
+ ascending=True,
+):
+ """
+ Runs all the necessary commands to obtain files in the structure expected by Drive frontend.
+ """
+ # Filter by team
+ if team and team != "all":
+ query = query.where((DriveFile.team == team) | (DriveFile.team.isnull()))
+
+ # Apply shared filter
+ query = _apply_shared_filter(query, shared_type)
+ query = query.select(
+ *FILE_FIELDS,
+ DrivePermission.user.as_("shared_team"),
+ ).where(fn.Coalesce(DrivePermission.read, 1).as_("read") == 1)
+
+ # Apply favourites filter
if favourites_only:
query = query.right_join(DriveFavourite)
else:
query = query.left_join(DriveFavourite)
+
query = query.on((DriveFavourite.entity == DriveFile.name) & (DriveFavourite.user == frappe.session.user)).select(
DriveFavourite.name.as_("is_favourite")
)
+ # Apply recents filter
if recents_only:
query = (
query.right_join(Recents)
@@ -122,64 +335,27 @@ def files(
query = (
query.left_join(Recents)
.on((Recents.entity_name == DriveFile.name) & (Recents.user == frappe.session.user))
- .orderby(DriveFile[field], order=Order.asc if ascending else Order.desc)
+ .orderby(DriveFile[order_by], order=Order.asc if ascending else Order.desc)
)
- if not is_active:
- query = query.where(DriveFile.owner == frappe.session.user)
- if search:
- # escape wildcards or lower() depending on DB
- query = query.where(DriveFile.title.like(f"%{search}%"))
-
query = query.select(Recents.last_interaction.as_("accessed"))
- if tag_list:
- tag_list = json.loads(tag_list)
- query = query.left_join(DriveEntityTag).on(DriveEntityTag.parent == DriveFile.name)
- tag_list_criterion = [DriveEntityTag.tag == tags for tags in tag_list]
- query = query.where(Criterion.any(tag_list_criterion))
-
- file_kinds = json.loads(file_kinds) if not isinstance(file_kinds, list) else file_kinds
- if file_kinds:
- mime_types = []
- for kind in file_kinds:
- mime_types.extend(MIME_LIST_MAP.get(kind, []))
- criterion = [DriveFile.mime_type == mime_type for mime_type in mime_types]
- if "Folder" in file_kinds:
- criterion.append(DriveFile.is_group == 1)
- query = query.where(Criterion.any(criterion))
-
- if folders:
- query = query.where(DriveFile.is_group == 1)
- res = query.run(as_dict=True)
- child_count_query = (
- frappe.qb.from_(DriveFile)
- .where((DriveFile.team == team) & (DriveFile.is_active == 1))
- .select(DriveFile.parent_entity, fn.Count("*").as_("child_count"))
- .groupby(DriveFile.parent_entity)
- )
- share_query = (
- frappe.qb.from_(DriveFile)
- .right_join(DrivePermission)
- .on(DrivePermission.entity == DriveFile.name)
- .where((DrivePermission.user != "") & (DrivePermission.user != "$TEAM"))
- .select(DriveFile.name, fn.Count("*").as_("share_count"))
- .groupby(DriveFile.name)
- )
- public_files_query = (
- frappe.qb.from_(DrivePermission).where(DrivePermission.user == "").select(DrivePermission.entity)
- )
- team_files_query = frappe.qb.from_(DrivePermission).where(DrivePermission.team == 1).select(DrivePermission.entity)
- public_files = set(k[0] for k in public_files_query.run())
- team_files = set(k[0] for k in team_files_query.run())
+ # Apply tag and file kind filters
+ query = _apply_tags_filter(query, tag_list)
+ query = _apply_file_kinds_filter(query, file_kinds)
- children_count = dict(child_count_query.run())
- share_count = dict(share_query.run())
+ res = query.run(as_dict=True)
- default = get_default_access(entity_name)
+ # Get aggregated data
+ children_count = _get_children_count(res)
+ share_count = _get_share_count()
+ public_files = _get_public_files()
+ team_files = _get_team_files()
- # Deduplicate
- if shared:
+ default = get_default_access(entity_name) if entity_name else 0
+
+ # Deduplicate results
+ if shared_type:
added = set()
filtered_list = []
for r in res:
@@ -188,26 +364,56 @@ def files(
added.add(r["name"])
res = filtered_list
- # Performance hit is wild, manually checking perms each time without cache.
+ # Enrich results with aggregated data and permissions
for r in res:
- r["children"] = children_count.get(r["name"], 0)
- r["file_type"] = get_file_type(r)
-
- if r["name"] in public_files:
- r["share_count"] = -2
- elif default > -1 and (r["name"] in team_files):
- r["share_count"] = -1
- elif default == 0:
- r["share_count"] = share_count.get(r["name"], default)
- else:
- r["share_count"] = default
+ r["child_count"] = children_count.get(r["name"], 0)
+ r["share_count"] = {
+ r["name"] in public_files: -2,
+ default > -1 and (r["name"] in team_files): -1,
+ default == 0: share_count.get(r["name"], default),
+ }.get(True, default)
+
+ if not r["is_drive_file"]:
+ r["file_type"] = map_ff_to_drive_type(r)
+ r["modifiable"] = r["is_drive_file"] and not r["details_doctype"] == "File"
+ r["is_attachment"] = r["is_drive_file"] and r["details_doctype"] == "File"
r |= get_user_access(r["name"])
+
return res
@frappe.whitelist()
-def get_transfers():
- transfers = frappe.get_list(
- "Drive Transfer", filters={"owner": frappe.session.user}, fields=["title", "file_size", "creation", "name"]
- )
- return transfers
+def get_attachments(doctype: str | None = None, docname: str | None = None):
+ """
+ Returns all files that are attached to a document.
+ If either doctype or docname isn't specified, returns a list of folder-like objects
+ that represents the tree Doctype > Doc > Attachments.
+ """
+ if doctype and docname:
+ files = frappe.get_list(
+ "File", filters={"attached_to_doctype": doctype, "attached_to_name": docname}, pluck="name"
+ )
+ query = frappe.qb.from_(DriveFile).where(DriveFile.name.isin(files))
+ return get_query_data(query)
+
+ if doctype:
+ names = frappe.get_list("File", filters={"attached_to_doctype": doctype}, fields=["attached_to_name"])
+ doctypes_set = Counter(k["attached_to_name"] for k in names)
+ else:
+ doctypes = frappe.get_list(
+ "File", filters={"attached_to_doctype": ["is", "set"]}, fields=["attached_to_doctype"]
+ )
+ doctypes_set = Counter(k["attached_to_doctype"] for k in doctypes)
+
+ return [
+ {
+ "name": name,
+ "file_name": name,
+ "is_folder": 1,
+ "file_type": "Folder",
+ "child_count": size,
+ "virtual": "docname" if doctype else "doctype",
+ "virtual_extra": doctype,
+ }
+ for name, size in doctypes_set.items()
+ ]
diff --git a/drive/api/notifications.py b/drive/api/notifications.py
index 3d4c8bcda..4514a2fbe 100644
--- a/drive/api/notifications.py
+++ b/drive/api/notifications.py
@@ -4,10 +4,10 @@
def get_link(entity):
- if entity.doc:
+ if entity.file_type == 'Document':
return "/writer/w/" + entity.name
- type_ = {True: "f", bool(entity.is_group): "d"}
- return entity.path if entity.is_link else f"/drive/{type_.get(True)}/{entity.name}/"
+ type_ = {True: "f", bool(entity.is_folder): "d"}
+ return entity.file_url if entity.file_type == 'Link' else f"/drive/{type_.get(True)}/{entity.name}/"
@frappe.whitelist()
@@ -66,14 +66,14 @@ def notify_mentions(entity_name, mentions, comment=False):
:param entity_name: ID of entity
:param document_name: ID of document containing mentions
"""
- entity = frappe.get_doc("Drive File", entity_name)
+ entity = frappe.get_doc("File", entity_name)
for mention in mentions:
create_notification(
frappe.session.user,
mention,
"Mention",
entity,
- f"You were mentioned in a {'comment in:' if comment else 'document:'} {entity.title}",
+ f"You were mentioned in a {'comment in:' if comment else 'document:'} {entity.file_name}",
)
@@ -83,13 +83,13 @@ def notify_share(entity_name, docperm_name):
:param entity_name: ID of entity
:param document_name: ID of docshare containing share info
"""
- entity = frappe.get_doc("Drive File", entity_name)
+ entity = frappe.get_doc("File", entity_name)
docshare = frappe.get_doc("Drive Permission", docperm_name)
author_full_name = frappe.db.get_value("User", {"name": docshare.owner}, ["full_name"])
- entity_type = "document" if entity.doc else "folder" if entity.is_group else "file"
+ entity_type = "document" if entity.file_type == 'Document' else "folder" if entity.is_folder else "file"
link = get_link(entity)
- message = f'{author_full_name} shared a {entity_type} with you: "{entity.title}"'
+ message = f'{author_full_name} shared a {entity_type} with you: "{entity.file_name}"'
if not frappe.db.exists("User", docshare.user):
key = frappe.get_value("Drive User Invitation", {"email": docshare.user})
link = frappe.utils.get_url(f"/api/method/drive.api.product.accept_invite?key={key}&redirect={link}")
@@ -105,13 +105,13 @@ def create_notification(from_user: str, to_user: str, type: str, entity: str, me
if user_access.get("read") == 0:
return
- entity_type = "Document" if entity.doc else "Folder" if entity.is_group else "File"
+ entity_type = "Document" if entity.file_type == 'Document' else "Folder" if entity.is_folder else "File"
details = {
"from_user": from_user,
"to_user": to_user,
"type": type,
"entity_type": entity_type,
- "notif_doctype": "Drive File",
+ "notif_doctype": "File",
"notif_doctype_name": entity.name,
"message": message,
}
diff --git a/drive/api/permissions.py b/drive/api/permissions.py
index f6f0b6208..5139f796b 100644
--- a/drive/api/permissions.py
+++ b/drive/api/permissions.py
@@ -1,34 +1,18 @@
-from frappe.model.document import Document
-import io
-
import frappe
-import markdown
from frappe.utils import getdate
-from markdown.extensions.wikilinks import WikiLinkExtension
-from pypika import Field
-
-from drive.utils import generate_upward_path, get_default_team, get_file_type, get_valid_breadcrumbs
-from drive.utils.files import FileManager
+from frappe.model.document import Document
+from frappe.core.doctype.file.file import has_permission as ff_has_permission
+
+from drive.utils import (
+ generate_upward_path,
+ get_default_team,
+ get_valid_breadcrumbs,
+ FILE_FIELDS,
+ get_home_folder,
+ map_ff_to_drive_type,
+)
from drive.utils.users import mark_as_viewed
-ENTITY_FIELDS = [
- "name",
- "title",
- "is_group",
- "is_link",
- "path",
- Field("_modified").as_("modified"),
- "creation",
- "file_size",
- "mime_type",
- "color",
- "doc",
- "owner",
- "parent_entity",
- "team",
- "allow_download",
-]
-
NO_ACCESS = {
"read": 0,
@@ -54,7 +38,8 @@ def get_user_access(entity: str | Document | frappe._dict, user: str = None, tea
Return the user specific permissions for an entity. Toggle `team` to check team permission.
"""
if isinstance(entity, str):
- entity = frappe.get_cached_doc("Drive File", entity)
+ entity = frappe.get_cached_doc("File", entity)
+
access = NO_ACCESS.copy()
if not user:
if team:
@@ -79,7 +64,7 @@ def get_user_access(entity: str | Document | frappe._dict, user: str = None, tea
"read": 1,
"comment": 1,
"share": 0,
- "upload": int(entity.is_group) and access_level,
+ "upload": int(entity.is_folder) and access_level,
"write": int(access_level == 2 or entity.owner == user),
"type": {2: "admin", 1: "user", 0: "guest"}[access_level],
}
@@ -136,9 +121,13 @@ def get_teams(user: str = None, details: bool = False, exclude_personal: bool =
filters=[["parenttype", "=", "Drive Team"], ["user", "=", user]],
)
if details:
- teams_info = {team: frappe.get_doc("Drive Team", team) for team in teams}
+ teams_info = {
+ team: {**frappe.get_doc("Drive Team", team).as_dict(), "file": get_home_folder(team)["name"]}
+ for team in teams
+ }
if exclude_personal:
- return {t: team for t, team in teams_info.items() if not team.personal}
+ return {t: team for t, team in teams_info.items() if not team["personal"]}
+ return teams_info
return teams
@@ -152,18 +141,20 @@ def get_entity_with_permissions(entity_name: str):
"""
Return file data with permissions
"""
- entity = frappe.db.get_value(
- "Drive File",
- {"is_active": 1, "name": entity_name},
- ENTITY_FIELDS,
- as_dict=1,
+ entity = frappe.get_all(
+ "File",
+ filters={"name": entity_name},
+ or_filters={"status": 1, "is_drive_file": 0},
+ fields=FILE_FIELDS,
+ limit=1,
)
if not entity:
- frappe.throw("We couldn't find what you're looking for.", {"error": frappe.NotFound})
+ frappe.throw("We couldn't find what you're looking for.", frappe.DoesNotExistError)
+ entity = entity[0]
entity["in_home"] = entity.team == get_default_team()
user_access = get_user_access(entity)
- if user_access.get("read") == 0:
+ if not user_access.get("read"):
frappe.throw("You don't have access to this file.", frappe.PermissionError)
owner_info = frappe.db.get_value("User", entity.owner, ["user_image", "full_name"], as_dict=True) or {}
@@ -177,22 +168,7 @@ def get_entity_with_permissions(entity_name: str):
["entity as is_favourite"],
)
mark_as_viewed(entity)
- file_type = get_file_type(entity)
- return_obj = entity | user_access | owner_info | breadcrumbs | {"is_favourite": favourite, "file_type": file_type}
- if entity.mime_type == "text/markdown":
- entity.document_type == "markdown"
- manager = FileManager()
- wrapper = io.TextIOWrapper(manager.get_file(entity))
- url_builder = (
- lambda label, base, end: f"/api/method/drive.api.docs.get_wiki_link?team={entity.team}&title={label}"
- )
- with wrapper as r:
- content = r.read()
- return_obj["raw_content"] = markdown.markdown(
- content,
- output_format="html",
- extensions=["extra", WikiLinkExtension(build_url=url_builder)],
- )
+ return_obj = entity | user_access | owner_info | breadcrumbs | {"is_favourite": favourite}
default = 0
if entity_name:
@@ -201,6 +177,11 @@ def get_entity_with_permissions(entity_name: str):
elif get_user_access(entity_name, team=1)["read"]:
default = -1
return_obj["share_count"] = default
+ if not entity.is_drive_file:
+ return_obj["file_type"] = map_ff_to_drive_type(entity)
+
+ return_obj["modifiable"] = entity["is_drive_file"] and not entity["details_doctype"] == "File"
+ return_obj["is_attachment"] = entity["is_drive_file"] and entity["details_doctype"] == "File"
return return_obj
@@ -225,7 +206,7 @@ def get_shared_with_list(entity: str):
fields=["user", "read", "write", "comment", "upload", "share"],
)
- owner = frappe.db.get_value("Drive File", entity, "owner")
+ owner = frappe.db.get_value("File", entity, "owner")
permissions.insert(
0,
frappe.db.get_value("User", owner, ["user_image", "full_name", "name as user"], as_dict=True),
@@ -238,26 +219,12 @@ def get_shared_with_list(entity: str):
return permissions
-def auto_delete_expired_perms():
- current_date = getdate()
- expired_documents = frappe.get_list(
- "Drive Permission",
- filters=[
- ["valid_until", "is", "set"],
- ["valid_until", "<", current_date],
- ],
- fields=["name", "valid_until"],
- )
- if expired_documents:
-
- def batch_delete_perms(docs):
- for d in docs:
- frappe.delete_doc("Drive Permission", d.name)
-
- frappe.enqueue(batch_delete_perms, docs=expired_documents)
-
-
def user_has_permission(doc, ptype, user=None, team=0):
+ if isinstance(doc, str):
+ doc = frappe.get_doc("File", doc)
+ if not doc.is_drive_file:
+ return ff_has_permission(doc, ptype, user)
+
if not user:
user = frappe.session.user
if user == "Administrator" or ptype == "create":
@@ -271,21 +238,6 @@ def user_has_permission(doc, ptype, user=None, team=0):
return bool(access[ptype])
-def user_has_permission_doc(doc, ptype, user=None):
- entity = frappe.get_value("Drive File", {"document": doc.name}, "name")
- if ptype == "create" or not entity:
- return True
- perm = user_has_permission(entity, ptype, user)
- return perm
-
-
-@frappe.whitelist()
-def toggle_allow_download(entity: str, val: bool):
- if not user_has_permission(entity, "share"):
- frappe.throw("You don't have permission for this action.", frappe.PermissionError)
- frappe.db.set_value("Drive File", entity, "allow_download", val)
-
-
def requires(perm):
def wrapped(fn):
def inner(*args, **kwargs):
diff --git a/drive/api/s3.py b/drive/api/s3.py
new file mode 100644
index 000000000..244fcbca0
--- /dev/null
+++ b/drive/api/s3.py
@@ -0,0 +1,11 @@
+from drive.api.permissions import user_has_permission
+import frappe
+from drive.utils.files import get_s3_url
+
+
+@frappe.whitelist(allow_guest=True)
+def fetch(path: str):
+ file = frappe.get_doc("File", {"file_url": get_s3_url(path)})
+ frappe.local.response["type"] = "redirect"
+ frappe.local.response["location"] = "/api/method/drive.api.files.get_file_content?entity_name=" + file.name
+ return
diff --git a/drive/api/scripts.py b/drive/api/scripts.py
index 527d6d905..7256408be 100644
--- a/drive/api/scripts.py
+++ b/drive/api/scripts.py
@@ -3,6 +3,8 @@
from drive.api.product import is_admin
from drive.utils import create_drive_file, default_team, get_home_folder, update_file_size
from drive.utils.files import FileManager
+from drive.api.files import delete_entities
+from datetime import date, timedelta
@frappe.whitelist()
@@ -38,8 +40,8 @@ def get_or_create_parent(parent_path, owner):
return home_folder
# Check if the parent folder exists
parent = frappe.get_value(
- "Drive File",
- {"path": (parent_path + "/") if parent_path else "", "team": team},
+ "File",
+ {"file_url": (parent_path + "/") if parent_path else "", "team": team},
"name",
)
if parent:
@@ -52,21 +54,21 @@ def get_or_create_parent(parent_path, owner):
# Now create this parent folder
new_parent = create_drive_file(
team,
- title=parent_path.strip("/").split("/")[-1],
+ file_name=parent_path.strip("/").split("/")[-1],
parent=grandparent,
mime_type="folder",
entity_path=lambda _: str(parent_path) + "/",
file_size=0,
- is_group=True,
+ is_folder=True,
owner=owner,
)
return new_parent.name
- for file, (file_size, last_modified, mime_type, actual_path) in sorted_files:
+ for file, (file_size, file_modified, mime_type, actual_path) in sorted_files:
parent_path = str(file.parent).strip("./")
parent = frappe.get_value(
- "Drive File",
- {"path": parent_path + "/" if parent_path else "", "team": team},
+ "File",
+ {"file_url": parent_path + "/" if parent_path else "", "team": team},
"name",
)
parent = get_or_create_parent(parent_path, frappe.session.user)
@@ -78,12 +80,34 @@ def get_or_create_parent(parent_path, owner):
parent,
mime_type,
lambda _: actual_path if mime_type != "folder" else actual_path.strip("/") + "/",
- last_modified=last_modified,
+ file_modified=file_modified,
file_size=file_size,
- is_group=mime_type == "folder",
+ is_folder=mime_type == "folder",
owner=frappe.session.user,
)
)
update_file_size(parent, file_size)
return files_added
+
+
+def auto_delete_from_trash():
+ days_before = (date.today() - timedelta(days=30)).isoformat()
+ result = frappe.db.get_all(
+ "File",
+ filters={"status": 0, "file_modified": ["<", days_before]},
+ fields=["name"],
+ )
+ delete_entities(result)
+
+
+def clear_deleted_files():
+ days_before = (date.today() + timedelta(days=30)).isoformat()
+ result = frappe.db.get_all(
+ "File",
+ filters={"status": -1, "modified": ["<", days_before]},
+ fields=["name"],
+ )
+ for entity in result:
+ doc = frappe.get_doc("File", entity, ignore_permissions=True)
+ doc.delete()
diff --git a/drive/api/storage.py b/drive/api/storage.py
index 100fa6b89..0fa03f792 100644
--- a/drive/api/storage.py
+++ b/drive/api/storage.py
@@ -1,11 +1,10 @@
import frappe
from pypika import functions as fn
-from drive.utils import default_team, get_file_type
-from drive.api.permissions import user_has_permission
+from drive.utils import default_team
MEGA_BYTE = 1024**2
-DriveFile = frappe.qb.DocType("Drive File")
+DriveFile = frappe.qb.DocType("File")
@frappe.whitelist()
@@ -13,34 +12,31 @@ def storage_breakdown(team: str, owned_only: bool):
limit = frappe.get_value("Drive Team", team, "quota" if owned_only else "storage") * MEGA_BYTE
filters = {
"team": team,
- "is_group": False,
- "is_active": 1,
+ "is_folder": False,
+ "status": 1,
"file_size": [">=", limit / 200],
}
if owned_only:
filters["owner"] = frappe.session.user
- # Get is_link because file type check requires it
entities = frappe.db.get_list(
- "Drive File",
+ "File",
filters=filters,
order_by="file_size desc",
- fields=["name", "title", "owner", "file_size", "mime_type", "is_group", "is_link"],
+ fields=["name", "file_name", "owner", "file_size", "file_type"],
)
- for r in entities:
- r["file_type"] = get_file_type(r)
query = (
frappe.qb.from_(DriveFile)
- .select(DriveFile.mime_type, fn.Sum(DriveFile.file_size).as_("file_size"))
- .where((DriveFile.is_group == 0) & (DriveFile.is_active == 1) & (DriveFile.team == team))
+ .select(DriveFile.file_type, fn.Sum(DriveFile.file_size).as_("file_size"))
+ .where((DriveFile.is_folder == 0) & (DriveFile.status == 1) & (DriveFile.team == team))
)
if owned_only:
query = query.where(DriveFile.owner == frappe.session.user)
return {
"limit": limit,
- "total": query.groupby(DriveFile.mime_type).run(as_dict=True),
+ "total": query.groupby(DriveFile.file_type).run(as_dict=True),
"entities": entities,
}
@@ -49,16 +45,17 @@ def storage_breakdown(team: str, owned_only: bool):
@default_team
def storage_bar_data(team: str | None = None, entity_name: str | None = None):
if not team:
- team = frappe.get_value("Drive File", entity_name, "team")
+ team = frappe.get_value("File", entity_name, "team")
if not team:
frappe.throw("Could not find team.", ValueError)
+
query = (
frappe.qb.from_(DriveFile)
.where(
(DriveFile.team == team)
- & (DriveFile.is_group == 0)
+ & (DriveFile.is_folder == 0)
& (DriveFile.owner == frappe.session.user)
- & (DriveFile.is_active == 1)
+ & (DriveFile.status == 1)
)
.select(fn.Coalesce(fn.Sum(DriveFile.file_size), 0).as_("total_size"))
)
diff --git a/drive/api/tags.py b/drive/api/tags.py
index 1ef608ccb..b61787f9c 100644
--- a/drive/api/tags.py
+++ b/drive/api/tags.py
@@ -33,7 +33,7 @@ def add_tag(entity: str, tag: str):
:param entity: Entity name
:param tag: Tag name
"""
- doc = frappe.get_doc("Drive File", entity)
+ doc = frappe.get_doc("File", entity)
doc.append("tags", {"tag": tag})
doc.save()
@@ -46,7 +46,7 @@ def get_entity_tags(entity: str):
:param entity: Entity name
"""
- entity = frappe.get_doc("Drive File", entity)
+ entity = frappe.get_doc("File", entity)
return map(
lambda x: frappe.db.get_value("Drive Tag", x.tag, ["name", "title", "color"], as_dict=1),
@@ -87,7 +87,7 @@ def edit_tag(tag: str, title: str, color: str):
:param color: Color to be update with
"""
doc = frappe.get_doc("Drive Tag", tag)
- doc.title = title
+ doc.file_name = title
doc.color = color
doc.save()
@@ -100,8 +100,7 @@ def remove_tag(entity: str, tag: str = None, all: bool = False):
:param entity: Entity name
:param tag: Tag name
"""
-
- entity_doc = frappe.get_doc("Drive File", entity)
+ entity_doc = frappe.get_doc("File", entity)
for tag_doc in entity_doc.tags:
if (tag_doc.tag == tag or all) and tag_doc.owner == frappe.session.user:
tag_doc.delete(ignore_permissions=True)
diff --git a/drive/drive/doctype/drive_comment/drive_comment.py b/drive/drive/doctype/drive_comment/drive_comment.py
index 2eeb294e7..1c0dd122b 100644
--- a/drive/drive/doctype/drive_comment/drive_comment.py
+++ b/drive/drive/doctype/drive_comment/drive_comment.py
@@ -31,15 +31,15 @@ def after_insert(self):
mention,
"Mention",
doc,
- f"{from_owner} mentioned you in a comment in {doc.title}",
+ f"{from_owner} mentioned you in a comment in {doc.file_name}",
)
frappe.sendmail(
recipients=[mention],
- subject=f"Frappe Drive - Comment in {doc.title}",
+ subject=f"Frappe Drive - Comment in {doc.file_name}",
template="drive_comment",
args={
- "message": f'{from_owner} mentioned you in a comment.',
- "doc": doc.title,
+ "message": f"{from_owner} mentioned you in a comment.",
+ "doc": doc.file_name,
"link": get_link(doc),
},
now=True,
diff --git a/drive/drive/doctype/drive_desktop_client/__init__.py b/drive/drive/doctype/drive_desktop_client/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/drive/drive/doctype/drive_desktop_client/drive_desktop_client.js b/drive/drive/doctype/drive_desktop_client/drive_desktop_client.js
deleted file mode 100644
index 94990cc97..000000000
--- a/drive/drive/doctype/drive_desktop_client/drive_desktop_client.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-// frappe.ui.form.on("Drive Desktop Client", {
-// refresh(frm) {
-
-// },
-// });
diff --git a/drive/drive/doctype/drive_desktop_client/drive_desktop_client.json b/drive/drive/doctype/drive_desktop_client/drive_desktop_client.json
deleted file mode 100644
index 00969fd7c..000000000
--- a/drive/drive/doctype/drive_desktop_client/drive_desktop_client.json
+++ /dev/null
@@ -1,61 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "autoname": "hash",
- "creation": "2025-09-26 12:28:54.454753",
- "doctype": "DocType",
- "engine": "InnoDB",
- "field_order": [
- "user",
- "team",
- "updates"
- ],
- "fields": [
- {
- "fieldname": "user",
- "fieldtype": "Link",
- "label": "User",
- "options": "User"
- },
- {
- "fieldname": "team",
- "fieldtype": "Link",
- "label": "Team",
- "options": "Drive Team"
- },
- {
- "description": "Queue for all the un-applied changes in the desktop.",
- "fieldname": "updates",
- "fieldtype": "Table",
- "label": "Updates",
- "options": "Drive File Update"
- }
- ],
- "grid_page_length": 50,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2026-02-05 15:54:03.437373",
- "modified_by": "Administrator",
- "module": "Drive",
- "name": "Drive Desktop Client",
- "naming_rule": "Random",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "row_format": "Dynamic",
- "sort_field": "creation",
- "sort_order": "DESC",
- "states": []
-}
diff --git a/drive/drive/doctype/drive_desktop_client/drive_desktop_client.py b/drive/drive/doctype/drive_desktop_client/drive_desktop_client.py
deleted file mode 100644
index dec191612..000000000
--- a/drive/drive/doctype/drive_desktop_client/drive_desktop_client.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class DriveDesktopClient(Document):
- pass
diff --git a/drive/drive/doctype/drive_desktop_client/test_drive_desktop_client.py b/drive/drive/doctype/drive_desktop_client/test_drive_desktop_client.py
deleted file mode 100644
index 3da822070..000000000
--- a/drive/drive/doctype/drive_desktop_client/test_drive_desktop_client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-from frappe.tests import IntegrationTestCase
-
-# On IntegrationTestCase, the doctype test records and all
-# link-field test record dependencies are recursively loaded
-# Use these module variables to add/remove to/from that list
-EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-
-
-class IntegrationTestDriveDesktopClient(IntegrationTestCase):
- """
- Integration tests for DriveDesktopClient.
- Use this class for testing interactions between multiple components.
- """
-
- pass
diff --git a/drive/drive/doctype/drive_disk_settings/drive_disk_settings.json b/drive/drive/doctype/drive_disk_settings/drive_disk_settings.json
index f045e7eb3..85d98ac94 100644
--- a/drive/drive/doctype/drive_disk_settings/drive_disk_settings.json
+++ b/drive/drive/doctype/drive_disk_settings/drive_disk_settings.json
@@ -1,145 +1,153 @@
{
- "actions": [],
- "allow_rename": 1,
- "creation": "2025-04-09 12:48:31.850910",
- "doctype": "DocType",
- "engine": "InnoDB",
- "field_order": [
- "general_section",
- "jwt_key",
- "preview_size",
- "disk_section",
- "flat",
- "root_folder",
- "team_prefix",
- "thumbnail_prefix",
- "s3_section",
- "enabled",
- "aws_key",
- "aws_secret",
- "bucket",
- "endpoint_url",
- "signature_version"
- ],
- "fields": [
- {
- "depends_on": "enabled",
- "fieldname": "aws_key",
- "fieldtype": "Data",
- "label": "AWS Key"
- },
- {
- "depends_on": "enabled",
- "fieldname": "aws_secret",
- "fieldtype": "Password",
- "label": "AWS Secret"
- },
- {
- "default": "0",
- "fieldname": "enabled",
- "fieldtype": "Check",
- "label": "Enabled"
- },
- {
- "depends_on": "enabled",
- "fieldname": "bucket",
- "fieldtype": "Data",
- "label": "S3 Bucket"
- },
- {
- "depends_on": "enabled",
- "description": "Example format: https://s3.ap-south-1.amazonaws.com",
- "fieldname": "endpoint_url",
- "fieldtype": "Data",
- "label": "Endpoint URL"
- },
- {
- "default": "s3v4",
- "depends_on": "enabled",
- "description": "Defaults to \"s3v4\". Some providers only support \"s3\".",
- "fieldname": "signature_version",
- "fieldtype": "Data",
- "label": "Signature Version"
- },
- {
- "fieldname": "s3_section",
- "fieldtype": "Section Break",
- "label": "S3"
- },
- {
- "default": "0",
- "description": "Legacy option.\n
\n
\nBy default, Drive stores files the way you see it. If this option is checked, the files are store flatly, with randomized names.\n",
- "fieldname": "flat",
- "fieldtype": "Check",
- "label": "Do not mimic folder/file structure"
- },
- {
- "fieldname": "general_section",
- "fieldtype": "Section Break",
- "label": "General"
- },
- {
- "default": ".thumbnails",
- "fieldname": "thumbnail_prefix",
- "fieldtype": "Data",
- "label": "Thumbnail prefix"
- },
- {
- "fieldname": "jwt_key",
- "fieldtype": "Data",
- "label": "JWT Key",
- "options": "Dangerous - if leaked, all files can be accessed."
- },
- {
- "default": "100",
- "description": "Used to configure maximum file sizes of in-browser previews (in megabytes).",
- "fieldname": "preview_size",
- "fieldtype": "Int",
- "in_list_view": 1,
- "label": "Preview size",
- "reqd": 1
- },
- {
- "fieldname": "disk_section",
- "fieldtype": "Section Break",
- "label": "Disk"
- },
- {
- "fieldname": "root_folder",
- "fieldtype": "Data",
- "label": "Drive Folder"
- },
- {
- "default": "team_name",
- "fieldname": "team_prefix",
- "fieldtype": "Select",
- "label": "Team Prefix",
- "options": "team_id\nteam_name\nnone"
- }
- ],
- "grid_page_length": 50,
- "index_web_pages_for_search": 1,
- "issingle": 1,
- "links": [],
- "modified": "2025-09-22 16:15:57.415076",
- "modified_by": "Administrator",
- "module": "Drive",
- "name": "Drive Disk Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "row_format": "Dynamic",
- "sort_field": "creation",
- "sort_order": "DESC",
- "states": []
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-04-09 12:48:31.850910",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "general_section",
+ "jwt_key",
+ "preview_size",
+ "use_drive_for_files",
+ "disk_section",
+ "flat",
+ "root_folder",
+ "team_prefix",
+ "thumbnail_prefix",
+ "s3_section",
+ "enabled",
+ "aws_key",
+ "aws_secret",
+ "bucket",
+ "endpoint_url",
+ "signature_version"
+ ],
+ "fields": [
+ {
+ "depends_on": "enabled",
+ "fieldname": "aws_key",
+ "fieldtype": "Data",
+ "label": "AWS Key"
+ },
+ {
+ "depends_on": "enabled",
+ "fieldname": "aws_secret",
+ "fieldtype": "Password",
+ "label": "AWS Secret"
+ },
+ {
+ "default": "0",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "label": "Enabled"
+ },
+ {
+ "depends_on": "enabled",
+ "fieldname": "bucket",
+ "fieldtype": "Data",
+ "label": "S3 Bucket"
+ },
+ {
+ "depends_on": "enabled",
+ "description": "Example format: https://s3.ap-south-1.amazonaws.com",
+ "fieldname": "endpoint_url",
+ "fieldtype": "Data",
+ "label": "Endpoint URL"
+ },
+ {
+ "default": "s3v4",
+ "depends_on": "enabled",
+ "description": "Defaults to \"s3v4\". Some providers only support \"s3\".",
+ "fieldname": "signature_version",
+ "fieldtype": "Data",
+ "label": "Signature Version"
+ },
+ {
+ "fieldname": "s3_section",
+ "fieldtype": "Section Break",
+ "label": "S3"
+ },
+ {
+ "default": "0",
+ "description": "Legacy option.\n
\n
\nBy default, Drive stores files the way you see it. If this option is checked, the files are store flatly, with randomized names.\n",
+ "fieldname": "flat",
+ "fieldtype": "Check",
+ "label": "Do not mimic folder/file structure"
+ },
+ {
+ "fieldname": "general_section",
+ "fieldtype": "Section Break",
+ "label": "General"
+ },
+ {
+ "default": ".thumbnails",
+ "fieldname": "thumbnail_prefix",
+ "fieldtype": "Data",
+ "label": "Thumbnail prefix"
+ },
+ {
+ "fieldname": "jwt_key",
+ "fieldtype": "Data",
+ "label": "JWT Key",
+ "options": "Dangerous - if leaked, all files can be accessed."
+ },
+ {
+ "default": "100",
+ "description": "Used to configure maximum file sizes of in-browser previews (in megabytes).",
+ "fieldname": "preview_size",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Preview size",
+ "reqd": 1
+ },
+ {
+ "fieldname": "disk_section",
+ "fieldtype": "Section Break",
+ "label": "Disk"
+ },
+ {
+ "fieldname": "root_folder",
+ "fieldtype": "Data",
+ "label": "Drive Folder"
+ },
+ {
+ "default": "team_name",
+ "fieldname": "team_prefix",
+ "fieldtype": "Select",
+ "label": "Team Prefix",
+ "options": "team_id\nteam_name\nnone"
+ },
+ {
+ "default": "0",
+ "description": "Make all uploaded files to be managed through Drive, instead of Framework.",
+ "fieldname": "use_drive_for_files",
+ "fieldtype": "Check",
+ "label": "Use Drive for file management"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2026-03-06 14:48:40.048946",
+ "modified_by": "Administrator",
+ "module": "Drive",
+ "name": "Drive Disk Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
}
diff --git a/drive/drive/doctype/drive_entity_activity_log/patches/initialize_creation.py b/drive/drive/doctype/drive_entity_activity_log/patches/initialize_creation.py
index 51823ad8e..15f1e7e96 100644
--- a/drive/drive/doctype/drive_entity_activity_log/patches/initialize_creation.py
+++ b/drive/drive/doctype/drive_entity_activity_log/patches/initialize_creation.py
@@ -8,7 +8,7 @@ def execute():
doc = frappe.new_doc("Drive Entity Activity Log")
doc.entity = i.name
doc.action_type = "create"
- doc.message = f"Created {i.title}"
+ doc.message = f"Created {i.file_name}"
doc.save()
frappe.db.set_value("Drive Entity Activity Log", doc.name, "owner", i.owner)
frappe.db.set_value("Drive Entity Activity Log", doc.name, "creation", i.creation)
diff --git a/drive/drive/doctype/drive_entity_activity_log/patches/share_creation.py b/drive/drive/doctype/drive_entity_activity_log/patches/share_creation.py
index f07578d54..8c71911b0 100644
--- a/drive/drive/doctype/drive_entity_activity_log/patches/share_creation.py
+++ b/drive/drive/doctype/drive_entity_activity_log/patches/share_creation.py
@@ -37,26 +37,26 @@ def create_activity_log(share):
def update_activity_log(log, share):
- title = frappe.db.get_value("Drive File", share.share_name, ["title"])
+ file_name = frappe.db.get_value("Drive File", share.share_name, ["file_name"])
owner_fullname = get_fullname(share.owner)
if share.everyone:
log.document_field = "everyone"
- message = f"{owner_fullname} shared {title} with everyone"
+ message = f"{owner_fullname} shared {file_name} with everyone"
log.old_value = False
log.new_value = True
elif share.public:
log.document_field = "public"
- message = f"{owner_fullname} shared {title} with publicly"
+ message = f"{owner_fullname} shared {file_name} with publicly"
log.old_value = False
log.new_value = True
elif share.user_doctype == "User Group":
log.document_field = "User Group"
- message = f"{owner_fullname} shared {title} with {share.user_name}"
+ message = f"{owner_fullname} shared {file_name} with {share.user_name}"
log.old_value = None
log.new_value = share.user_name
else:
log.document_field = "User"
- message = f"{owner_fullname} shared {title} with {share.user_name}"
+ message = f"{owner_fullname} shared {file_name} with {share.user_name}"
log.old_value = None
log.new_value = share.user_name
log.save()
diff --git a/drive/drive/doctype/drive_entity_log/drive_entity_log.py b/drive/drive/doctype/drive_entity_log/drive_entity_log.py
index 0443cc8dd..9ac5587b1 100644
--- a/drive/drive/doctype/drive_entity_log/drive_entity_log.py
+++ b/drive/drive/doctype/drive_entity_log/drive_entity_log.py
@@ -6,4 +6,13 @@
class DriveEntityLog(Document):
- pass
+ def validate(self):
+ """
+ Users can only create recent records for files they can access.
+ """
+ if frappe.session.user not in ["Administrator", self.user]:
+ raise frappe.PermissionError("You can only create a recent record for yourself.")
+
+ file = frappe.get_doc("File", self.entity_name)
+ if not user_has_permission(file, "read"):
+ raise frappe.PermissionError("You cannot create a recent record this file.")
diff --git a/drive/drive/doctype/drive_favourite/drive_favourite.json b/drive/drive/doctype/drive_favourite/drive_favourite.json
index 95061b15b..843592b07 100644
--- a/drive/drive/doctype/drive_favourite/drive_favourite.json
+++ b/drive/drive/doctype/drive_favourite/drive_favourite.json
@@ -22,12 +22,12 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Drive File",
- "options": "Drive File",
+ "options": "File",
"reqd": 1
}
],
"links": [],
- "modified": "2026-02-05 15:54:03.437373",
+ "modified": "2026-03-09 13:07:27.233796",
"modified_by": "Administrator",
"module": "Drive",
"name": "Drive Favourite",
diff --git a/drive/drive/doctype/drive_favourite/drive_favourite.py b/drive/drive/doctype/drive_favourite/drive_favourite.py
index 82f1e4115..50967db2b 100644
--- a/drive/drive/doctype/drive_favourite/drive_favourite.py
+++ b/drive/drive/doctype/drive_favourite/drive_favourite.py
@@ -2,8 +2,19 @@
# For license information, please see license.txt
# import frappe
+from drive.api.permissions import user_has_permission
from frappe.model.document import Document
+import frappe
class DriveFavourite(Document):
- pass
+ def validate(self):
+ """
+ Users can only create favourite files they can access.
+ """
+ if frappe.session.user not in ["Administrator", self.user]:
+ raise frappe.PermissionError("You can only create favourites for yourself.")
+
+ file = frappe.get_doc("File", self.entity)
+ if not user_has_permission(file, "read"):
+ raise frappe.PermissionError("You cannot favourite this file.")
diff --git a/drive/drive/doctype/drive_file/drive_file.js b/drive/drive/doctype/drive_file/drive_file.js
index 93a568979..98b8688df 100644
--- a/drive/drive/doctype/drive_file/drive_file.js
+++ b/drive/drive/doctype/drive_file/drive_file.js
@@ -1,13 +1,15 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on("Drive File", {
+frappe.ui.form.on('File', {
refresh: (frm) => {
- frm.add_custom_button(__("Permissions"), function () {
- window.open("/app/drive-permission?entity=" + frm.doc.name);
- });
- frm.add_custom_button(__("Open in Drive"), function () {
- window.open("/drive/g/" + frm.doc.name, "_blank");
- });
+ if (frm.doc.is_drive_file) {
+ frm.add_custom_button(__('Permissions'), function () {
+ window.open('/app/drive-permission?entity=' + frm.doc.name)
+ })
+ frm.add_custom_button(__('Open in Drive'), function () {
+ window.open('/drive/g/' + frm.doc.name, '_blank')
+ })
+ }
},
-});
+})
diff --git a/drive/drive/doctype/drive_file/drive_file.py b/drive/drive/doctype/drive_file/drive_file.py
index dd408b5c8..4708d27b4 100644
--- a/drive/drive/doctype/drive_file/drive_file.py
+++ b/drive/drive/doctype/drive_file/drive_file.py
@@ -9,12 +9,10 @@
from frappe.rate_limiter import rate_limit
from drive.api.activity import create_new_activity_log
-from drive.api.files import get_new_title
from drive.api.permissions import get_user_access, user_has_permission
from drive.api.product import invite_users
-from drive.utils import generate_upward_path, get_home_folder, update_file_size
+from drive.utils import generate_upward_path, get_home_folder, update_file_size, get_new_file_name
from drive.utils.files import FileManager
-from drive.utils.api import prettify_file
class DriveFile(Document):
@@ -43,7 +41,7 @@ def on_trash(self):
frappe.db.delete("Drive Notification", {"notif_doctype_name": self.name})
frappe.db.delete("Drive Entity Activity Log", {"entity": self.name})
- if self.is_group or self.document:
+ if self.is_folder or self.document:
for child in self.get_children():
has_write_access = user_has_permission(self, "write")
child.delete(ignore_permissions=has_write_access)
@@ -59,7 +57,7 @@ def after_delete(self):
def on_rollback(self):
if self.flags.file_created:
- shutil.rmtree(self.path) if self.is_group else self.path.unlink()
+ shutil.rmtree(self.path) if self.is_folder else self.path.unlink()
def get_children(self):
"""Return a generator that yields child Documents."""
@@ -102,7 +100,7 @@ def move(self, new_parent=None, new_team=None):
frappe.PermissionError,
)
if not (
- frappe.db.get_value("Drive File", new_parent, "is_group")
+ frappe.db.get_value("Drive File", new_parent, "is_folder")
or frappe.db.get_value("Drive File", new_parent, "doc")
):
frappe.throw(
@@ -123,7 +121,7 @@ def move(self, new_parent=None, new_team=None):
update_file_size(self.parent_entity, -self.file_size)
update_file_size(new_parent, +self.file_size)
self.parent_entity = new_parent
- self.title = get_new_title(self.title, new_parent, self.is_group, self.name)
+ self.title = get_new_file_name(self.title, new_parent, self.is_folder, self.name)
self.team = new_team
@@ -153,10 +151,10 @@ def rename(self: DriveFile, new_title: str):
if new_title == self.title:
return self
- validated_name = get_new_title(new_title, self.parent_entity, self.is_group, self.name)
+ validated_name = get_new_file_name(new_title, self.parent_entity, self.is_folder, self.name)
if new_title != validated_name:
return frappe.throw(
- f"{'Folder' if self.is_group else 'File'} '{new_title}' already exists\n Try '{validated_name}' ",
+ f"{'Folder' if self.is_folder else 'File'} '{new_title}' already exists\n Try '{validated_name}' ",
FileExistsError,
)
@@ -208,7 +206,7 @@ def permanent_delete(self):
frappe.throw("Not permitted", frappe.PermissionError)
self.is_active = -1
- if self.is_group:
+ if self.is_folder:
for child in self.get_children():
child.permanent_delete()
self.save()
diff --git a/drive/drive/doctype/drive_file_update/__init__.py b/drive/drive/doctype/drive_file_update/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/drive/drive/doctype/drive_file_update/drive_file_update.json b/drive/drive/doctype/drive_file_update/drive_file_update.json
deleted file mode 100644
index ec9acdaae..000000000
--- a/drive/drive/doctype/drive_file_update/drive_file_update.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "creation": "2025-09-26 12:29:17.008556",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": ["type", "entity"],
- "fields": [
- {
- "fieldname": "type",
- "fieldtype": "Select",
- "label": "Type",
- "options": "rename\nupload\ndelete\nmove\nedit"
- },
- {
- "fieldname": "entity",
- "fieldtype": "Link",
- "label": "Entity",
- "options": "Drive File"
- }
- ],
- "grid_page_length": 50,
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2025-09-26 12:29:57.626457",
- "modified_by": "Administrator",
- "module": "Drive",
- "name": "Drive File Update",
- "owner": "Administrator",
- "permissions": [],
- "row_format": "Dynamic",
- "sort_field": "creation",
- "sort_order": "DESC",
- "states": []
-}
diff --git a/drive/drive/doctype/drive_file_update/drive_file_update.py b/drive/drive/doctype/drive_file_update/drive_file_update.py
deleted file mode 100644
index 332d289a3..000000000
--- a/drive/drive/doctype/drive_file_update/drive_file_update.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class DriveFileUpdate(Document):
- pass
diff --git a/drive/drive/doctype/drive_settings/drive_settings.json b/drive/drive/doctype/drive_settings/drive_settings.json
index 6674c3dd2..3e5dc39cf 100644
--- a/drive/drive/doctype/drive_settings/drive_settings.json
+++ b/drive/drive/doctype/drive_settings/drive_settings.json
@@ -1,85 +1,78 @@
{
- "actions": [],
- "allow_rename": 1,
- "autoname": "field:user",
- "creation": "2025-04-24 17:38:30.853130",
- "doctype": "DocType",
- "engine": "InnoDB",
- "field_order": [
- "user",
- "single_click",
- "auto_detect_links",
- "writer_section",
- "writer_settings"
- ],
- "fields": [
- {
- "fieldname": "user",
- "fieldtype": "Link",
- "label": "User",
- "options": "User",
- "unique": 1
- },
- {
- "default": "1",
- "fieldname": "single_click",
- "fieldtype": "Check",
- "label": "Single Click"
- },
- {
- "default": "0",
- "fieldname": "auto_detect_links",
- "fieldtype": "Check",
- "label": "Auto Detect Links"
- },
- {
- "fieldname": "writer_section",
- "fieldtype": "Section Break",
- "label": "Writer"
- },
- {
- "default": "{\"font_family\":\"inter\",\"font_size\":\"15\",\"line_height\":\"1.5\", \"versioning\": 5}",
- "fieldname": "writer_settings",
- "fieldtype": "JSON",
- "label": "Writer Settings"
- }
- ],
- "grid_page_length": 50,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2026-02-05 15:54:03.437373",
- "modified_by": "Administrator",
- "module": "Drive",
- "name": "Drive Settings",
- "naming_rule": "By fieldname",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Drive User",
- "share": 1,
- "write": 1
- }
- ],
- "row_format": "Dynamic",
- "sort_field": "creation",
- "sort_order": "DESC",
- "states": []
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:user",
+ "creation": "2025-04-24 17:38:30.853130",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "auto_detect_links",
+ "writer_section",
+ "writer_settings"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User",
+ "unique": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "auto_detect_links",
+ "fieldtype": "Check",
+ "label": "Auto Detect Links"
+ },
+ {
+ "fieldname": "writer_section",
+ "fieldtype": "Section Break",
+ "label": "Writer"
+ },
+ {
+ "default": "{\"font_family\":\"inter\",\"font_size\":\"15\",\"line_height\":\"1.5\", \"versioning\": 5}",
+ "fieldname": "writer_settings",
+ "fieldtype": "JSON",
+ "label": "Writer Settings"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2026-03-09 13:07:34.980995",
+ "modified_by": "Administrator",
+ "module": "Drive",
+ "name": "Drive Settings",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Drive User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
}
diff --git a/drive/drive/doctype/drive_team/drive_team.py b/drive/drive/doctype/drive_team/drive_team.py
index 27ae9bc8c..67502cb4e 100644
--- a/drive/drive/doctype/drive_team/drive_team.py
+++ b/drive/drive/doctype/drive_team/drive_team.py
@@ -15,11 +15,12 @@ def after_insert(self):
"""Creates the file on disk"""
d = frappe.get_doc(
{
+ "is_drive_file": 1,
"name": self.name,
- "doctype": "Drive File",
- "title": f"Drive - {self.name}",
+ "doctype": "File",
+ "file_name": f"Drive - {self.name}",
"path": "",
- "is_group": 1,
+ "is_folder": 1,
"team": self.name,
}
)
@@ -65,6 +66,6 @@ def on_trash(self):
user_directory_path = files_dir / get_home_folder(self.name).path
if user_directory_path != files_dir:
shutil.rmtree(str(user_directory_path))
- frappe.db.delete("Drive File", {"team": self.name})
+ frappe.db.delete("File", {"team": self.name, "is_drive_file": 1})
except:
pass
diff --git a/drive/drive/doctype/drive_transfer/__init__.py b/drive/drive/doctype/drive_transfer/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/drive/drive/doctype/drive_transfer/drive_transfer.js b/drive/drive/doctype/drive_transfer/drive_transfer.js
deleted file mode 100644
index 7c3dbffa6..000000000
--- a/drive/drive/doctype/drive_transfer/drive_transfer.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-// frappe.ui.form.on("Drive Transfer", {
-// refresh(frm) {
-
-// },
-// });
diff --git a/drive/drive/doctype/drive_transfer/drive_transfer.json b/drive/drive/doctype/drive_transfer/drive_transfer.json
deleted file mode 100644
index eabdd58ca..000000000
--- a/drive/drive/doctype/drive_transfer/drive_transfer.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "creation": "2025-10-28 12:02:59.671617",
- "doctype": "DocType",
- "engine": "InnoDB",
- "field_order": [
- "title",
- "path",
- "file_size"
- ],
- "fields": [
- {
- "fieldname": "title",
- "fieldtype": "Data",
- "label": "Title"
- },
- {
- "fieldname": "path",
- "fieldtype": "Data",
- "label": "Path"
- },
- {
- "fieldname": "file_size",
- "fieldtype": "Int",
- "label": "File size"
- }
- ],
- "grid_page_length": 50,
- "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2025-10-28 12:03:25.633900",
- "modified_by": "Administrator",
- "module": "Drive",
- "name": "Drive Transfer",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "row_format": "Dynamic",
- "rows_threshold_for_grid_search": 20,
- "sort_field": "creation",
- "sort_order": "DESC",
- "states": []
-}
diff --git a/drive/drive/doctype/drive_transfer/drive_transfer.py b/drive/drive/doctype/drive_transfer/drive_transfer.py
deleted file mode 100644
index d772716d2..000000000
--- a/drive/drive/doctype/drive_transfer/drive_transfer.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-import frappe
-from frappe.model.document import Document
-from drive.utils.files import FileManager
-from drive.utils import get_default_team
-
-
-class DriveTransfer(Document):
- def after_delete(self):
- if self.path:
- FileManager().delete_file(frappe._dict(**self.as_dict(), team=get_default_team()))
diff --git a/drive/drive/doctype/drive_transfer/test_drive_transfer.py b/drive/drive/doctype/drive_transfer/test_drive_transfer.py
deleted file mode 100644
index e73379fac..000000000
--- a/drive/drive/doctype/drive_transfer/test_drive_transfer.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-from frappe.tests import IntegrationTestCase
-
-
-# On IntegrationTestCase, the doctype test records and all
-# link-field test record dependencies are recursively loaded
-# Use these module variables to add/remove to/from that list
-EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
-
-
-
-class IntegrationTestDriveTransfer(IntegrationTestCase):
- """
- Integration tests for DriveTransfer.
- Use this class for testing interactions between multiple components.
- """
-
- pass
diff --git a/drive/fixtures/custom_field.json b/drive/fixtures/custom_field.json
new file mode 100644
index 000000000..d4d8ee907
--- /dev/null
+++ b/drive/fixtures/custom_field.json
@@ -0,0 +1,524 @@
+[
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "section_break_nfot8",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "section_break_8",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Drive Properties",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-section_break_nfot8",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": null,
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": "0",
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "is_drive_file",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "section_break_nfot8",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Is Drive File",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-is_drive_file",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": null,
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "team",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "is_drive_file",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Drive Team",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-team",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "Drive Team",
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "mime_type",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "drive_team",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "MIME Type",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-mime_type",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "",
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": 1,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "status",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "team",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Status",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-status",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": null,
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "file_modified",
+ "fieldtype": "Datetime",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "status",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "File Modified",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-file_modified",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": null,
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "column_break_tapww",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "settings",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-column_break_tapww",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": null,
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "details_doctype",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "column_break_tapww",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Details Doctype",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-details_doctype",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "DocType",
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ },
+ {
+ "alignment": "",
+ "allow_in_quick_entry": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "button_color": "",
+ "collapsible": 0,
+ "collapsible_depends_on": null,
+ "columns": 0,
+ "default": null,
+ "depends_on": null,
+ "description": null,
+ "docstatus": 0,
+ "doctype": "Custom Field",
+ "dt": "File",
+ "fetch_from": null,
+ "fetch_if_empty": 0,
+ "fieldname": "details_docname",
+ "fieldtype": "Dynamic Link",
+ "hidden": 0,
+ "hide_border": 0,
+ "hide_days": 0,
+ "hide_seconds": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_preview": 0,
+ "in_standard_filter": 0,
+ "insert_after": "details_doctype",
+ "is_system_generated": 0,
+ "is_virtual": 0,
+ "label": "Details Doctype",
+ "length": 0,
+ "link_filters": null,
+ "mandatory_depends_on": null,
+ "module": null,
+ "name": "File-details_docname",
+ "no_copy": 0,
+ "non_negative": 0,
+ "options": "details_doctype",
+ "permlevel": 0,
+ "placeholder": null,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "print_width": null,
+ "read_only": 0,
+ "read_only_depends_on": null,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "show_dashboard": 0,
+ "sort_options": 0,
+ "translatable": 0,
+ "unique": 0,
+ "width": null
+ }
+]
\ No newline at end of file
diff --git a/drive/hooks.py b/drive/hooks.py
index a8735f62f..5792e5ee0 100644
--- a/drive/hooks.py
+++ b/drive/hooks.py
@@ -110,15 +110,17 @@
has_permission = {
"Drive File": "drive.api.permissions.user_has_permission",
- "Drive Document": "drive.api.permissions.user_has_permission_doc",
}
+
+after_upload_file = "drive.overrides.file.after_upload_file"
+# write_file = 'drive.overrides.file.write_file'
# DocType Class
# ---------------
# Override standard doctype classes
-# override_doctype_class = {
-# "ToDo": "custom_app.utils.overrides.CustomToDo"
-# }
+override_doctype_class = {
+ "File": "drive.overrides.file.File"
+}
# Document Events
# ---------------
@@ -138,8 +140,7 @@
# ---------------
scheduler_events = {
- "daily": ["drive.api.files.auto_delete_from_trash", "drive.api.files.clear_deleted_files"],
- "hourly": ["drive.api.permissions.auto_delete_expired_perms", "drive.api.files.auto_delete_transfers"],
+ "daily": ["drive.api.scripts.auto_delete_from_trash", "drive.api.scripts.clear_deleted_files"],
}
after_request = "drive.api.product.after_request"
diff --git a/drive/overrides/file.py b/drive/overrides/file.py
new file mode 100644
index 000000000..928e0badd
--- /dev/null
+++ b/drive/overrides/file.py
@@ -0,0 +1,371 @@
+from pathlib import Path
+import frappe
+from frappe.core.doctype.file.file import File as FrappeFile
+from frappe.core.doctype.file.utils import get_content_hash
+from frappe.utils import get_files_path, now
+
+from drive.api.files import get_file_type
+from drive.api.permissions import get_user_access, user_has_permission
+from drive.api.product import invite_users
+from drive.api.activity import create_new_activity_log
+
+from drive.utils import (
+ generate_upward_path,
+ get_home_folder,
+ update_file_size,
+ get_new_file_name,
+ validate_filename,
+ get_upload_path,
+)
+from drive.utils.files import FileManager
+import mimemapper
+
+
+def only_for_drive_files(func):
+ def inner(self, *args, **kwargs):
+ if self.is_drive_file:
+ return func(self, *args, **kwargs)
+ else:
+ parent_func = getattr(super(type(self), self), func.__name__, None)
+ if not parent_func:
+ raise ValueError("This function only exists for Drive files.")
+ return parent_func(*args, **kwargs)
+
+ return inner
+
+
+class File(FrappeFile):
+ @only_for_drive_files
+ def validate(self):
+ pass
+
+ @only_for_drive_files
+ def before_insert(self):
+ pass
+
+ @only_for_drive_files
+ def generate_content_hash(self):
+ pass
+
+ @only_for_drive_files
+ def get_full_path(self):
+ return get_files_path(self.file_url, private=True)
+
+ @only_for_drive_files
+ def set_folder_name(self):
+ pass
+
+ @only_for_drive_files
+ def autoname(self):
+ if getattr(self, "_name", None):
+ self.name = self._name
+ else:
+ self.name = frappe.generate_hash(length=10)
+
+ @only_for_drive_files
+ def set_is_private(self):
+ self.is_private = 1
+
+ @only_for_drive_files
+ def set_file_type(self):
+ pass
+
+ # Drive methods
+ def __update_modified(func):
+ """Used for functions that meaningfuly "modify" a file"""
+
+ def decorator(self, *args, **kwargs):
+ # Legacy code
+ res = func(self, *args, **kwargs)
+ self.db_set("file_modified", now())
+ return res
+
+ return decorator
+
+ @frappe.whitelist()
+ def share(
+ self,
+ user: str = None,
+ read: bool | None = None,
+ comment: bool | None = None,
+ share: bool | None = None,
+ upload: bool | None = None,
+ write: bool | None = None,
+ team: bool = False,
+ ):
+ if not user_has_permission(self, "share"):
+ frappe.throw("Not permitted to share", frappe.PermissionError)
+
+ # Clean out existing general records
+ if not user or team:
+ self.unshare("$GENERAL")
+
+ permission = frappe.db.get_value(
+ "Drive Permission",
+ {
+ "entity": self.name,
+ "user": user or "",
+ "team": team,
+ },
+ )
+ if not permission:
+ permission = frappe.new_doc("Drive Permission")
+ permission.update(
+ {
+ "user": user,
+ "team": team,
+ "entity": self.name,
+ }
+ )
+ else:
+ permission = frappe.get_doc("Drive Permission", permission)
+
+ # Create user
+ if user and not frappe.db.exists("User", user):
+ invite_users(user, auto=True)
+
+ levels = [
+ ["read", read],
+ ["comment", comment],
+ ["share", share],
+ ["upload", upload],
+ ["write", write],
+ ]
+ permission.update({l[0]: l[1] for l in levels if l[1] is not None})
+
+ permission.save(ignore_permissions=True)
+
+ @frappe.whitelist()
+ def unshare(self, user: str | None = None):
+ if not user_has_permission(self, "share"):
+ frappe.throw("Not permitted to unshare", frappe.PermissionError)
+
+ absolute_path = generate_upward_path(self.name)
+ for i in absolute_path:
+ if i["owner"] == user:
+ frappe.throw("User owns parent folder", frappe.PermissionError)
+
+ if user == "$GENERAL":
+ perm_names = frappe.db.get_list(
+ "Drive Permission",
+ {"entity": self.name},
+ or_filters={"user": "", "team": 1},
+ pluck="name",
+ )
+ for perm_name in perm_names:
+ frappe.delete_doc("Drive Permission", perm_name, ignore_permissions=True)
+
+ # If overriding perms of a parent folder, write out an explicit denial
+ public_access = get_user_access(self, "Guest")
+ team_access = get_user_access(self, team=1)
+ user = None
+ if public_access["read"]:
+ user = ""
+ elif team_access["read"]:
+ user = team_access["team"]
+
+ # Doesn't work as higher access "overrides" in get_user_access
+ if user is not None:
+ frappe.get_doc(
+ {
+ "doctype": "Drive Permission",
+ "user": user,
+ "entity": self.name,
+ "read": 0,
+ "comment": 0,
+ "share": 0,
+ "write": 0,
+ "team": bool(user),
+ }
+ ).insert()
+
+ else:
+ perm_name = frappe.db.get_value(
+ "Drive Permission",
+ {
+ "user": user,
+ "entity": self.name,
+ },
+ )
+ if perm_name:
+ frappe.delete_doc("Drive Permission", perm_name, ignore_permissions=True)
+
+ @__update_modified
+ def move(self, new_parent=None, new_team=None):
+ """
+ Move file to a new folder.
+ """
+ # Logic to decide new values
+ if new_team and not new_parent:
+ new_parent = new_parent or get_home_folder(new_team).name
+ elif new_parent and not new_team:
+ new_team = frappe.db.get_value("File", new_parent, "team")
+ elif not new_parent and not new_team:
+ new_team = self.team
+ new_parent = new_parent or get_home_folder(new_team).name
+
+ if new_parent == self.name:
+ frappe.throw(
+ "Cannot move into itself",
+ ValueError,
+ )
+ elif not user_has_permission(new_parent, "upload") or not user_has_permission(self, "write"):
+ frappe.throw("You don't have permission to move this file.", frappe.PermissionError)
+
+ if not (
+ frappe.db.get_value("File", new_parent, "is_folder")
+ # FIX: disable after redesign
+ or frappe.db.get_value("File", new_parent, "details_doctype")
+ ):
+ frappe.throw(
+ "Can only move into folders",
+ NotADirectoryError,
+ )
+
+ for child in self.get_children():
+ if child.name == self.name or child.name == new_parent:
+ frappe.throw(
+ "Cannot move into itself",
+ ValueError,
+ )
+ elif new_team != child.team:
+ child.move(self.name, new_team)
+
+ if new_parent != self.folder:
+ update_file_size(self.folder, -self.file_size)
+ update_file_size(new_parent, +self.file_size)
+ self.folder = new_parent
+ self.file_name = get_new_file_name(self.file_name, new_parent, self.is_folder, self.name)
+
+ self.team = new_team
+ not_in_disk = self.file_type == "Link" or not self.file_url
+
+ # Update all the children's paths
+ if not self.manager.flat and not not_in_disk:
+ new_path = self.manager.get_disk_path(self)
+ self.manager.move(self, str(new_path))
+ self.recursive_path_move(self.file_url, new_path)
+ self.file_url = new_path
+ self.save()
+
+ return frappe.get_value("File", new_parent, ["file_name", "team", "name", "folder"], as_dict=True)
+
+ def toggle_favourite(self):
+ existing_doc = frappe.db.exists(
+ {
+ "doctype": "Drive Favourite",
+ "entity": self.name,
+ "user": frappe.session.user,
+ }
+ )
+ if existing_doc:
+ frappe.delete_doc("Drive Favourite", existing_doc)
+ return False
+ else:
+ frappe.get_doc(
+ {
+ "doctype": "Drive Favourite",
+ "entity": self.name,
+ "user": frappe.session.user,
+ }
+ ).insert()
+ return True
+
+ @frappe.whitelist()
+ @__update_modified
+ def rename(self, new_file_name: str):
+ """
+ Rename file or folder
+ """
+ if not user_has_permission(self, "write"):
+ frappe.throw("You cannot rename this file", frappe.PermissionError)
+
+ if new_file_name == self.file_name:
+ return self
+ validate_filename(new_file_name, self.folder, self.file_type)
+
+ full_name = frappe.db.get_value("User", {"name": frappe.session.user}, ["full_name"])
+ message = f"{full_name} renamed {self.file_name} to {new_file_name}"
+ create_new_activity_log(
+ entity=self.name,
+ activity_type="rename",
+ activity_message=message,
+ document_field="file_name",
+ field_old_value=self.file_name,
+ field_new_value=new_file_name,
+ )
+ if len(new_file_name) > 140:
+ frappe.throw("Your file_name can't be more than 140 characters.")
+
+ self.file_name = new_file_name
+ path = self.manager.rename(self)
+ if self.file_url and self.mime_type != "frappe/slides":
+ self.recursive_path_move(self.file_url, path)
+
+ self.save()
+
+ def permanent_delete(self):
+ write_access = user_has_permission(self, "write")
+ parent_write_access = user_has_permission(self.folder, "write")
+ if not (write_access or parent_write_access):
+ frappe.throw("Not permitted", frappe.PermissionError)
+
+ self.status = -1
+ if self.is_folder:
+ for child in self.get_children():
+ child.permanent_delete()
+ self.save()
+
+ # Utils
+ @property
+ def manager(self):
+ return FileManager()
+
+ def recursive_path_move(self, old, new):
+ if new:
+ self.file_url = new
+ for child in self.get_children():
+ in_disk = child.file_type != "Link" and self.file_url
+ if in_disk:
+ child.recursive_path_move(child.file_url, str(Path(new) / Path(child.file_url).relative_to(old)))
+ self.save()
+
+ def get_children(self):
+ """Returns a generator that yields child Documents."""
+ child_names = frappe.get_list(self.doctype, filters={"folder": self.name}, pluck="name")
+ for name in child_names:
+ yield frappe.get_doc(self.doctype, name)
+
+
+def after_upload_file(doc):
+ if doc.is_drive_file:
+ return
+ settings = frappe.get_single("Drive Disk Settings")
+ if frappe.form_dict.library_file_name:
+ library_doc = frappe.get_doc("File", frappe.form_dict.library_file_name)
+ doc.is_drive_file = library_doc.is_drive_file
+ if doc.is_drive_file:
+ doc.file_type = library_doc.file_type
+ doc.file_size = library_doc.file_size
+ doc.modified = library_doc.modified
+ doc.details_doctype = "File"
+ doc.details_docname = frappe.form_dict.library_file_name
+ elif settings.use_drive_for_files and doc.attached_to_name:
+ doc.is_drive_file = 1
+ content_hash = get_content_hash(doc.content)
+ temp_path = get_upload_path("private/files", content_hash[:6] + "-" + doc.file_name)
+ with temp_path.open("wb") as f:
+ f.write(doc.content)
+
+ file_path = Path("private/files") / doc.attached_to_doctype / doc.attached_to_name / doc.file_name
+ save_folder = Path(frappe.get_site_path()) / file_path.parent
+ if not save_folder.exists():
+ save_folder.mkdir(parents=True)
+
+ doc.file_url = "/" + str(file_path)
+ doc.mime_type = mimemapper.get_mime_type(str(temp_path), native_first=False)
+ doc.file_type = get_file_type(doc.mime_type)
+ manager = FileManager()
+ manager.upload_file(temp_path, doc, create_thumbnail=False)
+
+ return doc
diff --git a/drive/patches.txt b/drive/patches.txt
index 659609d77..46ba65f56 100644
--- a/drive/patches.txt
+++ b/drive/patches.txt
@@ -10,3 +10,4 @@ drive.patches.new_writer #3
drive.patches.turn_on_flat_disk
drive.patches.add_modified_field
drive.patches.add_drive_user_role
+drive.patches.integrate_with_framework
diff --git a/drive/patches/folder_size.py b/drive/patches/folder_size.py
index 713c48a9a..36b587ccb 100644
--- a/drive/patches/folder_size.py
+++ b/drive/patches/folder_size.py
@@ -3,14 +3,14 @@
def scan(folder):
folder = frappe.get_doc("Drive File", folder)
- child_folders = frappe.get_list("Drive File", {"parent_entity": folder.name, "is_group": 1}, pluck="name")
+ child_folders = frappe.get_list("Drive File", {"folder": folder.name, "is_group": 1}, pluck="name")
for child in child_folders:
scan(child)
- sizes = frappe.get_list("Drive File", {"parent_entity": folder.name, "is_active": 1}, pluck="file_size")
+ sizes = frappe.get_list("Drive File", {"folder": folder.name, "is_active": 1}, pluck="file_size")
frappe.db.set_value("Drive File", folder.name, "file_size", sum(sizes), update_modified=False)
def execute():
- roots = frappe.get_list("Drive File", {"parent_entity": ""}, pluck="name")
+ roots = frappe.get_list("Drive File", {"folder": ""}, pluck="name")
for root in roots:
scan(root)
diff --git a/drive/patches/integrate_with_framework.py b/drive/patches/integrate_with_framework.py
new file mode 100644
index 000000000..88ca833b0
--- /dev/null
+++ b/drive/patches/integrate_with_framework.py
@@ -0,0 +1,103 @@
+"""
+Create `File` records of all existing `Drive File`s
+"""
+
+import frappe
+from drive.utils import MIME_LIST_MAP
+from drive.utils.files import get_s3_url
+
+
+def get_file_type(r):
+ if r["is_group"]:
+ return "Folder"
+ if r["is_link"]:
+ return "Link"
+ try:
+ return next(k for (k, v) in MIME_LIST_MAP.items() if r["mime_type"] in v)
+ except StopIteration:
+ return "Unknown"
+
+
+def execute(files=[]):
+ if not files:
+ root_files = frappe.get_all("Drive File", filters={"parent_entity": ""}, pluck="name")
+
+ is_remote = frappe.get_single("Drive Disk Settings").enabled
+ for file_id in root_files:
+ folder = frappe.get_doc("Drive File", file_id)
+ migrate_folder(folder, is_remote)
+
+
+def migrate_folder(folder, is_remote=False):
+ print(f"Migrating folder {folder}")
+ migrate_file(folder)
+
+ for child in folder.get_children():
+ if child.is_group or child.doc:
+ migrate_folder(child, is_remote)
+ else:
+ migrate_file(child, is_remote)
+
+
+def get_link(file, is_remote=False):
+ if file.mime_type == "frappe/slides" or not file.path:
+ return ""
+ elif file.is_link:
+ return file.path
+ elif is_remote:
+ return get_s3_url(file.path)
+
+ return "/private/files/" + file.path
+
+
+def migrate_file(file, is_remote=False):
+ if frappe.db.exists("File", {"is_drive_file": 1, "name": file.name}):
+ return
+
+ ff_file = frappe.get_doc(
+ {
+ "doctype": "File",
+ "is_drive_file": 1,
+ "_name": file.name,
+ "file_name": file.title,
+ "team": file.team,
+ "file_url": get_link(file, is_remote),
+ "folder": file.parent_entity,
+ "is_folder": file.is_group,
+ "file_size": file.file_size,
+ "last_modified": file._modified,
+ "status": file.is_active,
+ "mime_type": file.mime_type,
+ "is_private": 1,
+ }
+ )
+
+ if file.doc:
+ ff_file.special_file = "Writer Document"
+ ff_file.special_file_doc = file.doc
+
+ if file.mime_type == "frappe/slides":
+ ff_file.special_file = "Presentation"
+ ff_file.special_file_doc = file.path
+
+ # Attachment
+ if frappe.db.get_value("Drive File", file.parent_entity, "doc"):
+ ff_file.attached_to_doctype = "File"
+ ff_file.attached_to_name = file.parent_entity
+
+ # Calculate file type
+ ff_file.file_type = get_file_type(file.as_dict())
+
+ settings = {}
+ if file.color:
+ settings["color"] = file.color
+ if not file.allow_download:
+ settings["forbid_download"] = 1
+ ff_file.settings = settings
+ try:
+ ff_file.insert()
+ frappe.db.set_value("File", ff_file.name, "creation", file.creation, update_modified=False)
+ frappe.db.set_value("File", ff_file.name, "owner", file.owner, update_modified=False)
+ frappe.db.set_value("File", ff_file.name, "modified", file.modified, update_modified=False)
+ except Exception as e:
+ print(f"Error migrating file {file.name}: {e}")
diff --git a/drive/patches/remove_personal.py b/drive/patches/remove_personal.py
index 8fc77d670..09bd3dfe0 100644
--- a/drive/patches/remove_personal.py
+++ b/drive/patches/remove_personal.py
@@ -8,7 +8,7 @@ def execute():
print(
"This migration to a beta release might CORRUPT your data. Do NOT run this before taking a complete backup. You have two minutes left to cancel this deployment. "
)
- time.sleep(120)
+ time.sleep(120)
frappe.reload_doc("Drive", "doctype", "Drive Disk Settings")
doc = frappe.get_single("Drive Disk Settings")
@@ -52,14 +52,14 @@ def execute():
for f in frappe.get_all(
"Drive File",
filters={"is_private": 1},
- fields=["name", "is_private", "owner", "parent_entity"],
+ fields=["name", "is_private", "owner", "folder"],
):
try:
frappe.db.set_value("Drive File", f.name, "team", MAP[f.owner], update_modified=False)
# For root elements, change parent folder
- if not frappe.db.get_value("Drive File", f.parent_entity, "parent_entity"):
- new_parent = frappe.db.get_value("Drive File", {"team": MAP[f.owner], "parent_entity": None}, "name")
- frappe.db.set_value("Drive File", f.name, "parent_entity", new_parent)
+ if not frappe.db.get_value("Drive File", f.folder, "folder"):
+ new_parent = frappe.db.get_value("Drive File", {"team": MAP[f.owner], "folder": None}, "name")
+ frappe.db.set_value("Drive File", f.name, "folder", new_parent)
except KeyError:
print(f"There was an issue with the file {f} owned by {f.owner}")
diff --git a/drive/patches/team_restructure.py b/drive/patches/team_restructure.py
index 33543f7dc..902e630b3 100644
--- a/drive/patches/team_restructure.py
+++ b/drive/patches/team_restructure.py
@@ -75,7 +75,7 @@ def execute():
frappe.db.set_value(
"Drive File",
name,
- "parent_entity",
+ "folder",
home_folder,
update_modified=False,
)
@@ -83,7 +83,7 @@ def execute():
frappe.db.set_value(
"Drive File",
name,
- "parent_entity",
+ "folder",
translate[k["parent_drive_entity"]],
update_modified=False,
)
@@ -118,7 +118,7 @@ def execute():
RENAME_MAP = {
"Drive Notification": "notif_doctype_name",
"Drive Favourite": "entity",
- "Drive Document Version": "parent_entity",
+ "Drive Document Version": "folder",
"Drive Entity Activity Log": "entity",
"Drive Entity Log": "entity_name",
}
diff --git a/drive/public/js/FileUploader.vue b/drive/public/js/FileUploader.vue
index 6c0fb3076..46386cf57 100644
--- a/drive/public/js/FileUploader.vue
+++ b/drive/public/js/FileUploader.vue
@@ -66,7 +66,7 @@