Web: Fixed the download and preview file not authorized. (#3652)

https://github.com/infiniflow/ragflow/issues/3651

### What problem does this PR solve?

_Briefly describe what this PR aims to solve. Include background context
that will help reviewers understand the purpose of the PR._

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
Fachuan Bai 2024-12-04 11:48:06 +08:00 committed by GitHub
parent efae7afd62
commit fc38afcec4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1124 additions and 1034 deletions

View File

@ -323,7 +323,7 @@ def rename():
@manager.route('/get/<file_id>', methods=['GET'])
# @login_required
@login_required
def get(file_id):
try:
e, file = FileService.get_by_id(file_id)

View File

@ -20,6 +20,7 @@
"@ant-design/icons": "^5.2.6",
"@ant-design/pro-components": "^2.6.46",
"@ant-design/pro-layout": "^7.17.16",
"@antv/g": "^6.1.11",
"@antv/g6": "^5.0.10",
"@hookform/resolvers": "^3.9.1",
"@js-preview/excel": "^1.7.8",
@ -72,6 +73,7 @@
"react-i18next": "^14.0.0",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1",
"react-pdf": "^9.1.1",
"react-pdf-highlighter": "^6.1.0",
"react-string-replace": "^1.1.1",
"react-syntax-highlighter": "^15.5.0",

View File

@ -123,6 +123,6 @@ export {
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
NavigationMenuViewport,
navigationMenuTriggerStyle,
NavigationMenuViewport,
};

View File

@ -2,6 +2,7 @@ import { ResponseType } from '@/interfaces/database/base';
import { IFolder } from '@/interfaces/database/file-manager';
import { IConnectRequestBody } from '@/interfaces/request/file-manager';
import fileManagerService from '@/services/file-manager-service';
import { downloadFileFromBlob } from '@/utils/file-util';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { PaginationProps, UploadFile, message } from 'antd';
import React, { useCallback } from 'react';
@ -114,6 +115,39 @@ export const useDeleteFile = () => {
return { data, loading, deleteFile: mutateAsync };
};
export const useDownloadFile = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['downloadFile'],
mutationFn: async (params: { id: string; filename?: string }) => {
const response = await fileManagerService.getFile({}, params.id);
const blob = new Blob([response.data], { type: response.data.type });
downloadFileFromBlob(blob, params.filename);
},
});
return { data, loading, downloadFile: mutateAsync };
};
export const useLoadFile = () => {
const {
data,
isPending: loading,
mutateAsync,
error,
} = useMutation({
mutationKey: ['downloadFile'],
mutationFn: async (params: { id: string }) => {
const response = await fileManagerService.getFile({}, params.id);
const blob = new Blob([response.data], { type: response.data.type });
return blob;
},
});
return { data, loading, loadFile: mutateAsync, error };
};
export const useRenameFile = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();

View File

