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",