mirror of
https://github.com/papra-hq/papra.git
synced 2025-12-31 00:50:17 -06:00
Compare commits
5 Commits
loading-st
...
changeset-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c60c965e | ||
|
|
b8c14d0f44 | ||
|
|
4878a3f8dd | ||
|
|
a213f0683b | ||
|
|
6a5bcef5ad |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@papra/docker": patch
|
||||
---
|
||||
|
||||
Organizations listing and details in the admin dashboard
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'Die Anfrage hat zu lange gedauert und ist abgelaufen. Bitte versuchen Sie es erneut.',
|
||||
'api-errors.document.already_exists': 'Das Dokument existiert bereits',
|
||||
'api-errors.document.size_too_large': 'Die Datei ist zu groß',
|
||||
'api-errors.intake-emails.already_exists': 'Eine Eingang-Email mit dieser Adresse existiert bereits.',
|
||||
|
||||
@@ -598,6 +598,7 @@ export const translations = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'The request took too long and timed out. Please try again.',
|
||||
'api-errors.document.already_exists': 'The document already exists',
|
||||
'api-errors.document.size_too_large': 'The file size is too large',
|
||||
'api-errors.intake-emails.already_exists': 'An intake email with this address already exists.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'La solicitud tardó demasiado y se agotó el tiempo. Por favor, inténtalo de nuevo.',
|
||||
'api-errors.document.already_exists': 'El documento ya existe',
|
||||
'api-errors.document.size_too_large': 'El archivo es demasiado grande',
|
||||
'api-errors.intake-emails.already_exists': 'Ya existe un correo de ingreso con esta dirección.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'La requête a pris trop de temps et a expiré. Veuillez réessayer.',
|
||||
'api-errors.document.already_exists': 'Le document existe déjà',
|
||||
'api-errors.document.size_too_large': 'Le fichier est trop volumineux',
|
||||
'api-errors.intake-emails.already_exists': 'Un email de réception avec cette adresse existe déjà.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'La richiesta ha impiegato troppo tempo ed è scaduta. Riprova.',
|
||||
'api-errors.document.already_exists': 'Il documento esiste già',
|
||||
'api-errors.document.size_too_large': 'Il file è troppo grande',
|
||||
'api-errors.intake-emails.already_exists': 'Un\'email di acquisizione con questo indirizzo esiste già.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'Het verzoek duurde te lang en is verlopen. Probeer het opnieuw.',
|
||||
'api-errors.document.already_exists': 'Het document bestaat al',
|
||||
'api-errors.document.size_too_large': 'Het bestand is te groot',
|
||||
'api-errors.intake-emails.already_exists': 'Er bestaat al een intake-e-mail met dit adres.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'Żądanie trwało zbyt długo i przekroczyło limit czasu. Spróbuj ponownie.',
|
||||
'api-errors.document.already_exists': 'Dokument już istnieje',
|
||||
'api-errors.document.size_too_large': 'Plik jest zbyt duży',
|
||||
'api-errors.intake-emails.already_exists': 'Adres e-mail do przyjęć z tym adresem już istnieje.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'A solicitação demorou muito e expirou. Por favor, tente novamente.',
|
||||
'api-errors.document.already_exists': 'O documento já existe',
|
||||
'api-errors.document.size_too_large': 'O arquivo é muito grande',
|
||||
'api-errors.intake-emails.already_exists': 'Um e-mail de entrada com este endereço já existe.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'O pedido demorou muito tempo e expirou. Por favor, tente novamente.',
|
||||
'api-errors.document.already_exists': 'O documento já existe',
|
||||
'api-errors.document.size_too_large': 'O arquivo é muito grande',
|
||||
'api-errors.intake-emails.already_exists': 'Um e-mail de entrada com este endereço já existe.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': 'Cererea a durat prea mult și a expirat. Vă rugăm să încercați din nou.',
|
||||
'api-errors.document.already_exists': 'Documentul există deja',
|
||||
'api-errors.document.size_too_large': 'Fișierul este prea mare',
|
||||
'api-errors.intake-emails.already_exists': 'Un email de primire cu această adresă există deja.',
|
||||
|
||||
@@ -600,6 +600,7 @@ export const translations: Partial<TranslationsDictionary> = {
|
||||
|
||||
// API errors
|
||||
|
||||
'api-errors.api.timeout': '请求耗时过长已超时。请重试。',
|
||||
'api-errors.document.already_exists': '文档已存在',
|
||||
'api-errors.document.size_too_large': '文件大小过大',
|
||||
'api-errors.intake-emails.already_exists': '具有此地址的接收邮箱已存在。',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createTimeoutMiddleware } from './timeout.middleware';
|
||||
describe('middlewares', () => {
|
||||
describe('timeoutMiddleware', () => {
|
||||
test('when a request last longer than the config timeout, a 504 error is raised', async () => {
|
||||
const config = overrideConfig({ server: { routeTimeoutMs: 50 } });
|
||||
const config = overrideConfig({ server: { defaultRouteTimeoutMs: 50 } });
|
||||
|
||||
const app = new Hono<ServerInstanceGenerics>();
|
||||
registerErrorMiddleware({ app });
|
||||
@@ -45,5 +45,107 @@ describe('middlewares', () => {
|
||||
expect(response2.status).to.eql(200);
|
||||
expect(await response2.json()).to.eql({ status: 'ok' });
|
||||
});
|
||||
|
||||
test('route-specific timeout overrides default timeout for matching routes', async () => {
|
||||
const config = overrideConfig({
|
||||
server: {
|
||||
defaultRouteTimeoutMs: 50,
|
||||
routeTimeouts: [
|
||||
{
|
||||
method: 'POST',
|
||||
route: '/api/upload/:id',
|
||||
timeoutMs: 200,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const app = new Hono<ServerInstanceGenerics>();
|
||||
registerErrorMiddleware({ app });
|
||||
|
||||
// POST to matching route with longer timeout - should not timeout
|
||||
app.post(
|
||||
'/api/upload/:id',
|
||||
createTimeoutMiddleware({ config }),
|
||||
async (context) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return context.json({ status: 'ok' });
|
||||
},
|
||||
);
|
||||
|
||||
// GET to same route - should timeout with default
|
||||
app.get(
|
||||
'/api/upload/:id',
|
||||
createTimeoutMiddleware({ config }),
|
||||
async (context) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return context.json({ status: 'ok' });
|
||||
},
|
||||
);
|
||||
|
||||
// Different route - should timeout with default
|
||||
app.post(
|
||||
'/api/other',
|
||||
createTimeoutMiddleware({ config }),
|
||||
async (context) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return context.json({ status: 'ok' });
|
||||
},
|
||||
);
|
||||
|
||||
// POST to matching pattern should succeed
|
||||
const response1 = await app.request('/api/upload/123', { method: 'POST' });
|
||||
expect(response1.status).to.eql(200);
|
||||
|
||||
// GET to same path should timeout (method mismatch)
|
||||
const response2 = await app.request('/api/upload/123', { method: 'GET' });
|
||||
expect(response2.status).to.eql(504);
|
||||
|
||||
// POST to different path should timeout (path mismatch)
|
||||
const response3 = await app.request('/api/other', { method: 'POST' });
|
||||
expect(response3.status).to.eql(504);
|
||||
});
|
||||
|
||||
test('when registered globally with .use(), route-specific timeouts should work', async () => {
|
||||
const config = overrideConfig({
|
||||
server: {
|
||||
defaultRouteTimeoutMs: 50,
|
||||
routeTimeouts: [
|
||||
{
|
||||
method: 'POST',
|
||||
route: '/api/organizations/:orgId/documents',
|
||||
timeoutMs: 200,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const app = new Hono<ServerInstanceGenerics>();
|
||||
registerErrorMiddleware({ app });
|
||||
|
||||
// Register middleware globally (like in server.ts)
|
||||
app.use(createTimeoutMiddleware({ config }));
|
||||
|
||||
// Route that should have extended timeout
|
||||
app.post('/api/organizations/:orgId/documents', async (context) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return context.json({ status: 'upload ok' });
|
||||
});
|
||||
|
||||
// Route that should use default timeout
|
||||
app.get('/api/other', async (context) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return context.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
// POST to upload route should succeed (extended timeout)
|
||||
const response1 = await app.request('/api/organizations/org-123/documents', { method: 'POST' });
|
||||
expect(response1.status).to.eql(200);
|
||||
expect(await response1.json()).to.eql({ status: 'upload ok' });
|
||||
|
||||
// GET to other route should timeout (default timeout)
|
||||
const response2 = await app.request('/api/other', { method: 'GET' });
|
||||
expect(response2.status).to.eql(504);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
import type { Config } from '../../config/config.types';
|
||||
import type { Context } from '../server.types';
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { routePath } from 'hono/route';
|
||||
import { createError } from '../../shared/errors/errors';
|
||||
|
||||
function getTimeoutForRoute({
|
||||
defaultRouteTimeoutMs,
|
||||
routeTimeouts,
|
||||
method,
|
||||
path,
|
||||
}: {
|
||||
defaultRouteTimeoutMs: number;
|
||||
routeTimeouts: { method: string; route: string; timeoutMs: number }[];
|
||||
method: string;
|
||||
path: string;
|
||||
}): number {
|
||||
const matchingRoute = routeTimeouts.find((routeConfig) => {
|
||||
if (routeConfig.method !== method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (routeConfig.route !== path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return matchingRoute?.timeoutMs ?? defaultRouteTimeoutMs;
|
||||
}
|
||||
|
||||
export function createTimeoutMiddleware({ config }: { config: Config }) {
|
||||
return createMiddleware(async (context: Context, next) => {
|
||||
const { server: { routeTimeoutMs } } = config;
|
||||
const method = context.req.method;
|
||||
const path = routePath(context, -1); // Get the last matched route path, without the -1 we get /* for all routes
|
||||
const { defaultRouteTimeoutMs, routeTimeouts } = config.server;
|
||||
|
||||
const timeoutMs = getTimeoutForRoute({ defaultRouteTimeoutMs, routeTimeouts, method, path });
|
||||
|
||||
let timerId: NodeJS.Timeout | undefined;
|
||||
|
||||
@@ -16,7 +47,7 @@ export function createTimeoutMiddleware({ config }: { config: Config }) {
|
||||
message: 'The request timed out',
|
||||
statusCode: 504,
|
||||
}),
|
||||
), routeTimeoutMs);
|
||||
), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { intakeEmailsConfig } from '../intake-emails/intake-emails.config';
|
||||
import { organizationsConfig } from '../organizations/organizations.config';
|
||||
import { organizationPlansConfig } from '../plans/plans.config';
|
||||
import { createLogger } from '../shared/logger/logger';
|
||||
import { IN_MS } from '../shared/units';
|
||||
import { isString } from '../shared/utils';
|
||||
import { subscriptionsConfig } from '../subscriptions/subscriptions.config';
|
||||
import { tasksConfig } from '../tasks/tasks.config';
|
||||
@@ -84,12 +85,29 @@ export const configDefinition = {
|
||||
default: '0.0.0.0',
|
||||
env: 'SERVER_HOSTNAME',
|
||||
},
|
||||
routeTimeoutMs: {
|
||||
defaultRouteTimeoutMs: {
|
||||
doc: 'The maximum time in milliseconds for a route to complete before timing out',
|
||||
schema: z.coerce.number().int().positive(),
|
||||
default: 20_000,
|
||||
default: 20 * IN_MS.SECOND,
|
||||
env: 'SERVER_API_ROUTES_TIMEOUT_MS',
|
||||
},
|
||||
routeTimeouts: {
|
||||
doc: 'Route-specific timeout overrides. Allows setting different timeouts for specific HTTP method and route paths.',
|
||||
schema: z.array(
|
||||
z.object({
|
||||
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']),
|
||||
route: z.string(),
|
||||
timeoutMs: z.number().int().positive(),
|
||||
}),
|
||||
),
|
||||
default: [
|
||||
{
|
||||
method: 'POST',
|
||||
route: '/api/organizations/:organizationId/documents',
|
||||
timeoutMs: 5 * IN_MS.MINUTE,
|
||||
},
|
||||
],
|
||||
},
|
||||
corsOrigins: {
|
||||
doc: 'The CORS origin for the api server',
|
||||
schema: z.union([
|
||||
|
||||
@@ -6,8 +6,11 @@ import { getUser } from '../app/auth/auth.models';
|
||||
import { organizationIdSchema } from '../organizations/organization.schemas';
|
||||
import { createOrganizationsRepository } from '../organizations/organizations.repository';
|
||||
import { ensureUserIsInOrganization } from '../organizations/organizations.usecases';
|
||||
import { createPlansRepository } from '../plans/plans.repository';
|
||||
import { getOrganizationPlan } from '../plans/plans.usecases';
|
||||
import { getFileStreamFromMultipartForm } from '../shared/streams/file-upload';
|
||||
import { validateJsonBody, validateParams, validateQuery } from '../shared/validation/validation';
|
||||
import { createSubscriptionsRepository } from '../subscriptions/subscriptions.repository';
|
||||
import { createDocumentIsNotDeletedError } from './documents.errors';
|
||||
import { formatDocumentForApi, formatDocumentsForApi, isDocumentSizeLimitEnabled } from './documents.models';
|
||||
import { createDocumentsRepository } from './documents.repository';
|
||||
@@ -45,12 +48,17 @@ function setupCreateDocumentRoute({ app, ...deps }: RouteDefinitionContext) {
|
||||
const organizationsRepository = createOrganizationsRepository({ db });
|
||||
await ensureUserIsInOrganization({ userId, organizationId, organizationsRepository });
|
||||
|
||||
const { maxUploadSize } = config.documentsStorage;
|
||||
// Get organization's plan-specific upload limit
|
||||
const plansRepository = createPlansRepository({ config });
|
||||
const subscriptionsRepository = createSubscriptionsRepository({ db });
|
||||
|
||||
const { organizationPlan } = await getOrganizationPlan({ organizationId, plansRepository, subscriptionsRepository });
|
||||
const { maxFileSize } = organizationPlan.limits;
|
||||
|
||||
const { fileStream, fileName, mimeType } = await getFileStreamFromMultipartForm({
|
||||
body: context.req.raw.body,
|
||||
headers: context.req.header(),
|
||||
maxFileSize: isDocumentSizeLimitEnabled({ maxUploadSize }) ? maxUploadSize : undefined,
|
||||
maxFileSize: isDocumentSizeLimitEnabled({ maxUploadSize: maxFileSize }) ? maxFileSize : undefined,
|
||||
});
|
||||
|
||||
const createDocument = createDocumentCreationUsecase({ ...deps });
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createServer } from '../../app/server';
|
||||
import { createTestServerDependencies } from '../../app/server.test-utils';
|
||||
import { overrideConfig } from '../../config/config.test-utils';
|
||||
import { ORGANIZATION_ROLES } from '../../organizations/organizations.constants';
|
||||
import { PLUS_PLAN_ID, PRO_PLAN_ID } from '../../plans/plans.constants';
|
||||
import { documentsTable } from '../documents.table';
|
||||
import { inMemoryStorageDriverFactory } from '../storage/drivers/memory/memory.storage-driver';
|
||||
|
||||
@@ -247,5 +248,123 @@ describe('documents e2e', () => {
|
||||
expect(retrievedDocument).to.eql({ ...document, tags: [] });
|
||||
}
|
||||
});
|
||||
|
||||
test('organizations on Plus plan should be able to upload files up to 100 MiB (not limited by global config)', async () => {
|
||||
const { db } = await createInMemoryDatabase({
|
||||
users: [{ id: 'usr_111111111111111111111111', email: 'user@example.com' }],
|
||||
organizations: [{ id: 'org_222222222222222222222222', name: 'Plus Org', customerId: 'cus_plus123' }],
|
||||
organizationMembers: [{ organizationId: 'org_222222222222222222222222', userId: 'usr_111111111111111111111111', role: ORGANIZATION_ROLES.OWNER }],
|
||||
organizationSubscriptions: [{
|
||||
id: 'sub_plus123',
|
||||
customerId: 'cus_plus123',
|
||||
organizationId: 'org_222222222222222222222222',
|
||||
planId: PLUS_PLAN_ID,
|
||||
status: 'active',
|
||||
seatsCount: 5,
|
||||
currentPeriodStart: new Date('2024-01-01'),
|
||||
currentPeriodEnd: new Date('2024-02-01'),
|
||||
cancelAtPeriodEnd: false,
|
||||
}],
|
||||
});
|
||||
|
||||
const { app } = createServer(createTestServerDependencies({
|
||||
db,
|
||||
config: overrideConfig({
|
||||
env: 'test',
|
||||
documentsStorage: {
|
||||
driver: 'in-memory',
|
||||
// Global config set to 10 MiB (simulating free tier limit)
|
||||
maxUploadSize: 1024 * 1024 * 10, // 10 MiB
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// File size: 50 MiB - exceeds global config (10 MiB) but within Plus plan limit (100 MiB)
|
||||
const fileSizeBytes = 1024 * 1024 * 50; // 50 MiB
|
||||
const formData = new FormData();
|
||||
formData.append('file', new File(['a'.repeat(fileSizeBytes)], 'large-document.txt', { type: 'text/plain' }));
|
||||
const body = new Response(formData);
|
||||
|
||||
const createDocumentResponse = await app.request(
|
||||
'/api/organizations/org_222222222222222222222222/documents',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...Object.fromEntries(body.headers.entries()),
|
||||
},
|
||||
body: await body.arrayBuffer(),
|
||||
},
|
||||
{ loggedInUserId: 'usr_111111111111111111111111' },
|
||||
);
|
||||
|
||||
// Should succeed because Plus plan allows 100 MiB
|
||||
expect(createDocumentResponse.status).to.eql(200);
|
||||
const { document } = (await createDocumentResponse.json()) as { document: Document };
|
||||
|
||||
expect(document).to.include({
|
||||
name: 'large-document.txt',
|
||||
mimeType: 'text/plain',
|
||||
originalSize: fileSizeBytes,
|
||||
});
|
||||
});
|
||||
|
||||
test('organizations on Pro plan should be able to upload files up to 500 MiB (not limited by global config)', async () => {
|
||||
const { db } = await createInMemoryDatabase({
|
||||
users: [{ id: 'usr_111111111111111111111111', email: 'user@example.com' }],
|
||||
organizations: [{ id: 'org_333333333333333333333333', name: 'Pro Org', customerId: 'cus_pro123' }],
|
||||
organizationMembers: [{ organizationId: 'org_333333333333333333333333', userId: 'usr_111111111111111111111111', role: ORGANIZATION_ROLES.OWNER }],
|
||||
organizationSubscriptions: [{
|
||||
id: 'sub_pro123',
|
||||
customerId: 'cus_pro123',
|
||||
organizationId: 'org_333333333333333333333333',
|
||||
planId: PRO_PLAN_ID,
|
||||
status: 'active',
|
||||
seatsCount: 20,
|
||||
currentPeriodStart: new Date('2024-01-01'),
|
||||
currentPeriodEnd: new Date('2024-02-01'),
|
||||
cancelAtPeriodEnd: false,
|
||||
}],
|
||||
});
|
||||
|
||||
const { app } = createServer(createTestServerDependencies({
|
||||
db,
|
||||
config: overrideConfig({
|
||||
env: 'test',
|
||||
documentsStorage: {
|
||||
driver: 'in-memory',
|
||||
// Global config set to 10 MiB (simulating free tier limit)
|
||||
maxUploadSize: 1024 * 1024 * 10, // 10 MiB
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// File size: 200 MiB - exceeds global config (10 MiB) but within Pro plan limit (500 MiB)
|
||||
const fileSizeBytes = 1024 * 1024 * 200; // 200 MiB
|
||||
const formData = new FormData();
|
||||
formData.append('file', new File(['a'.repeat(fileSizeBytes)], 'very-large-document.txt', { type: 'text/plain' }));
|
||||
const body = new Response(formData);
|
||||
|
||||
const createDocumentResponse = await app.request(
|
||||
'/api/organizations/org_333333333333333333333333/documents',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...Object.fromEntries(body.headers.entries()),
|
||||
},
|
||||
body: await body.arrayBuffer(),
|
||||
},
|
||||
{ loggedInUserId: 'usr_111111111111111111111111' },
|
||||
);
|
||||
|
||||
// Should succeed because Pro plan allows 500 MiB
|
||||
expect(createDocumentResponse.status).to.eql(200);
|
||||
const { document } = (await createDocumentResponse.json()) as { document: Document };
|
||||
|
||||
expect(document).to.include({
|
||||
name: 'very-large-document.txt',
|
||||
mimeType: 'text/plain',
|
||||
originalSize: fileSizeBytes,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ConfigDefinition } from 'figue';
|
||||
import { z } from 'zod';
|
||||
import { booleanishSchema } from '../config/config.schemas';
|
||||
import { IN_MS } from '../shared/units';
|
||||
import { isString } from '../shared/utils';
|
||||
import { defaultIgnoredPatterns } from './ingestion-folders.constants';
|
||||
|
||||
@@ -27,7 +28,7 @@ export const ingestionFolderConfig = {
|
||||
pollingInterval: {
|
||||
doc: 'When polling is used, this is the interval at which the watcher checks for changes in the ingestion folder (in milliseconds)',
|
||||
schema: z.coerce.number().int().positive(),
|
||||
default: 2_000,
|
||||
default: 2 * IN_MS.SECOND,
|
||||
env: 'INGESTION_FOLDER_WATCHER_POLLING_INTERVAL_MS',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Config } from '../config/config.types';
|
||||
import type { OrganizationPlanRecord } from './plans.types';
|
||||
import { injectArguments } from '@corentinth/chisels';
|
||||
import { isDocumentSizeLimitEnabled } from '../documents/documents.models';
|
||||
import { IN_BYTES } from '../shared/units';
|
||||
import { FREE_PLAN_ID, PLUS_PLAN_ID, PRO_PLAN_ID } from './plans.constants';
|
||||
import { createPlanNotFoundError } from './plans.errors';
|
||||
|
||||
@@ -30,7 +31,7 @@ export function getOrganizationPlansRecords({ config }: { config: Config }) {
|
||||
id: FREE_PLAN_ID,
|
||||
name: 'Free',
|
||||
limits: {
|
||||
maxDocumentStorageBytes: isFreePlanUnlimited ? Number.POSITIVE_INFINITY : 1024 * 1024 * 500, // 500 MiB
|
||||
maxDocumentStorageBytes: isFreePlanUnlimited ? Number.POSITIVE_INFINITY : 500 * IN_BYTES.MEGABYTE,
|
||||
maxIntakeEmailsCount: isFreePlanUnlimited ? Number.POSITIVE_INFINITY : 1,
|
||||
maxOrganizationsMembersCount: isFreePlanUnlimited ? Number.POSITIVE_INFINITY : 3,
|
||||
maxFileSize: isDocumentSizeLimitEnabled({ maxUploadSize }) ? maxUploadSize : Number.POSITIVE_INFINITY,
|
||||
@@ -42,10 +43,10 @@ export function getOrganizationPlansRecords({ config }: { config: Config }) {
|
||||
monthlyPriceId: config.organizationPlans.plusPlanMonthlyPriceId,
|
||||
annualPriceId: config.organizationPlans.plusPlanAnnualPriceId,
|
||||
limits: {
|
||||
maxDocumentStorageBytes: 1024 * 1024 * 1024 * 5, // 5 GiB
|
||||
maxDocumentStorageBytes: 5 * IN_BYTES.GIGABYTE, // 5 GiB
|
||||
maxIntakeEmailsCount: 10,
|
||||
maxOrganizationsMembersCount: 10,
|
||||
maxFileSize: 1024 * 1024 * 100, // 100 MiB
|
||||
maxFileSize: 100 * IN_BYTES.MEGABYTE, // 100 MiB
|
||||
},
|
||||
},
|
||||
[PRO_PLAN_ID]: {
|
||||
@@ -54,10 +55,10 @@ export function getOrganizationPlansRecords({ config }: { config: Config }) {
|
||||
monthlyPriceId: config.organizationPlans.proPlanMonthlyPriceId,
|
||||
annualPriceId: config.organizationPlans.proPlanAnnualPriceId,
|
||||
limits: {
|
||||
maxDocumentStorageBytes: 1024 * 1024 * 1024 * 50, // 50 GiB
|
||||
maxDocumentStorageBytes: 50 * IN_BYTES.GIGABYTE, // 50 GiB
|
||||
maxIntakeEmailsCount: 100,
|
||||
maxOrganizationsMembersCount: 50,
|
||||
maxFileSize: 1024 * 1024 * 500, // 500 MiB
|
||||
maxFileSize: 500 * IN_BYTES.MEGABYTE, // 500 MiB
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
16
apps/papra-server/src/modules/shared/units.ts
Normal file
16
apps/papra-server/src/modules/shared/units.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const IN_MS = {
|
||||
SECOND: 1_000,
|
||||
MINUTE: 60_000, // 60 * 1_000
|
||||
HOUR: 3_600_000, // 60 * 60 * 1_000
|
||||
DAY: 86_400_000, // 24 * 60 * 60 * 1_000
|
||||
WEEK: 604_800_000, // 7 * 24 * 60 * 60 * 1_000
|
||||
MONTH: 2_630_016_000, // 30.44 * 24 * 60 * 60 * 1_000 -- approximation using average month length
|
||||
YEAR: 31_556_736_000, // 365.24 * 24 * 60 * 60 * 1_000 -- approximation using average year length
|
||||
};
|
||||
|
||||
export const IN_BYTES = {
|
||||
KILOBYTE: 1_024,
|
||||
MEGABYTE: 1_048_576, // 1_024 * 1_024
|
||||
GIGABYTE: 1_073_741_824, // 1_024 * 1_024 * 1_024
|
||||
TERABYTE: 1_099_511_627_776, // 1_024 * 1_024 * 1_024 * 1_024
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import type { ConfigDefinition } from 'figue';
|
||||
import type { TasksDriverName } from './drivers/tasks-driver.constants';
|
||||
import { z } from 'zod';
|
||||
import { booleanishSchema } from '../config/config.schemas';
|
||||
import { IN_MS } from '../shared/units';
|
||||
import { tasksDriverNames } from './drivers/tasks-driver.constants';
|
||||
|
||||
export const tasksConfig = {
|
||||
@@ -35,7 +36,7 @@ export const tasksConfig = {
|
||||
pollIntervalMs: {
|
||||
doc: 'The interval at which the task persistence driver polls for new tasks',
|
||||
schema: z.coerce.number().int().positive(),
|
||||
default: 1_000,
|
||||
default: 1 * IN_MS.SECOND,
|
||||
env: 'TASKS_PERSISTENCE_DRIVERS_LIBSQL_POLL_INTERVAL_MS',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @papra/docker
|
||||
|
||||
## 25.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#707](https://github.com/papra-hq/papra/pull/707) [`a213f06`](https://github.com/papra-hq/papra/commit/a213f0683baebd6546bf38ba9e719c31b60064ed) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added a dedicated increased timeout for the document upload route
|
||||
|
||||
- [#712](https://github.com/papra-hq/papra/pull/712) [`b8c14d0`](https://github.com/papra-hq/papra/commit/b8c14d0f44628843c7a682f84f7215fecc50f426) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Added a feedback message upon request timeout
|
||||
|
||||
- [#702](https://github.com/papra-hq/papra/pull/702) [`ec34cf1`](https://github.com/papra-hq/papra/commit/ec34cf17880682369d1ecf2957c2d7e0eed9f499) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Organizations listing and details in the admin dashboard
|
||||
|
||||
- [#707](https://github.com/papra-hq/papra/pull/707) [`a213f06`](https://github.com/papra-hq/papra/commit/a213f0683baebd6546bf38ba9e719c31b60064ed) Thanks [@CorentinTh](https://github.com/CorentinTh)! - Changed config key `config.server.routeTimeoutMs` to `config.server.defaultRouteTimeoutMs` (env variable remains the same)
|
||||
|
||||
## 25.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@papra/docker",
|
||||
"version": "25.12.0",
|
||||
"version": "25.12.1",
|
||||
"private": true,
|
||||
"description": "Docker image version tracker for Papra, calver-ish versioned.",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user