mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
📦 feat: Configure Skill Import Size Limit (#13073)
* fix: configure skill import size limit * fix: validate skill import size in ui * fix: align skill import size boundary * fix: show exact skill import limit
This commit is contained in:
parent
8735c1763c
commit
c385f2ba88
10 changed files with 329 additions and 10 deletions
|
|
@ -16,6 +16,7 @@ const {
|
|||
PermissionTypes,
|
||||
Permissions,
|
||||
FileContext,
|
||||
mergeFileConfig,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
createSkill,
|
||||
|
|
@ -52,6 +53,11 @@ const MAX_IMPORT_SIZE = 50 * 1024 * 1024; // 50 MB
|
|||
|
||||
const memoryStorage = multer.memoryStorage();
|
||||
|
||||
function getSkillImportSizeLimit(req) {
|
||||
const fileConfig = mergeFileConfig(req.config?.fileConfig);
|
||||
return fileConfig.skills?.fileSizeLimit ?? MAX_IMPORT_SIZE;
|
||||
}
|
||||
|
||||
const skillImportFilter = (_req, file, cb) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
if (ALLOWED_EXTENSIONS.has(ext)) {
|
||||
|
|
@ -62,11 +68,12 @@ const skillImportFilter = (_req, file, cb) => {
|
|||
}
|
||||
};
|
||||
|
||||
const skillUpload = multer({
|
||||
storage: memoryStorage,
|
||||
fileFilter: skillImportFilter,
|
||||
limits: { fileSize: MAX_IMPORT_SIZE },
|
||||
});
|
||||
const skillUpload = (req, res, next) =>
|
||||
multer({
|
||||
storage: memoryStorage,
|
||||
fileFilter: skillImportFilter,
|
||||
limits: { fileSize: getSkillImportSizeLimit(req) },
|
||||
}).single('file')(req, res, next);
|
||||
|
||||
// Per-file upload (for adding individual files to an existing skill)
|
||||
const MAX_SINGLE_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
|
|
@ -135,6 +142,9 @@ function resolveSkillStorage(req, { isImage = false } = {}) {
|
|||
// Import handler (zip/md/skill → create skill + files)
|
||||
// ---------------------------------------------------------------------------
|
||||
const importHandler = createImportHandler({
|
||||
limits: (req) => ({
|
||||
maxZipBytes: getSkillImportSizeLimit(req),
|
||||
}),
|
||||
createSkill,
|
||||
getSkillById,
|
||||
deleteSkill,
|
||||
|
|
@ -269,7 +279,7 @@ router.post(
|
|||
checkSkillCreate,
|
||||
fileUploadIpLimiter,
|
||||
fileUploadUserLimiter,
|
||||
skillUpload.single('file'),
|
||||
skillUpload,
|
||||
restoreTenantContextFromReq,
|
||||
importHandler,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,27 @@ const request = require('supertest');
|
|||
const JSZip = require('jszip');
|
||||
const mongoose = require('mongoose');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
|
||||
jest.mock('librechat-data-provider', () => {
|
||||
const actual = jest.requireActual('librechat-data-provider');
|
||||
return {
|
||||
...actual,
|
||||
mergeFileConfig: jest.fn((dynamic) => {
|
||||
const skillFileSizeLimit = dynamic?.skills?.fileSizeLimit;
|
||||
return {
|
||||
...actual.fileConfig,
|
||||
...dynamic,
|
||||
skills: {
|
||||
...(actual.fileConfig.skills ?? { fileSizeLimit: 50 * 1024 * 1024 }),
|
||||
...(skillFileSizeLimit !== undefined
|
||||
? { fileSizeLimit: skillFileSizeLimit * 1024 * 1024 }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const {
|
||||
SystemRoles,
|
||||
ResourceType,
|
||||
|
|
@ -11,6 +32,8 @@ const {
|
|||
PermissionBits,
|
||||
} = require('librechat-data-provider');
|
||||
|
||||
let mockFileConfig;
|
||||
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getCachedTools: jest.fn().mockResolvedValue({}),
|
||||
getAppConfig: jest.fn().mockResolvedValue({
|
||||
|
|
@ -23,6 +46,7 @@ jest.mock('~/server/middleware/config/app', () => (req, _res, next) => {
|
|||
req.config = {
|
||||
fileStrategy: 'local',
|
||||
paths: { uploads: '/tmp/uploads', images: '/tmp/images' },
|
||||
fileConfig: mockFileConfig,
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
|
@ -127,6 +151,7 @@ afterEach(async () => {
|
|||
await SkillFile.deleteMany({});
|
||||
await AclEntry.deleteMany({});
|
||||
currentTestUser = testUsers.owner;
|
||||
mockFileConfig = undefined;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
@ -300,6 +325,26 @@ describe('Skill routes', () => {
|
|||
});
|
||||
|
||||
describe('POST /api/skills/import', () => {
|
||||
it('enforces fileConfig.skills.fileSizeLimit before import handling', async () => {
|
||||
mockFileConfig = {
|
||||
skills: {
|
||||
fileSizeLimit: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const res = await request(app)
|
||||
.post('/api/skills/import')
|
||||
.attach('file', Buffer.alloc(2 * 1024 * 1024), {
|
||||
filename: 'too-large.skill',
|
||||
contentType: 'application/zip',
|
||||
});
|
||||
|
||||
const { mergeFileConfig } = require('librechat-data-provider');
|
||||
expect(mergeFileConfig).toHaveBeenCalledWith(mockFileConfig);
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toMatch(/file too large/i);
|
||||
});
|
||||
|
||||
it('persists storage metadata for imported skill files', async () => {
|
||||
const savedFilepath =
|
||||
'https://cdn.example.com/r/us-east-2/uploads/user123/imported-script.sh';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ import { useRef, useCallback, useState } from 'react';
|
|||
import { Upload } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { OGDialog, OGDialogContent, Spinner, useToastContext } from '@librechat/client';
|
||||
import { useImportSkillMutation } from '~/data-provider';
|
||||
import {
|
||||
megabyte,
|
||||
mergeFileConfig,
|
||||
fileConfig as defaultFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { useGetFileConfig, useImportSkillMutation } from '~/data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
|
@ -11,12 +16,30 @@ interface UploadSkillDialogProps {
|
|||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
function formatMegabytes(bytes: number): string {
|
||||
const value = bytes / megabyte;
|
||||
return Number.isInteger(value) ? `${value}` : value.toFixed(1);
|
||||
}
|
||||
|
||||
export default function UploadSkillDialog({ isOpen, setIsOpen }: UploadSkillDialogProps) {
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
const { showToast } = useToastContext();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const { data: skillFileConfig = { fileConfig: defaultFileConfig } } = useGetFileConfig({
|
||||
select: (data) => ({
|
||||
configuredSizeLimitMb: data?.skills?.fileSizeLimit,
|
||||
fileConfig: mergeFileConfig(data),
|
||||
}),
|
||||
});
|
||||
const { configuredSizeLimitMb, fileConfig } = skillFileConfig;
|
||||
const skillImportSizeLimit =
|
||||
fileConfig.skills?.fileSizeLimit ?? defaultFileConfig.skills?.fileSizeLimit ?? 0;
|
||||
const displayedSizeLimit =
|
||||
configuredSizeLimitMb !== undefined
|
||||
? `${configuredSizeLimitMb}`
|
||||
: formatMegabytes(skillImportSizeLimit);
|
||||
|
||||
const importMutation = useImportSkillMutation({
|
||||
onSuccess: (skill) => {
|
||||
|
|
@ -38,11 +61,18 @@ export default function UploadSkillDialog({ isOpen, setIsOpen }: UploadSkillDial
|
|||
if (importMutation.isLoading) {
|
||||
return;
|
||||
}
|
||||
if (file.size > skillImportSizeLimit) {
|
||||
showToast({
|
||||
status: 'error',
|
||||
message: localize('com_ui_skill_upload_size_error', { 0: displayedSizeLimit }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
importMutation.mutate(formData);
|
||||
},
|
||||
[importMutation],
|
||||
[displayedSizeLimit, importMutation, localize, showToast, skillImportSizeLimit],
|
||||
);
|
||||
|
||||
const handleFileInput = useCallback(
|
||||
|
|
@ -109,6 +139,7 @@ export default function UploadSkillDialog({ isOpen, setIsOpen }: UploadSkillDial
|
|||
<ul className="mt-1 list-inside list-disc">
|
||||
<li>{localize('com_ui_skill_upload_req_md')}</li>
|
||||
<li>{localize('com_ui_skill_upload_req_zip')}</li>
|
||||
<li>{localize('com_ui_skill_upload_req_size', { 0: displayedSizeLimit })}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { FileConfigInput } from 'librechat-data-provider';
|
||||
import UploadSkillDialog from '../UploadSkillDialog';
|
||||
|
||||
const mockMutate = jest.fn();
|
||||
const mockNavigate = jest.fn();
|
||||
const mockSetIsOpen = jest.fn();
|
||||
const mockShowToast = jest.fn();
|
||||
let mockFileConfigInput: FileConfigInput | undefined = {
|
||||
skills: {
|
||||
fileSizeLimit: 1,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@librechat/client',
|
||||
() => {
|
||||
const React = jest.requireActual<typeof import('react')>('react');
|
||||
return {
|
||||
OGDialog: ({ open, children }: { open: boolean; children: ReactNode }) =>
|
||||
open ? React.createElement('div', null, children) : null,
|
||||
OGDialogContent: ({ children }: { children: ReactNode }) =>
|
||||
React.createElement('div', null, children),
|
||||
Spinner: () => React.createElement('div', { 'data-testid': 'spinner' }),
|
||||
useToastContext: () => ({
|
||||
showToast: mockShowToast,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useGetFileConfig: ({ select }: { select?: (data: FileConfigInput | undefined) => unknown }) => ({
|
||||
data: select != null ? select(mockFileConfigInput) : mockFileConfigInput,
|
||||
}),
|
||||
useImportSkillMutation: () => ({
|
||||
mutate: mockMutate,
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useLocalize:
|
||||
() =>
|
||||
(key: string, params?: Record<string, string | number | undefined>): string => {
|
||||
const translations: Record<string, string> = {
|
||||
com_ui_skill_upload_title: 'Upload skill',
|
||||
com_ui_skill_upload_drag: 'Drag and drop or click to upload',
|
||||
com_ui_skill_upload_requirements: 'File requirements',
|
||||
com_ui_skill_upload_req_md:
|
||||
'.md file must contain skill name and description formatted in YAML',
|
||||
com_ui_skill_upload_req_zip: '.zip or .skill file must include a SKILL.md file',
|
||||
com_ui_skill_upload_req_size: `File size must not exceed ${params?.[0]} MB`,
|
||||
com_ui_skill_upload_size_error: `Skill import must not exceed ${params?.[0]} MB`,
|
||||
com_ui_skill_created: 'Skill created',
|
||||
com_ui_create_skill_upload_error: 'Failed to read the uploaded file',
|
||||
};
|
||||
return translations[key] ?? key;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('~/utils', () => ({
|
||||
cn: (...classes: Array<string | false | null | undefined>) => classes.filter(Boolean).join(' '),
|
||||
}));
|
||||
|
||||
function getFileInput(container: HTMLElement): HTMLInputElement {
|
||||
const input = container.querySelector('input[type="file"]');
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
throw new Error('Upload input was not rendered');
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
describe('UploadSkillDialog', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockFileConfigInput = {
|
||||
skills: {
|
||||
fileSizeLimit: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('renders the configured skill import size limit', () => {
|
||||
render(<UploadSkillDialog isOpen={true} setIsOpen={mockSetIsOpen} />);
|
||||
|
||||
expect(screen.getByText('File size must not exceed 1 MB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders fractional configured skill import size limits exactly', () => {
|
||||
mockFileConfigInput = {
|
||||
skills: {
|
||||
fileSizeLimit: 1.06,
|
||||
},
|
||||
};
|
||||
|
||||
render(<UploadSkillDialog isOpen={true} setIsOpen={mockSetIsOpen} />);
|
||||
|
||||
expect(screen.getByText('File size must not exceed 1.06 MB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('rejects files above the configured skill import limit before upload', () => {
|
||||
const { container } = render(<UploadSkillDialog isOpen={true} setIsOpen={mockSetIsOpen} />);
|
||||
const file = new File([new Uint8Array(1024 * 1024 + 1)], 'too-large.skill', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
|
||||
fireEvent.change(getFileInput(container), {
|
||||
target: {
|
||||
files: [file],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
status: 'error',
|
||||
message: 'Skill import must not exceed 1 MB',
|
||||
});
|
||||
});
|
||||
|
||||
it('uploads files exactly at the configured skill import limit', () => {
|
||||
const appendSpy = jest.spyOn(FormData.prototype, 'append');
|
||||
const { container } = render(<UploadSkillDialog isOpen={true} setIsOpen={mockSetIsOpen} />);
|
||||
const file = new File([new Uint8Array(1024 * 1024)], 'exact-limit.skill', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
|
||||
fireEvent.change(getFileInput(container), {
|
||||
target: {
|
||||
files: [file],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockShowToast).not.toHaveBeenCalled();
|
||||
expect(appendSpy).toHaveBeenCalledWith('file', file, file.name);
|
||||
expect(mockMutate).toHaveBeenCalledWith(expect.any(FormData));
|
||||
appendSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('uploads files under the configured skill import limit', () => {
|
||||
const appendSpy = jest.spyOn(FormData.prototype, 'append');
|
||||
const { container } = render(<UploadSkillDialog isOpen={true} setIsOpen={mockSetIsOpen} />);
|
||||
const file = new File([new Uint8Array(1024)], 'small.skill', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
|
||||
fireEvent.change(getFileInput(container), {
|
||||
target: {
|
||||
files: [file],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockShowToast).not.toHaveBeenCalled();
|
||||
expect(appendSpy).toHaveBeenCalledWith('file', file, file.name);
|
||||
expect(mockMutate).toHaveBeenCalledWith(expect.any(FormData));
|
||||
appendSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
@ -1524,8 +1524,10 @@
|
|||
"com_ui_skill_upload_drag": "Drag and drop or click to upload",
|
||||
"com_ui_skill_upload_file": "Upload File",
|
||||
"com_ui_skill_upload_req_md": ".md file must contain skill name and description formatted in YAML",
|
||||
"com_ui_skill_upload_req_size": "File size must not exceed {{0}} MB",
|
||||
"com_ui_skill_upload_req_zip": ".zip or .skill file must include a SKILL.md file",
|
||||
"com_ui_skill_upload_requirements": "File requirements",
|
||||
"com_ui_skill_upload_size_error": "Skill import must not exceed {{0}} MB",
|
||||
"com_ui_skill_upload_title": "Upload skill",
|
||||
"com_ui_skill_version": "v{{0}}",
|
||||
"com_ui_skill_view_rendered": "View rendered",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,24 @@ describe('parseFrontmatter', () => {
|
|||
});
|
||||
|
||||
describe('createImportHandler', () => {
|
||||
it('uses request-scoped import limits', async () => {
|
||||
const buffer = await zipWithAdditionalFiles(0, 0);
|
||||
const deps = mockImportDeps(() => ({
|
||||
maxZipBytes: buffer.length - 1,
|
||||
}));
|
||||
const handler = createImportHandler(deps);
|
||||
const res = mockResponse();
|
||||
|
||||
await handler(mockZipRequest(buffer), res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.body).toEqual(
|
||||
expect.objectContaining({
|
||||
error: expect.stringContaining('File too large'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('counts rejected oversized zip entries toward the cumulative decompressed limit', async () => {
|
||||
const kib = 1024;
|
||||
const deps = mockImportDeps({
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ function isDuplicateKeyError(error: unknown): boolean {
|
|||
}
|
||||
|
||||
export interface ImportSkillDeps {
|
||||
limits?: Partial<ImportLimits>;
|
||||
limits?: Partial<ImportLimits> | ((req: ServerRequest) => Partial<ImportLimits> | undefined);
|
||||
createSkill: (data: CreateSkillInput) => Promise<CreateSkillResult>;
|
||||
getSkillById: (id: string | Types.ObjectId) => Promise<(ISkill & { _id: Types.ObjectId }) | null>;
|
||||
deleteSkill: (id: string) => Promise<{ deleted: boolean }>;
|
||||
|
|
@ -257,6 +257,13 @@ function getImportLimits(limits?: Partial<ImportLimits>): ImportLimits {
|
|||
};
|
||||
}
|
||||
|
||||
function resolveImportLimits(
|
||||
limits: ImportSkillDeps['limits'],
|
||||
req: ServerRequest,
|
||||
): Partial<ImportLimits> | undefined {
|
||||
return typeof limits === 'function' ? limits(req) : limits;
|
||||
}
|
||||
|
||||
/** Resolve author metadata from the request user. */
|
||||
function getAuthorInfo(req: ServerRequest) {
|
||||
const user = req.user;
|
||||
|
|
@ -353,7 +360,7 @@ async function handleZip(
|
|||
file: Express.Multer.File,
|
||||
) {
|
||||
const userId = req.user.id;
|
||||
const limits = getImportLimits(deps.limits);
|
||||
const limits = getImportLimits(resolveImportLimits(deps.limits, req));
|
||||
|
||||
const zipBuffer = file.buffer;
|
||||
|
||||
|
|
|
|||
|
|
@ -836,6 +836,22 @@ describe('getEndpointFileConfig', () => {
|
|||
expect(result.fileSizeLimit).toBe(10 * 1024 * 1024);
|
||||
});
|
||||
|
||||
it('should convert skills fileSizeLimit from MB to bytes', () => {
|
||||
const merged = mergeFileConfig({
|
||||
skills: {
|
||||
fileSizeLimit: 15,
|
||||
},
|
||||
});
|
||||
|
||||
expect(merged.skills?.fileSizeLimit).toBe(15 * 1024 * 1024);
|
||||
});
|
||||
|
||||
it('should default skills fileSizeLimit to 50 MB', () => {
|
||||
const merged = mergeFileConfig(undefined);
|
||||
|
||||
expect(merged.skills?.fileSizeLimit).toBe(50 * 1024 * 1024);
|
||||
});
|
||||
|
||||
it('should preserve disabled: false in merged config', () => {
|
||||
const dynamicConfig = {
|
||||
endpoints: {
|
||||
|
|
|
|||
|
|
@ -397,6 +397,7 @@ export const megabyte = 1024 * 1024;
|
|||
export const mbToBytes = (mb: number): number => mb * megabyte;
|
||||
|
||||
const defaultSizeLimit = mbToBytes(512);
|
||||
const defaultSkillImportSizeLimit = mbToBytes(50);
|
||||
const defaultTokenLimit = 100000;
|
||||
const assistantsFileConfig = {
|
||||
fileLimit: 10,
|
||||
|
|
@ -426,6 +427,9 @@ export const fileConfig = {
|
|||
disabled: false,
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
fileSizeLimit: defaultSkillImportSizeLimit,
|
||||
},
|
||||
serverFileSizeLimit: defaultSizeLimit,
|
||||
avatarSizeLimit: mbToBytes(2),
|
||||
fileTokenLimit: defaultTokenLimit,
|
||||
|
|
@ -459,8 +463,13 @@ export const endpointFileConfigSchema = z.object({
|
|||
supportedMimeTypes: supportedMimeTypesSchema.optional(),
|
||||
});
|
||||
|
||||
const skillFileConfigSchema = z.object({
|
||||
fileSizeLimit: z.number().min(0).optional(),
|
||||
});
|
||||
|
||||
export const fileConfigSchema = z.object({
|
||||
endpoints: z.record(endpointFileConfigSchema).optional(),
|
||||
skills: skillFileConfigSchema.optional(),
|
||||
serverFileSizeLimit: z.number().min(0).optional(),
|
||||
avatarSizeLimit: z.number().min(0).optional(),
|
||||
fileTokenLimit: z.number().min(0).optional(),
|
||||
|
|
@ -652,6 +661,9 @@ export function mergeFileConfig(dynamic: z.infer<typeof fileConfigSchema> | unde
|
|||
endpoints: {
|
||||
...fileConfig.endpoints,
|
||||
},
|
||||
skills: {
|
||||
...fileConfig.skills,
|
||||
},
|
||||
ocr: {
|
||||
...fileConfig.ocr,
|
||||
supportedMimeTypes: fileConfig.ocr?.supportedMimeTypes || [],
|
||||
|
|
@ -681,6 +693,13 @@ export function mergeFileConfig(dynamic: z.infer<typeof fileConfigSchema> | unde
|
|||
mergedConfig.fileTokenLimit = dynamic.fileTokenLimit;
|
||||
}
|
||||
|
||||
if (dynamic.skills?.fileSizeLimit !== undefined) {
|
||||
mergedConfig.skills = {
|
||||
...mergedConfig.skills,
|
||||
fileSizeLimit: mbToBytes(dynamic.skills.fileSizeLimit),
|
||||
};
|
||||
}
|
||||
|
||||
// Merge clientImageResize configuration
|
||||
if (dynamic.clientImageResize !== undefined) {
|
||||
mergedConfig.clientImageResize = {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ export type FileConfig = {
|
|||
endpoints: {
|
||||
[key: string]: EndpointFileConfig;
|
||||
};
|
||||
skills?: {
|
||||
fileSizeLimit?: number;
|
||||
};
|
||||
fileTokenLimit?: number;
|
||||
serverFileSizeLimit?: number;
|
||||
avatarSizeLimit?: number;
|
||||
|
|
@ -76,6 +79,9 @@ export type FileConfigInput = {
|
|||
endpoints?: {
|
||||
[key: string]: EndpointFileConfig;
|
||||
};
|
||||
skills?: {
|
||||
fileSizeLimit?: number;
|
||||
};
|
||||
serverFileSizeLimit?: number;
|
||||
avatarSizeLimit?: number;
|
||||
clientImageResize?: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue