mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
feat: implement file extension blacklist for upload security (#27540)
This commit is contained in:
@@ -371,6 +371,12 @@ UPLOAD_IMAGE_FILE_SIZE_LIMIT=10
|
|||||||
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
|
UPLOAD_VIDEO_FILE_SIZE_LIMIT=100
|
||||||
UPLOAD_AUDIO_FILE_SIZE_LIMIT=50
|
UPLOAD_AUDIO_FILE_SIZE_LIMIT=50
|
||||||
|
|
||||||
|
# Comma-separated list of file extensions blocked from upload for security reasons.
|
||||||
|
# Extensions should be lowercase without dots (e.g., exe,bat,sh,dll).
|
||||||
|
# Empty by default to allow all file types.
|
||||||
|
# Recommended: exe,bat,cmd,com,scr,vbs,ps1,msi,dll
|
||||||
|
UPLOAD_FILE_EXTENSION_BLACKLIST=
|
||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
MULTIMODAL_SEND_FORMAT=base64
|
MULTIMODAL_SEND_FORMAT=base64
|
||||||
PROMPT_GENERATION_MAX_TOKENS=512
|
PROMPT_GENERATION_MAX_TOKENS=512
|
||||||
|
|||||||
@@ -331,6 +331,31 @@ class FileUploadConfig(BaseSettings):
|
|||||||
default=10,
|
default=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
inner_UPLOAD_FILE_EXTENSION_BLACKLIST: str = Field(
|
||||||
|
description=(
|
||||||
|
"Comma-separated list of file extensions that are blocked from upload. "
|
||||||
|
"Extensions should be lowercase without dots (e.g., 'exe,bat,sh,dll'). "
|
||||||
|
"Empty by default to allow all file types."
|
||||||
|
),
|
||||||
|
validation_alias=AliasChoices("UPLOAD_FILE_EXTENSION_BLACKLIST"),
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
@computed_field # type: ignore[misc]
|
||||||
|
@property
|
||||||
|
def UPLOAD_FILE_EXTENSION_BLACKLIST(self) -> set[str]:
|
||||||
|
"""
|
||||||
|
Parse and return the blacklist as a set of lowercase extensions.
|
||||||
|
Returns an empty set if no blacklist is configured.
|
||||||
|
"""
|
||||||
|
if not self.inner_UPLOAD_FILE_EXTENSION_BLACKLIST:
|
||||||
|
return set()
|
||||||
|
return {
|
||||||
|
ext.strip().lower().strip(".")
|
||||||
|
for ext in self.inner_UPLOAD_FILE_EXTENSION_BLACKLIST.split(",")
|
||||||
|
if ext.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class HttpConfig(BaseSettings):
|
class HttpConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ class UnsupportedFileTypeError(BaseHTTPException):
|
|||||||
code = 415
|
code = 415
|
||||||
|
|
||||||
|
|
||||||
|
class BlockedFileExtensionError(BaseHTTPException):
|
||||||
|
error_code = "file_extension_blocked"
|
||||||
|
description = "The file extension is blocked for security reasons."
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
class TooManyFilesError(BaseHTTPException):
|
class TooManyFilesError(BaseHTTPException):
|
||||||
error_code = "too_many_files"
|
error_code = "too_many_files"
|
||||||
description = "Only one file is allowed."
|
description = "Only one file is allowed."
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import services
|
|||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from constants import DOCUMENT_EXTENSIONS
|
from constants import DOCUMENT_EXTENSIONS
|
||||||
from controllers.common.errors import (
|
from controllers.common.errors import (
|
||||||
|
BlockedFileExtensionError,
|
||||||
FilenameNotExistsError,
|
FilenameNotExistsError,
|
||||||
FileTooLargeError,
|
FileTooLargeError,
|
||||||
NoFileUploadedError,
|
NoFileUploadedError,
|
||||||
@@ -83,6 +84,8 @@ class FileApi(Resource):
|
|||||||
raise FileTooLargeError(file_too_large_error.description)
|
raise FileTooLargeError(file_too_large_error.description)
|
||||||
except services.errors.file.UnsupportedFileTypeError:
|
except services.errors.file.UnsupportedFileTypeError:
|
||||||
raise UnsupportedFileTypeError()
|
raise UnsupportedFileTypeError()
|
||||||
|
except services.errors.file.BlockedFileExtensionError as blocked_extension_error:
|
||||||
|
raise BlockedFileExtensionError(blocked_extension_error.description)
|
||||||
|
|
||||||
return upload_file, 201
|
return upload_file, 201
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,7 @@ class FileTooLargeError(BaseServiceError):
|
|||||||
|
|
||||||
class UnsupportedFileTypeError(BaseServiceError):
|
class UnsupportedFileTypeError(BaseServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BlockedFileExtensionError(BaseServiceError):
|
||||||
|
description = "File extension '{extension}' is not allowed for security reasons"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from models import Account
|
|||||||
from models.enums import CreatorUserRole
|
from models.enums import CreatorUserRole
|
||||||
from models.model import EndUser, UploadFile
|
from models.model import EndUser, UploadFile
|
||||||
|
|
||||||
from .errors.file import FileTooLargeError, UnsupportedFileTypeError
|
from .errors.file import BlockedFileExtensionError, FileTooLargeError, UnsupportedFileTypeError
|
||||||
|
|
||||||
PREVIEW_WORDS_LIMIT = 3000
|
PREVIEW_WORDS_LIMIT = 3000
|
||||||
|
|
||||||
@@ -59,6 +59,10 @@ class FileService:
|
|||||||
if len(filename) > 200:
|
if len(filename) > 200:
|
||||||
filename = filename.split(".")[0][:200] + "." + extension
|
filename = filename.split(".")[0][:200] + "." + extension
|
||||||
|
|
||||||
|
# check if extension is in blacklist
|
||||||
|
if extension and extension in dify_config.UPLOAD_FILE_EXTENSION_BLACKLIST:
|
||||||
|
raise BlockedFileExtensionError(f"File extension '.{extension}' is not allowed for security reasons")
|
||||||
|
|
||||||
if source == "datasets" and extension not in DOCUMENT_EXTENSIONS:
|
if source == "datasets" and extension not in DOCUMENT_EXTENSIONS:
|
||||||
raise UnsupportedFileTypeError()
|
raise UnsupportedFileTypeError()
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from configs import dify_config
|
|||||||
from models import Account, Tenant
|
from models import Account, Tenant
|
||||||
from models.enums import CreatorUserRole
|
from models.enums import CreatorUserRole
|
||||||
from models.model import EndUser, UploadFile
|
from models.model import EndUser, UploadFile
|
||||||
from services.errors.file import FileTooLargeError, UnsupportedFileTypeError
|
from services.errors.file import BlockedFileExtensionError, FileTooLargeError, UnsupportedFileTypeError
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|
||||||
|
|
||||||
@@ -943,3 +943,150 @@ class TestFileService:
|
|||||||
|
|
||||||
# Should have the signed URL when source_url is empty
|
# Should have the signed URL when source_url is empty
|
||||||
assert upload_file2.source_url == "https://example.com/signed-url"
|
assert upload_file2.source_url == "https://example.com/signed-url"
|
||||||
|
|
||||||
|
# Test file extension blacklist
|
||||||
|
def test_upload_file_blocked_extension(
|
||||||
|
self, db_session_with_containers, engine, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test file upload with blocked extension.
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
|
||||||
|
|
||||||
|
# Mock blacklist configuration by patching the inner field
|
||||||
|
with patch.object(dify_config, "inner_UPLOAD_FILE_EXTENSION_BLACKLIST", "exe,bat,sh"):
|
||||||
|
filename = "malware.exe"
|
||||||
|
content = b"test content"
|
||||||
|
mimetype = "application/x-msdownload"
|
||||||
|
|
||||||
|
with pytest.raises(BlockedFileExtensionError):
|
||||||
|
FileService(engine).upload_file(
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
mimetype=mimetype,
|
||||||
|
user=account,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_upload_file_blocked_extension_case_insensitive(
|
||||||
|
self, db_session_with_containers, engine, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test file upload with blocked extension (case insensitive).
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
|
||||||
|
|
||||||
|
# Mock blacklist configuration by patching the inner field
|
||||||
|
with patch.object(dify_config, "inner_UPLOAD_FILE_EXTENSION_BLACKLIST", "exe,bat"):
|
||||||
|
# Test with uppercase extension
|
||||||
|
filename = "malware.EXE"
|
||||||
|
content = b"test content"
|
||||||
|
mimetype = "application/x-msdownload"
|
||||||
|
|
||||||
|
with pytest.raises(BlockedFileExtensionError):
|
||||||
|
FileService(engine).upload_file(
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
mimetype=mimetype,
|
||||||
|
user=account,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_upload_file_not_in_blacklist(self, db_session_with_containers, engine, mock_external_service_dependencies):
|
||||||
|
"""
|
||||||
|
Test file upload with extension not in blacklist.
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
|
||||||
|
|
||||||
|
# Mock blacklist configuration by patching the inner field
|
||||||
|
with patch.object(dify_config, "inner_UPLOAD_FILE_EXTENSION_BLACKLIST", "exe,bat,sh"):
|
||||||
|
filename = "document.pdf"
|
||||||
|
content = b"test content"
|
||||||
|
mimetype = "application/pdf"
|
||||||
|
|
||||||
|
upload_file = FileService(engine).upload_file(
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
mimetype=mimetype,
|
||||||
|
user=account,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert upload_file is not None
|
||||||
|
assert upload_file.name == filename
|
||||||
|
assert upload_file.extension == "pdf"
|
||||||
|
|
||||||
|
def test_upload_file_empty_blacklist(self, db_session_with_containers, engine, mock_external_service_dependencies):
|
||||||
|
"""
|
||||||
|
Test file upload with empty blacklist (default behavior).
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
|
||||||
|
|
||||||
|
# Mock empty blacklist configuration by patching the inner field
|
||||||
|
with patch.object(dify_config, "inner_UPLOAD_FILE_EXTENSION_BLACKLIST", ""):
|
||||||
|
# Should allow all file types when blacklist is empty
|
||||||
|
filename = "script.sh"
|
||||||
|
content = b"#!/bin/bash\necho test"
|
||||||
|
mimetype = "application/x-sh"
|
||||||
|
|
||||||
|
upload_file = FileService(engine).upload_file(
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
mimetype=mimetype,
|
||||||
|
user=account,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert upload_file is not None
|
||||||
|
assert upload_file.extension == "sh"
|
||||||
|
|
||||||
|
def test_upload_file_multiple_blocked_extensions(
|
||||||
|
self, db_session_with_containers, engine, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test file upload with multiple blocked extensions.
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
|
||||||
|
|
||||||
|
# Mock blacklist with multiple extensions by patching the inner field
|
||||||
|
blacklist_str = "exe,bat,cmd,com,scr,vbs,ps1,msi,dll"
|
||||||
|
with patch.object(dify_config, "inner_UPLOAD_FILE_EXTENSION_BLACKLIST", blacklist_str):
|
||||||
|
for ext in blacklist_str.split(","):
|
||||||
|
filename = f"malware.{ext}"
|
||||||
|
content = b"test content"
|
||||||
|
mimetype = "application/octet-stream"
|
||||||
|
|
||||||
|
with pytest.raises(BlockedFileExtensionError):
|
||||||
|
FileService(engine).upload_file(
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
mimetype=mimetype,
|
||||||
|
user=account,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_upload_file_no_extension_with_blacklist(
|
||||||
|
self, db_session_with_containers, engine, mock_external_service_dependencies
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Test file upload with no extension when blacklist is configured.
|
||||||
|
"""
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, mock_external_service_dependencies)
|
||||||
|
|
||||||
|
# Mock blacklist configuration by patching the inner field
|
||||||
|
with patch.object(dify_config, "inner_UPLOAD_FILE_EXTENSION_BLACKLIST", "exe,bat"):
|
||||||
|
# Files with no extension should not be blocked
|
||||||
|
filename = "README"
|
||||||
|
content = b"test content"
|
||||||
|
mimetype = "text/plain"
|
||||||
|
|
||||||
|
upload_file = FileService(engine).upload_file(
|
||||||
|
filename=filename,
|
||||||
|
content=content,
|
||||||
|
mimetype=mimetype,
|
||||||
|
user=account,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert upload_file is not None
|
||||||
|
assert upload_file.extension == ""
|
||||||
|
|||||||
@@ -762,6 +762,12 @@ UPLOAD_FILE_SIZE_LIMIT=15
|
|||||||
# The maximum number of files that can be uploaded at a time, default 5.
|
# The maximum number of files that can be uploaded at a time, default 5.
|
||||||
UPLOAD_FILE_BATCH_LIMIT=5
|
UPLOAD_FILE_BATCH_LIMIT=5
|
||||||
|
|
||||||
|
# Comma-separated list of file extensions blocked from upload for security reasons.
|
||||||
|
# Extensions should be lowercase without dots (e.g., exe,bat,sh,dll).
|
||||||
|
# Empty by default to allow all file types.
|
||||||
|
# Recommended: exe,bat,cmd,com,scr,vbs,ps1,msi,dll
|
||||||
|
UPLOAD_FILE_EXTENSION_BLACKLIST=
|
||||||
|
|
||||||
# ETL type, support: `dify`, `Unstructured`
|
# ETL type, support: `dify`, `Unstructured`
|
||||||
# `dify` Dify's proprietary file extraction scheme
|
# `dify` Dify's proprietary file extraction scheme
|
||||||
# `Unstructured` Unstructured.io file extraction scheme
|
# `Unstructured` Unstructured.io file extraction scheme
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
CLICKZETTA_VECTOR_DISTANCE_FUNCTION: ${CLICKZETTA_VECTOR_DISTANCE_FUNCTION:-cosine_distance}
|
CLICKZETTA_VECTOR_DISTANCE_FUNCTION: ${CLICKZETTA_VECTOR_DISTANCE_FUNCTION:-cosine_distance}
|
||||||
UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15}
|
UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15}
|
||||||
UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5}
|
UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5}
|
||||||
|
UPLOAD_FILE_EXTENSION_BLACKLIST: ${UPLOAD_FILE_EXTENSION_BLACKLIST:-}
|
||||||
ETL_TYPE: ${ETL_TYPE:-dify}
|
ETL_TYPE: ${ETL_TYPE:-dify}
|
||||||
UNSTRUCTURED_API_URL: ${UNSTRUCTURED_API_URL:-}
|
UNSTRUCTURED_API_URL: ${UNSTRUCTURED_API_URL:-}
|
||||||
UNSTRUCTURED_API_KEY: ${UNSTRUCTURED_API_KEY:-}
|
UNSTRUCTURED_API_KEY: ${UNSTRUCTURED_API_KEY:-}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { FileEntity } from './types'
|
|||||||
import { useFileStore } from './store'
|
import { useFileStore } from './store'
|
||||||
import {
|
import {
|
||||||
fileUpload,
|
fileUpload,
|
||||||
|
getFileUploadErrorMessage,
|
||||||
getSupportFileType,
|
getSupportFileType,
|
||||||
isAllowedFileExtension,
|
isAllowedFileExtension,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
@@ -172,8 +173,9 @@ export const useFile = (fileConfig: FileUpload) => {
|
|||||||
onSuccessCallback: (res) => {
|
onSuccessCallback: (res) => {
|
||||||
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
|
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
|
||||||
},
|
},
|
||||||
onErrorCallback: () => {
|
onErrorCallback: (error?: any) => {
|
||||||
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
|
const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
handleUpdateFile({ ...uploadingFile, progress: -1 })
|
handleUpdateFile({ ...uploadingFile, progress: -1 })
|
||||||
},
|
},
|
||||||
}, !!params.token)
|
}, !!params.token)
|
||||||
@@ -279,8 +281,9 @@ export const useFile = (fileConfig: FileUpload) => {
|
|||||||
onSuccessCallback: (res) => {
|
onSuccessCallback: (res) => {
|
||||||
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
|
handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 })
|
||||||
},
|
},
|
||||||
onErrorCallback: () => {
|
onErrorCallback: (error?: any) => {
|
||||||
notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') })
|
const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
handleUpdateFile({ ...uploadingFile, progress: -1 })
|
handleUpdateFile({ ...uploadingFile, progress: -1 })
|
||||||
},
|
},
|
||||||
}, !!params.token)
|
}, !!params.token)
|
||||||
|
|||||||
@@ -7,11 +7,30 @@ import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
|||||||
import type { FileResponse } from '@/types/workflow'
|
import type { FileResponse } from '@/types/workflow'
|
||||||
import { TransferMethod } from '@/types/app'
|
import { TransferMethod } from '@/types/app'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get appropriate error message for file upload errors
|
||||||
|
* @param error - The error object from upload failure
|
||||||
|
* @param defaultMessage - Default error message to use if no specific error is matched
|
||||||
|
* @param t - Translation function
|
||||||
|
* @returns Localized error message
|
||||||
|
*/
|
||||||
|
export const getFileUploadErrorMessage = (error: any, defaultMessage: string, t: (key: string) => string): string => {
|
||||||
|
const errorCode = error?.response?.code
|
||||||
|
|
||||||
|
if (errorCode === 'forbidden')
|
||||||
|
return error?.response?.message
|
||||||
|
|
||||||
|
if (errorCode === 'file_extension_blocked')
|
||||||
|
return t('common.fileUploader.fileExtensionBlocked')
|
||||||
|
|
||||||
|
return defaultMessage
|
||||||
|
}
|
||||||
|
|
||||||
type FileUploadParams = {
|
type FileUploadParams = {
|
||||||
file: File
|
file: File
|
||||||
onProgressCallback: (progress: number) => void
|
onProgressCallback: (progress: number) => void
|
||||||
onSuccessCallback: (res: { id: string }) => void
|
onSuccessCallback: (res: { id: string }) => void
|
||||||
onErrorCallback: () => void
|
onErrorCallback: (error?: any) => void
|
||||||
}
|
}
|
||||||
type FileUpload = (v: FileUploadParams, isPublic?: boolean, url?: string) => void
|
type FileUpload = (v: FileUploadParams, isPublic?: boolean, url?: string) => void
|
||||||
export const fileUpload: FileUpload = ({
|
export const fileUpload: FileUpload = ({
|
||||||
@@ -37,8 +56,8 @@ export const fileUpload: FileUpload = ({
|
|||||||
.then((res: { id: string }) => {
|
.then((res: { id: string }) => {
|
||||||
onSuccessCallback(res)
|
onSuccessCallback(res)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
onErrorCallback()
|
onErrorCallback(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'
|
|||||||
import type { ClipboardEvent } from 'react'
|
import type { ClipboardEvent } from 'react'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { imageUpload } from './utils'
|
import { getImageUploadErrorMessage, imageUpload } from './utils'
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app'
|
import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app'
|
||||||
import type { ImageFile, VisionSettings } from '@/types/app'
|
import type { ImageFile, VisionSettings } from '@/types/app'
|
||||||
@@ -81,8 +81,9 @@ export const useImageFiles = () => {
|
|||||||
filesRef.current = newFiles
|
filesRef.current = newFiles
|
||||||
setFiles(newFiles)
|
setFiles(newFiles)
|
||||||
},
|
},
|
||||||
onErrorCallback: () => {
|
onErrorCallback: (error?: any) => {
|
||||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)]
|
const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)]
|
||||||
filesRef.current = newFiles
|
filesRef.current = newFiles
|
||||||
setFiles(newFiles)
|
setFiles(newFiles)
|
||||||
@@ -158,8 +159,9 @@ export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useL
|
|||||||
onSuccessCallback: (res) => {
|
onSuccessCallback: (res) => {
|
||||||
onUpload({ ...imageFile, fileId: res.id, progress: 100 })
|
onUpload({ ...imageFile, fileId: res.id, progress: 100 })
|
||||||
},
|
},
|
||||||
onErrorCallback: () => {
|
onErrorCallback: (error?: any) => {
|
||||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
onUpload({ ...imageFile, progress: -1 })
|
onUpload({ ...imageFile, progress: -1 })
|
||||||
},
|
},
|
||||||
}, !!params.token)
|
}, !!params.token)
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
import { upload } from '@/service/base'
|
import { upload } from '@/service/base'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get appropriate error message for image upload errors
|
||||||
|
* @param error - The error object from upload failure
|
||||||
|
* @param defaultMessage - Default error message to use if no specific error is matched
|
||||||
|
* @param t - Translation function
|
||||||
|
* @returns Localized error message
|
||||||
|
*/
|
||||||
|
export const getImageUploadErrorMessage = (error: any, defaultMessage: string, t: (key: string) => string): string => {
|
||||||
|
const errorCode = error?.response?.code
|
||||||
|
|
||||||
|
if (errorCode === 'forbidden')
|
||||||
|
return error?.response?.message
|
||||||
|
|
||||||
|
if (errorCode === 'file_extension_blocked')
|
||||||
|
return t('common.fileUploader.fileExtensionBlocked')
|
||||||
|
|
||||||
|
return defaultMessage
|
||||||
|
}
|
||||||
|
|
||||||
type ImageUploadParams = {
|
type ImageUploadParams = {
|
||||||
file: File
|
file: File
|
||||||
onProgressCallback: (progress: number) => void
|
onProgressCallback: (progress: number) => void
|
||||||
onSuccessCallback: (res: { id: string }) => void
|
onSuccessCallback: (res: { id: string }) => void
|
||||||
onErrorCallback: () => void
|
onErrorCallback: (error?: any) => void
|
||||||
}
|
}
|
||||||
type ImageUpload = (v: ImageUploadParams, isPublic?: boolean, url?: string) => void
|
type ImageUpload = (v: ImageUploadParams, isPublic?: boolean, url?: string) => void
|
||||||
export const imageUpload: ImageUpload = ({
|
export const imageUpload: ImageUpload = ({
|
||||||
@@ -30,7 +49,7 @@ export const imageUpload: ImageUpload = ({
|
|||||||
.then((res: { id: string }) => {
|
.then((res: { id: string }) => {
|
||||||
onSuccessCallback(res)
|
onSuccessCallback(res)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
onErrorCallback()
|
onErrorCallback(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import Button from '@/app/components/base/button'
|
|||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { imageUpload } from '@/app/components/base/image-uploader/utils'
|
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
|
||||||
import { useToastContext } from '@/app/components/base/toast'
|
import { useToastContext } from '@/app/components/base/toast'
|
||||||
import { BubbleTextMod } from '@/app/components/base/icons/src/vender/solid/communication'
|
import { BubbleTextMod } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||||
import {
|
import {
|
||||||
@@ -67,8 +67,9 @@ const CustomWebAppBrand = () => {
|
|||||||
setUploadProgress(100)
|
setUploadProgress(100)
|
||||||
setFileId(res.id)
|
setFileId(res.id)
|
||||||
},
|
},
|
||||||
onErrorCallback: () => {
|
onErrorCallback: (error?: any) => {
|
||||||
notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') })
|
const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
setUploadProgress(-1)
|
setUploadProgress(-1)
|
||||||
},
|
},
|
||||||
}, false, '/workspaces/custom-config/webapp-logo/upload')
|
}, false, '/workspaces/custom-config/webapp-logo/upload')
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { LanguagesSupported } from '@/i18n-config/language'
|
|||||||
import { IS_CE_EDITION } from '@/config'
|
import { IS_CE_EDITION } from '@/config'
|
||||||
import { Theme } from '@/types/app'
|
import { Theme } from '@/types/app'
|
||||||
import useTheme from '@/hooks/use-theme'
|
import useTheme from '@/hooks/use-theme'
|
||||||
|
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
||||||
|
|
||||||
type IFileUploaderProps = {
|
type IFileUploaderProps = {
|
||||||
fileList: FileItem[]
|
fileList: FileItem[]
|
||||||
@@ -134,7 +135,8 @@ const FileUploader = ({
|
|||||||
return Promise.resolve({ ...completeFile })
|
return Promise.resolve({ ...completeFile })
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
notify({ type: 'error', message: e?.response?.code === 'forbidden' ? e?.response?.message : t('datasetCreation.stepOne.uploader.failed') })
|
const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
onFileUpdate(fileItem, -2, fileListRef.current)
|
onFileUpdate(fileItem, -2, fileListRef.current)
|
||||||
return Promise.resolve({ ...fileItem })
|
return Promise.resolve({ ...fileItem })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import cn from '@/utils/classnames'
|
|||||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import { upload } from '@/service/base'
|
import { upload } from '@/service/base'
|
||||||
|
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
||||||
import I18n from '@/context/i18n'
|
import I18n from '@/context/i18n'
|
||||||
import { LanguagesSupported } from '@/i18n-config/language'
|
import { LanguagesSupported } from '@/i18n-config/language'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
import { IS_CE_EDITION } from '@/config'
|
||||||
@@ -154,7 +155,8 @@ const LocalFile = ({
|
|||||||
return Promise.resolve({ ...completeFile })
|
return Promise.resolve({ ...completeFile })
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
notify({ type: 'error', message: e?.response?.code === 'forbidden' ? e?.response?.message : t('datasetCreation.stepOne.uploader.failed') })
|
const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
updateFile(fileItem, -2, fileListRef.current)
|
updateFile(fileItem, -2, fileListRef.current)
|
||||||
return Promise.resolve({ ...fileItem })
|
return Promise.resolve({ ...fileItem })
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ToastContext } from '@/app/components/base/toast'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import type { FileItem } from '@/models/datasets'
|
import type { FileItem } from '@/models/datasets'
|
||||||
import { upload } from '@/service/base'
|
import { upload } from '@/service/base'
|
||||||
|
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { fetchFileUploadConfig } from '@/service/common'
|
import { fetchFileUploadConfig } from '@/service/common'
|
||||||
import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
||||||
@@ -74,7 +75,8 @@ const CSVUploader: FC<Props> = ({
|
|||||||
return Promise.resolve({ ...completeFile })
|
return Promise.resolve({ ...completeFile })
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
notify({ type: 'error', message: e?.response?.code === 'forbidden' ? e?.response?.message : t('datasetCreation.stepOne.uploader.failed') })
|
const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t)
|
||||||
|
notify({ type: 'error', message: errorMessage })
|
||||||
const errorFile = {
|
const errorFile = {
|
||||||
...fileItem,
|
...fileItem,
|
||||||
progress: -2,
|
progress: -2,
|
||||||
|
|||||||
@@ -734,6 +734,7 @@ const translation = {
|
|||||||
uploadFromComputerLimit: 'Upload {{type}} cannot exceed {{size}}',
|
uploadFromComputerLimit: 'Upload {{type}} cannot exceed {{size}}',
|
||||||
pasteFileLinkInvalid: 'Invalid file link',
|
pasteFileLinkInvalid: 'Invalid file link',
|
||||||
fileExtensionNotSupport: 'File extension not supported',
|
fileExtensionNotSupport: 'File extension not supported',
|
||||||
|
fileExtensionBlocked: 'This file type is blocked for security reasons',
|
||||||
},
|
},
|
||||||
tag: {
|
tag: {
|
||||||
placeholder: 'All Tags',
|
placeholder: 'All Tags',
|
||||||
|
|||||||
@@ -728,6 +728,7 @@ const translation = {
|
|||||||
uploadFromComputerLimit: '上传 {{type}} 不能超过 {{size}}',
|
uploadFromComputerLimit: '上传 {{type}} 不能超过 {{size}}',
|
||||||
pasteFileLinkInvalid: '文件链接无效',
|
pasteFileLinkInvalid: '文件链接无效',
|
||||||
fileExtensionNotSupport: '文件类型不支持',
|
fileExtensionNotSupport: '文件类型不支持',
|
||||||
|
fileExtensionBlocked: '出于安全考虑,该文件类型已被禁止上传',
|
||||||
},
|
},
|
||||||
tag: {
|
tag: {
|
||||||
placeholder: '全部标签',
|
placeholder: '全部标签',
|
||||||
|
|||||||
Reference in New Issue
Block a user