@ -231,7 +231,8 @@ export default {
maxTokensMessage: 'El máximo de tokens es obligatorio',
maxTokensTip:
'Esto establece la longitud máxima de la salida del modelo, medida en el número de tokens (palabras o piezas de palabras).',
maxTokensInvalidMessage: 'Por favor, ingresa un número válido para Max Tokens.',
maxTokensInvalidMessage:
'Por favor, ingresa un número válido para Max Tokens.',
maxTokensMinMessage: 'Max Tokens no puede ser menor que 0.',
quote: 'Mostrar cita',
quoteTip: '¿Debe mostrarse la fuente del texto original?',
@ -284,7 +285,8 @@ export default {
maxTokensMessage: 'El máximo de tokens es obligatorio',
maxTokensTip:
'Esto establece la longitud máxima de la salida del modelo, medida en el número de tokens (palabras o piezas de palabras).',
maxTokensInvalidMessage: 'Por favor, ingresa un número válido para Max Tokens.',
maxTokensInvalidMessage:
'Por favor, ingresa un número válido para Max Tokens.',
maxTokensMinMessage: 'Max Tokens no puede ser menor que 0.',
password: 'Contraseña',
passwordDescription:

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,7 @@
import { useShowDeleteConfirm, useTranslate } from '@/hooks/common-hooks';
import { useRemoveNextDocument } from '@/hooks/document-hooks';
import { IDocumentInfo } from '@/interfaces/database/document';
import { api_host } from '@/utils/api';
import { downloadFile } from '@/utils/file-util';
import { downloadDocument } from '@/utils/file-util';
import {
DeleteOutlined,
DownloadOutlined,
@ -40,8 +39,8 @@ const ParsingActionCell = ({
};
const onDownloadDocument = () => {
downloadFile({
url: `${api_host}/document/get/${documentId}`,
downloadDocument({
id: documentId,
filename: record.name,
});
};

View File

@ -1,3 +1,5 @@
import { Authorization } from '@/constants/authorization';
import { getAuthorization } from '@/utils/authorization-util';
import jsPreviewExcel from '@js-preview/excel';
import axios from 'axios';
import mammoth from 'mammoth';
@ -23,7 +25,12 @@ export const useCatchError = (api: string) => {
export const useFetchDocument = () => {
const fetchDocument = useCallback(async (api: string) => {
const ret = await axios.get(api, { responseType: 'arraybuffer' });
const ret = await axios.get(api, {
headers: {
[Authorization]: getAuthorization(),
},
responseType: 'arraybuffer',
});
return ret;
}, []);
@ -64,30 +71,34 @@ export const useFetchExcel = (filePath: string) => {
export const useFetchDocx = (filePath: string) => {
const [succeed, setSucceed] = useState(true);
const [error, setError] = useState<string>();
const { fetchDocument } = useFetchDocument();
const containerRef = useRef<HTMLDivElement>(null);
const { error } = useCatchError(filePath);
const fetchDocumentAsync = useCallback(async () => {
const jsonFile = await fetchDocument(filePath);
mammoth
.convertToHtml(
{ arrayBuffer: jsonFile.data },
{ includeDefaultStyleMap: true },
)
.then((result) => {
setSucceed(true);
const docEl = document.createElement('div');
docEl.className = 'document-container';
docEl.innerHTML = result.value;
const container = containerRef.current;
if (container) {
container.innerHTML = docEl.outerHTML;
}
})
.catch(() => {
setSucceed(false);
});
try {
const jsonFile = await fetchDocument(filePath);
mammoth
.convertToHtml(
{ arrayBuffer: jsonFile.data },
{ includeDefaultStyleMap: true },
)
.then((result) => {
setSucceed(true);
const docEl = document.createElement('div');
docEl.className = 'document-container';
docEl.innerHTML = result.value;
const container = containerRef.current;
if (container) {
container.innerHTML = docEl.outerHTML;
}
})
.catch(() => {
setSucceed(false);
});
} catch (error: any) {
setError(error.toString());
}
}, [filePath, fetchDocument]);
useEffect(() => {

View File

@ -1,7 +1,14 @@
import { Authorization } from '@/constants/authorization';
import { getAuthorization } from '@/utils/authorization-util';
import { Skeleton } from 'antd';
import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter';
import FileError from '../file-error';
import { useCatchError } from '../hooks';
type PdfLoaderProps = React.ComponentProps<typeof PdfLoader> & {
httpHeaders?: Record<string, string>;
};
const Loader = PdfLoader as React.ComponentType<PdfLoaderProps>;
interface IProps {
url: string;
@ -10,11 +17,14 @@ interface IProps {
const PdfPreviewer = ({ url }: IProps) => {
const { error } = useCatchError(url);
const resetHash = () => {};
const httpHeaders = {
[Authorization]: getAuthorization(),
};
return (
<div style={{ width: '100%', height: '100%' }}>
<PdfLoader
<Loader
url={url}
httpHeaders={httpHeaders}
beforeLoad={<Skeleton active />}
workerSrc="/pdfjs-dist/pdf.worker.min.js"
errorMessage={<FileError>{error}</FileError>}
@ -37,7 +47,7 @@ const PdfPreviewer = ({ url }: IProps) => {
/>
);
}}
</PdfLoader>
</Loader>
</div>
);
};

View File

@ -1,13 +1,12 @@
import NewDocumentLink from '@/components/new-document-link';
import SvgIcon from '@/components/svg-icon';
import { useTranslate } from '@/hooks/common-hooks';
import { useDownloadFile } from '@/hooks/file-manager-hooks';
import { IFile } from '@/interfaces/database/file-manager';
import { api_host } from '@/utils/api';
import {
getExtension,
isSupportedPreviewDocumentType,
} from '@/utils/document-util';
import { downloadFile } from '@/utils/file-util';
import {
DeleteOutlined,
DownloadOutlined,
@ -42,12 +41,13 @@ const ActionCell = ({
[documentId],
setSelectedRowKeys,
);
const { downloadFile, loading } = useDownloadFile();
const extension = getExtension(record.name);
const isKnowledgeBase = record.source_type === 'knowledgebase';
const onDownloadDocument = () => {
downloadFile({
url: `${api_host}/file/get/${documentId}`,
id: documentId,
filename: record.name,
});
};
@ -106,7 +106,12 @@ const ActionCell = ({
)}
{record.type !== 'folder' && (
<Tooltip title={t('download', { keyPrefix: 'common' })}>
<Button type="text" disabled={beingUsed} onClick={onDownloadDocument}>
<Button
type="text"
disabled={beingUsed}
loading={loading}
onClick={onDownloadDocument}
>
<DownloadOutlined size={20} />
</Button>
</Tooltip>

View File

@ -40,7 +40,7 @@
word-break: break-all;
}
.description{
.description {
margin-top: 4px;
display: -webkit-box;
-webkit-line-clamp: 3;

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, Modal, Select, Space, InputNumber } from 'antd';
import { Flex, Form, Input, Modal, Select, Space } from 'antd';
import omit from 'lodash/omit';
type FieldType = IAddLlmRequestBody & {
@ -30,7 +30,7 @@ const TencentCloudModal = ({
...omit(values),
model_type: modelType,
llm_factory: llmFactory,
max_tokens:16000,
max_tokens: 16000,
};
console.info(data);

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, Modal, Select, Switch, InputNumber } from 'antd';
import { Form, Input, InputNumber, Modal, Select, Switch } from 'antd';
import omit from 'lodash/omit';
type FieldType = IAddLlmRequestBody & {
@ -33,7 +33,7 @@ const AzureOpenAIModal = ({
...omit(values, ['vision']),
model_type: modelType,
llm_factory: llmFactory,
max_tokens:values.max_tokens,
max_tokens: values.max_tokens,
};
console.info(data);

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, Modal, Select, Space, InputNumber } from 'antd';
import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd';
import { useMemo } from 'react';
import { BedrockRegionList } from '../constant';
@ -34,7 +34,7 @@ const BedrockModal = ({
const data = {
...values,
llm_factory: llmFactory,
max_tokens:values.max_tokens,
max_tokens: values.max_tokens,
};
onOk?.(data);
@ -136,7 +136,6 @@ const BedrockModal = ({
style={{ width: '100%' }}
/>
</Form.Item>
</Form>
</Modal>
);

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, Modal, Select, Space, InputNumber } from 'antd';
import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd';
import omit from 'lodash/omit';
type FieldType = IAddLlmRequestBody & {
@ -30,7 +30,7 @@ const FishAudioModal = ({
...omit(values),
model_type: modelType,
llm_factory: llmFactory,
max_tokens:values.max_tokens,
max_tokens: values.max_tokens,
};
console.info(data);
@ -118,7 +118,6 @@ const FishAudioModal = ({
style={{ width: '100%' }}
/>
</Form.Item>
</Form>
</Modal>
);

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, Modal, Select, InputNumber } from 'antd';
import { Form, Input, InputNumber, Modal, Select } from 'antd';
type FieldType = IAddLlmRequestBody & {
google_project_id: string;
@ -27,7 +27,7 @@ const GoogleModal = ({
const data = {
...values,
llm_factory: llmFactory,
max_tokens:values.max_tokens,
max_tokens: values.max_tokens,
};
onOk?.(data);
@ -112,7 +112,6 @@ const GoogleModal = ({
style={{ width: '100%' }}
/>
</Form.Item>
</Form>
</Modal>
);

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, Modal, Select} from 'antd';
import { Form, Input, Modal, Select } from 'antd';
import omit from 'lodash/omit';
type FieldType = IAddLlmRequestBody & {

View File

@ -1,7 +1,16 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, Modal, Select, Space, Switch, InputNumber } from 'antd';
import {
Flex,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Switch,
} from 'antd';
import omit from 'lodash/omit';
type FieldType = IAddLlmRequestBody & { vision: boolean };
@ -45,7 +54,7 @@ const OllamaModal = ({
...omit(values, ['vision']),
model_type: modelType,
llm_factory: llmFactory,
max_tokens:values.max_tokens,
max_tokens: values.max_tokens,
};
console.info(data);

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, Modal, Select, InputNumber } from 'antd';
import { Form, Input, InputNumber, Modal, Select } from 'antd';
import omit from 'lodash/omit';
type FieldType = IAddLlmRequestBody & {
@ -36,7 +36,7 @@ const SparkModal = ({
...omit(values, ['vision']),
model_type: modelType,
llm_factory: llmFactory,
max_tokens:values.max_tokens,
max_tokens: values.max_tokens,
};
console.info(data);
@ -80,52 +80,56 @@ const SparkModal = ({
<Form.Item noStyle dependencies={['model_type']}>
{({ getFieldValue }) =>
getFieldValue('model_type') === 'chat' && (
<Form.Item<FieldType>
label={t('addSparkAPIPassword')}
name="spark_api_password"
rules={[{ required: true, message: t('SparkAPIPasswordMessage') }]}
>
<Input placeholder={t('SparkAPIPasswordMessage')} />
</Form.Item>
<Form.Item<FieldType>
label={t('addSparkAPIPassword')}
name="spark_api_password"
rules={[
{ required: true, message: t('SparkAPIPasswordMessage') },
]}
>
<Input placeholder={t('SparkAPIPasswordMessage')} />
</Form.Item>
)
}
</Form.Item>
<Form.Item noStyle dependencies={['model_type']}>
{({ getFieldValue }) =>
getFieldValue('model_type') === 'tts' && (
<Form.Item<FieldType>
label={t('addSparkAPPID')}
name="spark_app_id"
rules={[{ required: true, message: t('SparkAPPIDMessage') }]}
>
<Input placeholder={t('SparkAPPIDMessage')} />
</Form.Item>
<Form.Item<FieldType>
label={t('addSparkAPPID')}
name="spark_app_id"
rules={[{ required: true, message: t('SparkAPPIDMessage') }]}
>
<Input placeholder={t('SparkAPPIDMessage')} />
</Form.Item>
)
}
</Form.Item>
<Form.Item noStyle dependencies={['model_type']}>
{({ getFieldValue }) =>
getFieldValue('model_type') === 'tts' && (
<Form.Item<FieldType>
label={t('addSparkAPISecret')}
name="spark_api_secret"
rules={[{ required: true, message: t('SparkAPISecretMessage') }]}
>
<Input placeholder={t('SparkAPISecretMessage')} />
</Form.Item>
<Form.Item<FieldType>
label={t('addSparkAPISecret')}
name="spark_api_secret"
rules={[
{ required: true, message: t('SparkAPISecretMessage') },
]}
>
<Input placeholder={t('SparkAPISecretMessage')} />
</Form.Item>
)
}
</Form.Item>
<Form.Item noStyle dependencies={['model_type']}>
{({ getFieldValue }) =>
getFieldValue('model_type') === 'tts' && (
<Form.Item<FieldType>
label={t('addSparkAPIKey')}
name="spark_api_key"
rules={[{ required: true, message: t('SparkAPIKeyMessage') }]}
>
<Input placeholder={t('SparkAPIKeyMessage')} />
</Form.Item>
<Form.Item<FieldType>
label={t('addSparkAPIKey')}
name="spark_api_key"
rules={[{ required: true, message: t('SparkAPIKeyMessage') }]}
>
<Input placeholder={t('SparkAPIKeyMessage')} />
</Form.Item>
)
}
</Form.Item>
@ -153,7 +157,6 @@ const SparkModal = ({
style={{ width: '100%' }}
/>
</Form.Item>
</Form>
</Modal>
);

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, Modal, Select, Space, Switch, InputNumber } from 'antd';
import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd';
import omit from 'lodash/omit';
type FieldType = IAddLlmRequestBody & {
@ -36,7 +36,7 @@ const VolcEngineModal = ({
...omit(values, ['vision']),
model_type: modelType,
llm_factory: llmFactory,
max_tokens:values.max_tokens,
max_tokens: values.max_tokens,
};
console.info(data);
@ -128,7 +128,6 @@ const VolcEngineModal = ({
style={{ width: '100%' }}
/>
</Form.Item>
</Form>
</Modal>
);

View File

@ -1,7 +1,7 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, Modal, Select, InputNumber } from 'antd';
import { Form, Input, InputNumber, Modal, Select } from 'antd';
import omit from 'lodash/omit';
type FieldType = IAddLlmRequestBody & {
@ -34,7 +34,7 @@ const YiyanModal = ({
...omit(values, ['vision']),
model_type: modelType,
llm_factory: llmFactory,
max_tokens:values.max_tokens,
max_tokens: values.max_tokens,
};
console.info(data);

View File

@ -45,11 +45,16 @@ const methods = {
url: connectFileToKnowledge,
method: 'post',
},
getDocumentFile: {
getFile: {
url: getFile,
method: 'get',
responseType: 'blob',
},
getDocumentFile: {
url: get_document_file,
method: 'get',
responseType: 'blob',
},
moveFile: {
url: moveFile,
method: 'post',

View File

@ -1,3 +1,4 @@
import fileManagerService from '@/services/file-manager-service';
import { UploadFile } from 'antd';
export const transformFile2Base64 = (val: any): Promise<any> => {
@ -100,29 +101,41 @@ export const getBase64FromUploadFileList = async (fileList?: UploadFile[]) => {
return '';
};
export const downloadFile = ({
url,
export const downloadFileFromBlob = (blob: Blob, name?: string) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
if (name) {
a.download = name;
}
a.click();
window.URL.revokeObjectURL(url);
};
export const downloadFile = async ({
id,
filename,
target,
}: {
url: string;
id: string;
filename?: string;
target?: string;
}) => {
console.log('downloadFile', url);
const downloadElement = document.createElement('a');
downloadElement.style.display = 'none';
downloadElement.href = url;
if (target) {
downloadElement.target = '_blank';
}
downloadElement.rel = 'noopener noreferrer';
if (filename) {
downloadElement.download = filename;
}
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
const response = await fileManagerService.getFile({}, id);
const blob = new Blob([response.data], { type: response.data.type });
downloadFileFromBlob(blob, filename);
};
export const downloadDocument = async ({
id,
filename,
}: {
id: string;
filename?: string;
}) => {
const response = await fileManagerService.getDocumentFile({}, id);
const blob = new Blob([response.data], { type: response.data.type });
downloadFileFromBlob(blob, filename);
};
const Units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];