Merge pull request #33753 from frappe/drive-integration

feat: allow adding other methods of file upload
This commit is contained in:
Suraj Shetty 2025-09-01 12:06:06 +05:30 committed by GitHub
commit 98eaa04b2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 63 additions and 10 deletions

View file

@ -17,7 +17,13 @@ from frappe.database.schema import SPECIAL_CHAR_PATTERN
from frappe.exceptions import DoesNotExistError
from frappe.model.document import Document
from frappe.permissions import SYSTEM_USER_ROLE, get_doctypes_with_read
from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url
from frappe.utils import (
call_hook_method,
cint,
get_files_path,
get_hook_method,
get_url,
)
from frappe.utils.file_manager import is_safe_path
from frappe.utils.image import optimize_image, strip_exif_data
@ -31,7 +37,7 @@ from .utils import *
exclude_from_linked_with = True
ImageFile.LOAD_TRUNCATED_IMAGES = True
URL_PREFIXES = ("http://", "https://")
URL_PREFIXES = ("http://", "https://", "/api/method/")
FILE_ENCODING_OPTIONS = ("utf-8-sig", "utf-8", "windows-1250", "windows-1252")
@ -139,7 +145,10 @@ class File(Document):
return
if not self.attached_to_name or not isinstance(self.attached_to_name, str | int):
frappe.throw(_("Attached To Name must be a string or an integer"), frappe.ValidationError)
frappe.throw(
_("Attached To Name must be a string or an integer"),
frappe.ValidationError,
)
if self.attached_to_field and SPECIAL_CHAR_PATTERN.search(self.attached_to_field):
frappe.throw(_("The fieldname you've specified in Attached To Field is invalid"))
@ -213,8 +222,8 @@ class File(Document):
if self.is_remote_file or not self.file_url:
return
if not self.file_url.startswith(("/files/", "/private/files/")):
# Probably an invalid URL since it doesn't start with http either
if not self.file_url.startswith(("/files/", "/private/files/", "/api/method/")):
# Probably an invalid URL since it doesn't start with http and isn't an internal URL either
frappe.throw(
_("URL must start with http:// or https://"),
title=_("Invalid URL"),
@ -318,7 +327,9 @@ class File(Document):
if current_attachment_count >= attachment_limit:
frappe.throw(
_("Maximum Attachment Limit of {0} has been reached for {1} {2}.").format(
frappe.bold(attachment_limit), self.attached_to_doctype, self.attached_to_name
frappe.bold(attachment_limit),
self.attached_to_doctype,
self.attached_to_name,
),
exc=AttachmentLimitReached,
title=_("Attachment Limit Reached"),
@ -372,7 +383,10 @@ class File(Document):
return
if self.file_type not in allowed_extensions.splitlines():
frappe.throw(_("File type of {0} is not allowed").format(self.file_type), exc=FileTypeNotAllowed)
frappe.throw(
_("File type of {0} is not allowed").format(self.file_type),
exc=FileTypeNotAllowed,
)
def validate_duplicate_entry(self):
if not self.flags.ignore_duplicate_entry_error and not self.is_folder:
@ -407,7 +421,8 @@ class File(Document):
def set_file_name(self):
if not self.file_name and not self.file_url:
frappe.throw(
_("Fields `file_name` or `file_url` must be set for File"), exc=frappe.MandatoryError
_("Fields `file_name` or `file_url` must be set for File"),
exc=frappe.MandatoryError,
)
elif not self.file_name and self.file_url:
self.file_name = self.file_url.split("/")[-1]
@ -779,6 +794,9 @@ class File(Document):
frappe.clear_messages()
def set_is_private(self):
if self.is_private:
return
if self.file_url:
self.is_private = cint(self.file_url.startswith("/private"))

View file

@ -163,6 +163,23 @@
</svg>
<div class="mt-1">{{ __("Google Drive") }}</div>
</button>
<template v-for="option in additional_upload_handlers">
<button class="btn btn-file-upload" @click="option.wrappedAction">
<svg
v-if="typeof option.icon === 'string'"
v-html="option.icon"
width="30"
height="30"
/>
<component
v-else-if="option.icon"
:is="option.icon"
width="30"
height="30"
/>
<div class="mt-1">{{ option.label }}</div>
</button>
</template>
</div>
<div class="mt-3 text-center" v-if="upload_notes">
{{ upload_notes }}
@ -291,6 +308,9 @@ const props = defineProps({
allow_google_drive: {
default: true,
},
additional_upload_handlers: {
default: [],
},
});
// variables
@ -645,7 +665,9 @@ function upload_file(file, i) {
if (file.file_url) {
form_data.append("file_url", file.file_url);
}
if (file.file_size) {
form_data.append("file_size", file.file_size);
}
if (file.file_name) {
form_data.append("file_name", file.file_name);
}
@ -762,6 +784,7 @@ watch(
defineExpose({
files,
add_files,
upload_file,
upload_files,
toggle_all_private,
wrapper_ready,

View file

@ -3,6 +3,7 @@ import FileUploaderComponent from "./FileUploader.vue";
import { watch } from "vue";
class FileUploader {
static UploadOptions = [];
constructor({
wrapper,
method,
@ -65,6 +66,17 @@ class FileUploader {
allow_toggle_private,
allow_toggle_optimize,
allow_google_drive,
additional_upload_handlers: this.constructor.UploadOptions.map((k) => ({
...k,
wrappedAction: () =>
k.action({
dialog: this.dialog,
uploader: this.uploader,
doctype,
docname,
fieldname,
}),
})),
});
SetVueGlobals(app);
this.uploader = app.mount(this.wrapper);

View file

@ -417,7 +417,7 @@ def add_attachments(doctype, name, attachments):
def is_safe_path(path: str) -> bool:
if path.startswith(("http://", "https://")):
if path.startswith(("http://", "https://", "/api/method/")):
return True
basedir = frappe.get_site_path()