/* * Copyright 2025 coze-dev Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import mime from 'mime-types'; import { isNil } from 'lodash-es'; import { I18n } from '@coze-arch/i18n'; import { getFileExtension } from '../file-icon'; import { type FileItem } from './types'; import { MAX_FILE_SIZE } from './constants'; interface UploadValidateRule { maxSize?: number; imageSize?: ImageSizeRule; accept?: string; customValidate?: (file: FileItem) => Promise; } /** * 格式化文件大小 * @param bytes 文件大小 * @param decimals 小数位数, 默认 2 位 * @example * formatBytes(1024); // 1KB * formatBytes('1024'); // 1KB * formatBytes(1234); // 1.21KB * formatBytes(1234, 3); // 1.205KB */ export function formatBytes(bytes: number, decimals = 2) { if (bytes === 0) { return '0 Bytes'; } const k = 1024, dm = decimals, sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}`; } /** 文件大小校验 */ export const sizeValidate = ( size: number, maxSize: number = MAX_FILE_SIZE, ): string | undefined => { if (maxSize && size > maxSize) { return I18n.t('imageflow_upload_exceed', { size: formatBytes(maxSize), }); } }; export interface ImageSizeRule { maxWidth?: number; minWidth?: number; maxHeight?: number; minHeight?: number; aspectRatio?: number; } /** * 获取图片的宽高 */ export async function getImageSize( file: FileItem, ): Promise<{ width: number; height: number }> { const url = URL.createObjectURL(file); return new Promise((resolve, reject) => { const img = new window.Image(); img.onload = () => { resolve({ width: img.naturalWidth, height: img.naturalHeight, }); }; img.onerror = e => { reject(e); }; img.src = url; }); } /** 图像宽高校验 */ // eslint-disable-next-line complexity export const imageSizeValidate = async ( file: FileItem, rule?: ImageSizeRule, ): Promise => { const { maxWidth, minWidth, maxHeight, minHeight, aspectRatio } = rule || {}; // 未定义时不校验 if (isNil(maxWidth || minWidth || maxHeight || minHeight || aspectRatio)) { return; } const { width, height } = await getImageSize(file); if (maxWidth && width > maxWidth) { return I18n.t('imageflow_upload_error5', { value: `${maxWidth}px`, }); } if (minWidth && width < minWidth) { return I18n.t('imageflow_upload_error3', { value: `${minWidth}px`, }); } if (maxHeight && height > maxHeight) { return I18n.t('imageflow_upload_error4', { value: `${maxHeight}px`, }); } if (minHeight && height < minHeight) { return I18n.t('imageflow_upload_error2', { value: `${minHeight}px`, }); } if (aspectRatio && width / height > aspectRatio) { return I18n.t('imageflow_upload_error1'); } }; export const acceptValidate = (fileName: string, accept?: string) => { if (!accept) { return; } const acceptList = accept.split(','); const fileExtension = getFileExtension(fileName); const mimeType = mime.lookup(fileExtension); // image/* 匹配所有的图片类型 if (acceptList.includes('image/*') && mimeType?.startsWith?.('image/')) { return undefined; } if (!acceptList.includes(`.${fileExtension}`)) { return I18n.t('imageflow_upload_error_type', { type: `${acceptList.filter(Boolean).join('/')}`, }); } }; export const validate = async (file: FileItem, rules?: UploadValidateRule) => { const { size, name } = file; const { maxSize, imageSize, accept, customValidate } = rules || {}; const validators = [ async () => await customValidate?.(file), () => sizeValidate(size, maxSize), async () => await imageSizeValidate(file, imageSize), () => acceptValidate(name, accept), ]; for await (const validator of validators) { const errorMsg = await validator(); if (errorMsg) { return errorMsg; } } };