mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue