1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

1 Commits

Author SHA1 Message Date
Laurent Cozic
a1652d57d7 Server: Add support for user max item size 2021-05-17 17:54:13 +02:00
9 changed files with 96 additions and 10 deletions

Binary file not shown.

View File

@@ -275,6 +275,7 @@ export interface User extends WithDates, WithUuid {
password?: string;
full_name?: string;
is_admin?: number;
item_max_size?: number;
}
export interface Session extends WithDates, WithUuid {
@@ -377,6 +378,7 @@ export const databaseSchema: DatabaseTables = {
is_admin: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
item_max_size: { type: 'number' },
},
sessions: {
id: { type: 'string' },

View File

@@ -0,0 +1,12 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';
export async function up(db: DbConnection): Promise<any> {
await db.schema.alterTable('users', function(table: Knex.CreateTableBuilder) {
table.integer('item_max_size').defaultTo(0).notNullable();
});
}
export async function down(_db: DbConnection): Promise<any> {
}

View File

@@ -3,7 +3,7 @@ import { ItemType, databaseSchema, Uuid, Item, ShareType, Share, ChangeType, Use
import { defaultPagination, paginateDbQuery, PaginatedResults, Pagination } from './utils/pagination';
import { isJoplinItemName, isJoplinResourceBlobPath, linkedResourceIds, serializeJoplinItem, unserializeJoplinItem } from '../utils/joplinUtils';
import { ModelType } from '@joplin/lib/BaseModel';
import { ApiError, ErrorForbidden, ErrorNotFound, ErrorUnprocessableEntity } from '../utils/errors';
import { ApiError, ErrorForbidden, ErrorNotFound, ErrorPayloadTooLarge, ErrorUnprocessableEntity } from '../utils/errors';
import { Knex } from 'knex';
import { ChangePreviousItem } from './ChangeModel';
@@ -282,10 +282,10 @@ export default class ItemModel extends BaseModel<Item> {
return this.itemToJoplinItem(raw);
}
public async saveFromRawContent(userId: Uuid, name: string, buffer: Buffer, options: ItemSaveOption = null): Promise<Item> {
public async saveFromRawContent(user: User, name: string, buffer: Buffer, options: ItemSaveOption = null): Promise<Item> {
options = options || {};
const existingItem = await this.loadByName(userId, name);
const existingItem = await this.loadByName(user.id, name);
const isJoplinItem = isJoplinItemName(name);
let isNote = false;
@@ -322,8 +322,14 @@ export default class ItemModel extends BaseModel<Item> {
if (options.shareId) item.jop_share_id = options.shareId;
// If the item is encrypted, we apply a multipler because encrypted
// items can be much larger (seems to be up to twice the size but for
// safety let's go with 2.2).
const maxSize = user.item_max_size * (item.jop_encryption_applied ? 2.2 : 1);
if (maxSize && buffer.byteLength > maxSize) throw new ErrorPayloadTooLarge();
return this.withTransaction<Item>(async () => {
const savedItem = await this.saveForUser(userId, item);
const savedItem = await this.saveForUser(user.id, item);
if (isNote) {
await this.models().itemResource().deleteByItemId(savedItem.id);

View File

@@ -1,4 +1,4 @@
import { beforeAllDb, afterAllTests, beforeEachDb, createUserAndSession, models, createItem, makeTempFileWithContent, makeNoteSerializedBody, createItemTree, expectHttpError } from '../../utils/testing/testUtils';
import { beforeAllDb, afterAllTests, beforeEachDb, createUserAndSession, models, createItem, makeTempFileWithContent, makeNoteSerializedBody, createItemTree, expectHttpError, createNote, expectNoHttpError } from '../../utils/testing/testUtils';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { ModelType } from '@joplin/lib/BaseModel';
import { deleteApi, getApi, putApi } from '../../utils/testing/apiUtils';
@@ -6,7 +6,7 @@ import { Item } from '../../db';
import { PaginatedItems } from '../../models/ItemModel';
import { shareFolderWithUser } from '../../utils/testing/shareApiUtils';
import { resourceBlobPath } from '../../utils/joplinUtils';
import { ErrorForbidden } from '../../utils/errors';
import { ErrorForbidden, ErrorPayloadTooLarge } from '../../utils/errors';
describe('api_items', function() {
@@ -239,4 +239,41 @@ describe('api_items', function() {
);
});
test('should check permissions - uploaded item should be below the allowed limit', async function() {
const { user: user1, session: session1 } = await createUserAndSession(1);
{
await models().user().save({ id: user1.id, item_max_size: 4 });
await expectHttpError(
async () => createNote(session1.id, {
id: '00000000000000000000000000000001',
body: '12345',
}),
ErrorPayloadTooLarge.httpCode);
}
{
await models().user().save({ id: user1.id, item_max_size: 1000 });
await expectNoHttpError(
async () => createNote(session1.id, {
id: '00000000000000000000000000000002',
body: '12345',
})
);
}
{
await models().user().save({ id: user1.id, item_max_size: 0 });
await expectNoHttpError(
async () => createNote(session1.id, {
id: '00000000000000000000000000000003',
body: '12345',
})
);
}
});
});

View File

@@ -86,7 +86,7 @@ router.put('api/items/:id/content', async (path: SubPath, ctx: AppContext) => {
await itemModel.checkIfAllowed(ctx.owner, AclAction.Create, { jop_share_id: saveOptions.shareId });
}
const item = await itemModel.saveFromRawContent(ctx.owner.id, name, buffer, saveOptions);
const item = await itemModel.saveFromRawContent(ctx.owner, name, buffer, saveOptions);
outputItem = itemModel.toApiOutput(item) as Item;
} finally {
if (filePath) await safeRemove(filePath);

View File

@@ -77,3 +77,12 @@ export class ErrorResyncRequired extends ApiError {
Object.setPrototypeOf(this, ErrorResyncRequired.prototype);
}
}
export class ErrorPayloadTooLarge extends ApiError {
public static httpCode: number = 413;
public constructor(message: string = 'Payload Too Large') {
super(message, ErrorPayloadTooLarge.httpCode);
Object.setPrototypeOf(this, ErrorPayloadTooLarge.prototype);
}
}

View File

@@ -45,6 +45,8 @@ function convertTree(tree: any): any[] {
}
async function createItemTree3(sessionId: Uuid, userId: Uuid, parentFolderId: string, shareId: Uuid, tree: any[]): Promise<void> {
const user = await models().user().load(userId);
for (const jopItem of tree) {
const isFolder = !!jopItem.children;
const serializedBody = isFolder ?
@@ -58,7 +60,7 @@ async function createItemTree3(sessionId: Uuid, userId: Uuid, parentFolderId: st
}
}
const newItem = await models().item().saveFromRawContent(userId, `${jopItem.id}.md`, Buffer.from(serializedBody));
const newItem = await models().item().saveFromRawContent(user, `${jopItem.id}.md`, Buffer.from(serializedBody));
if (isFolder && jopItem.children.length) await createItemTree3(sessionId, userId, newItem.jop_id, shareId, jopItem.children);
}
}

View File

@@ -272,26 +272,28 @@ export async function createItemTree(userId: Uuid, parentFolderId: string, tree:
export async function createItemTree2(userId: Uuid, parentFolderId: string, tree: any[]): Promise<void> {
const itemModel = models().item();
const user = await models().user().load(userId);
for (const jopItem of tree) {
const isFolder = !!jopItem.children;
const serializedBody = isFolder ?
makeFolderSerializedBody({ ...jopItem, parent_id: parentFolderId }) :
makeNoteSerializedBody({ ...jopItem, parent_id: parentFolderId });
const newItem = await itemModel.saveFromRawContent(userId, `${jopItem.id}.md`, Buffer.from(serializedBody));
const newItem = await itemModel.saveFromRawContent(user, `${jopItem.id}.md`, Buffer.from(serializedBody));
if (isFolder && jopItem.children.length) await createItemTree2(userId, newItem.jop_id, jopItem.children);
}
}
export async function createItemTree3(userId: Uuid, parentFolderId: string, shareId: Uuid, tree: any[]): Promise<void> {
const itemModel = models().item();
const user = await models().user().load(userId);
for (const jopItem of tree) {
const isFolder = !!jopItem.children;
const serializedBody = isFolder ?
makeFolderSerializedBody({ ...jopItem, parent_id: parentFolderId, share_id: shareId }) :
makeNoteSerializedBody({ ...jopItem, parent_id: parentFolderId, share_id: shareId });
const newItem = await itemModel.saveFromRawContent(userId, `${jopItem.id}.md`, Buffer.from(serializedBody));
const newItem = await itemModel.saveFromRawContent(user, `${jopItem.id}.md`, Buffer.from(serializedBody));
if (isFolder && jopItem.children.length) await createItemTree3(userId, newItem.jop_id, shareId, jopItem.children);
}
}
@@ -408,6 +410,22 @@ export async function expectHttpError(asyncFn: Function, expectedHttpCode: numbe
}
}
export async function expectNoHttpError(asyncFn: Function): Promise<void> {
let thrownError = null;
try {
await asyncFn();
} catch (error) {
thrownError = error;
}
if (thrownError) {
expect('throw').toBe('not throw');
} else {
expect(true).toBe(true);
}
}
export function makeNoteSerializedBody(note: NoteEntity = {}): string {
return `${'title' in note ? note.title : 'Title'}