diff --git a/drive/api/files.py b/drive/api/files.py index 5c3a37ad7..0e1c5041d 100644 --- a/drive/api/files.py +++ b/drive/api/files.py @@ -8,6 +8,7 @@ import jwt import magic import mimemapper +import shutil from pypika import Order from werkzeug.utils import secure_filename, send_file from werkzeug.wrappers import Response @@ -200,7 +201,7 @@ def create_document_entity(title, personal, team, content, parent=None): ) drive_doc = frappe.new_doc("Drive Document") drive_doc.title = title - drive_doc.content = content + drive_doc.raw_content = content drive_doc.version = 2 drive_doc.save() @@ -541,24 +542,27 @@ def remove_or_restore(entity_names): def depth_zero_toggle_is_active(doc): if doc.is_active: flag = 0 - manager.move_to_trash(doc) + if doc.path: + manager.move_to_trash(doc) else: storage_data = storage_bar_data(doc.team) if (storage_data["limit"] - storage_data["total_size"]) < doc.file_size: frappe.throw("You're out of storage!", ValueError) - manager.restore(doc) + if doc.path: + manager.restore(doc) flag = 1 doc.is_active = flag - folder_size = frappe.db.get_value("Drive File", doc.parent_entity, "file_size") - frappe.db.set_value( + doc.save() + + if doc.parent_entity: + folder_size = frappe.db.get_value("Drive File", doc.parent_entity, "file_size") + frappe.db.set_value( "Drive File", doc.parent_entity, "file_size", folder_size + doc.file_size * (1 if flag else -1), - ) - - doc.save() + ) for entity in entity_names: doc = frappe.get_doc("Drive File", entity) @@ -681,6 +685,79 @@ def move(entity_names, new_parent=None, is_private=None): return res +@frappe.whitelist() +def create_copy(entity_name, parent=None): + """ + Creates a copy of file/files. + + :param entity_name: Name of the file to copy. + :param parent: Optional. Parent folder. + """ + original_doc = frappe.get_doc("Drive File", entity_name) + home_folder = get_home_folder(original_doc.team) + if original_doc.is_group or original_doc.is_link: + frappe.throw("Copying folders or links is not supported.") + + destination_parent = parent or original_doc.parent_entity + if not user_has_permission(entity_name, "read"): + frappe.throw( + "You do not have permission to view the original file.", + frappe.PermissionError + ) + if not user_has_permission(destination_parent, "upload"): + frappe.throw( + "You do not have permission to create a file in the destination folder.", + frappe.PermissionError + ) + + storage_data = storage_bar_data(original_doc.team) + if (storage_data["limit"] - storage_data["total_size"]) < original_doc.file_size: + frappe.throw("Not enough storage space to create a copy.", ValueError) + + new_title = get_new_title(original_doc.title, destination_parent) + new_entity = None + + if original_doc.document: + original_content = frappe.get_value( + "Drive Document", original_doc.document, "raw_content" + ) + + new_entity = create_document_entity( + new_title, + original_doc.is_private, + original_doc.team, + original_content, + destination_parent + ) + frappe.db.set_value("Drive File", new_entity.name, "title", new_title) + + else: + manager = FileManager() + + original_relative_path = manager.get_disk_path(original_doc, home_folder, embed=0) + + new_entity = create_drive_file( + original_doc.team, + original_doc.is_private, + new_title, + destination_parent, + original_doc.mime_type, + lambda entity: manager.get_disk_path(entity, home_folder, embed=0), + original_doc.file_size + ) + + new_relative_path = new_entity.path + base_private_path = frappe.get_site_path("private", "files") + absolute_original_path = os.path.join(base_private_path, original_relative_path) + absolute_new_path = os.path.join(base_private_path, new_relative_path) + + if not os.path.exists(os.path.dirname(absolute_new_path)): + os.makedirs(os.path.dirname(absolute_new_path)) + + shutil.copy2(absolute_original_path, absolute_new_path) + + update_file_size(destination_parent, original_doc.file_size) + # return new_entity @frappe.whitelist() def search(query, team): diff --git a/drive/utils/__init__.py b/drive/utils/__init__.py index 48d86d52b..9885a21f7 100644 --- a/drive/utils/__init__.py +++ b/drive/utils/__init__.py @@ -240,17 +240,21 @@ def get_file_type(r): except StopIteration: return "Unknown" +def update_file_size(folder_name, size_change): + if not folder_name or not isinstance(size_change, (int, float)): + return -def update_file_size(entity, delta): - doc = frappe.get_doc("Drive File", entity) - while doc.parent_entity: - doc.file_size += delta - doc.save(ignore_permissions=True) - doc = frappe.get_doc("Drive File", doc.parent_entity) - # Update root - doc.file_size += delta - doc.save(ignore_permissions=True) - + frappe.db.sql(""" + UPDATE `tabDrive File` + SET + `file_size` = `file_size` + %(size_change)s, + `modified` = %(now)s + WHERE `name` = %(folder_name)s + """, { + "size_change": size_change, + "folder_name": folder_name, + "now": frappe.utils.now() + }, auto_commit=True) def if_folder_exists(team, folder_name, parent, personal): values = { diff --git a/frontend/src/components/GenericPage.vue b/frontend/src/components/GenericPage.vue index 097fcc408..c74beeba6 100644 --- a/frontend/src/components/GenericPage.vue +++ b/frontend/src/components/GenericPage.vue @@ -2,7 +2,7 @@ @@ -69,7 +69,7 @@ import Navbar from "@/components/Navbar.vue" import NoFilesSection from "@/components/NoFilesSection.vue" import ErrorPage from "@/components/ErrorPage.vue" import { getLink, pasteObj } from "@/utils/files" -import { toggleFav, clearRecent } from "@/resources/files" +import { toggleFav, clearRecent, copyFile } from "@/resources/files" import { allUsers } from "@/resources/permissions" import { entitiesDownload } from "@/utils/download" import FileUploader from "@/components/FileUploader.vue" @@ -95,6 +95,7 @@ import LucideShare2 from "~icons/lucide/share-2" import LucideSquarePen from "~icons/lucide/square-pen" import LucideStar from "~icons/lucide/star" import LucideTrash from "~icons/lucide/trash" +import LucideCopy from "~icons/lucide/copy" import emitter from "../emitter" const props = defineProps({ @@ -123,12 +124,22 @@ watch( store.commit("setListResource", props.getEntities) const selections = ref(new Set()) -const selectedEntitities = computed( - () => - props.getEntities.data?.filter?.(({ name }) => - selections.value.has(name) - ) || [] -) +const selectedEntities = computed(() => { + if (selections.value.size > 0) { + return ( + props.getEntities.data?.filter?.(({ name }) => + selections.value.has(name) + ) || [] + ) + } + if (activeEntity.value) { + const exists = props.getEntities.data?.find( + (e) => e.name === activeEntity.value.name + ) + return exists ? [activeEntity.value] : [] + } + return [] +}) const verifyAccess = computed(() => props.verify?.data || !props.verify) watchEffect(() => { @@ -176,6 +187,8 @@ const onDrop = (targetFile, draggedItem) => { // Action Items const actionItems = computed(() => { + const invalidCopyItem = selectedEntities.value.some(e => e.is_group || e.is_link); + if (route.name === "Trash") { return [ { @@ -243,7 +256,22 @@ const actionItems = computed(() => { label: __("Rename"), icon: LucideSquarePen, action: () => (dialog.value = "rn"), - isEnabled: (e) => e.write, + isEnabled: (e) => e.write, + }, + { + label: __("Create Copy"), + icon: LucideCopy, + action: (entities) => { + entities.forEach((entity) => { + copyFile.submit({ + entity, + parent: entity.parent_entity, + }); + }); + }, + isEnabled: (e) => e.write && !invalidCopyItem, + multi: true, + important: true, }, { label: __("Show Info"), diff --git a/frontend/src/resources/files.js b/frontend/src/resources/files.js index 9697dabc4..d7a12ddfa 100644 --- a/frontend/src/resources/files.js +++ b/frontend/src/resources/files.js @@ -262,6 +262,27 @@ export const createDocument = createResource({ makeParams: (params) => params, }) +export const copyFile = createResource({ + url: "drive.api.files.create_copy", + + makeParams(data) { + return { + entity_name: data.entity.name, + parent: data.parent, + }; + }, + + onSuccess() { toast({title: "Copied successfully!",});}, + + onError(error) { + toast({ + title: "Error copying file!", + description: error.message || "Something went wrong.", + variant: "error", + }); + }, +}); + export const togglePersonal = createResource({ method: "POST", url: "drive.api.files.call_controller_method",