-
-
Notifications
You must be signed in to change notification settings - Fork 5k
/
items.ts
148 lines (127 loc) · 6.02 KB
/
items.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { Item, Uuid } from '../../services/database/types';
import { formParse } from '../../utils/requestUtils';
import { respondWithItemContent, SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import * as fs from 'fs-extra';
import { ErrorForbidden, ErrorMethodNotAllowed, ErrorNotFound, ErrorPayloadTooLarge, errorToPlainObject } from '../../utils/errors';
import ItemModel, { ItemSaveOption, SaveFromRawContentItem } from '../../models/ItemModel';
import { requestPagination } from '../../models/utils/pagination';
import { AclAction } from '../../models/BaseModel';
import { safeRemove } from '../../utils/fileUtils';
import { formatBytes, MB } from '../../utils/bytes';
import { requestDeltaPagination } from '../../models/ChangeModel';
const router = new Router(RouteType.Api);
const batchMaxSize = 1 * MB;
export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: boolean) {
if (!ctx.joplin.owner.can_upload) throw new ErrorForbidden('Uploading content is disabled');
const parsedBody = await formParse(ctx.req);
const bodyFields = parsedBody.fields;
const saveOptions: ItemSaveOption = {};
let items: SaveFromRawContentItem[] = [];
if (isBatch) {
let totalSize = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
items = bodyFields.items.map((item: any) => {
totalSize += item.name.length + (item.body ? item.body.length : 0);
return {
name: item.name,
body: item.body ? Buffer.from(item.body, 'utf8') : Buffer.alloc(0),
};
});
if (totalSize > batchMaxSize) throw new ErrorPayloadTooLarge(`Size of items (${formatBytes(totalSize)}) is over the limit (${formatBytes(batchMaxSize)})`);
} else {
const filePath = parsedBody?.files?.file ? parsedBody.files.file.filepath : null;
try {
const buffer = filePath ? await fs.readFile(filePath) : Buffer.alloc(0);
// This end point can optionally set the associated jop_share_id field. It
// is only useful when uploading resource blob (under .resource folder)
// since they can't have metadata. Note, Folder and Resource items all
// include the "share_id" field property so it doesn't need to be set via
// query parameter.
if (ctx.query['share_id']) {
saveOptions.shareId = ctx.query['share_id'] as string;
await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Create, { jop_share_id: saveOptions.shareId });
}
items = [
{
name: ctx.joplin.models.item().pathToName(path.id),
body: buffer,
},
];
} finally {
if (filePath) await safeRemove(filePath);
}
}
const output = await ctx.joplin.models.item().saveFromRawContent(ctx.joplin.owner, items, saveOptions);
for (const [name] of Object.entries(output)) {
if (output[name].item) output[name].item = ctx.joplin.models.item().toApiOutput(output[name].item) as Item;
if (output[name].error) output[name].error = errorToPlainObject(output[name].error);
}
return output;
}
// Note about access control:
//
// - All these calls are scoped to a user, which is derived from the session
// - All items are accessed by userId/itemName
// - In other words, it is not possible for a user to access another user's
// items, thus the lack of checkIfAllowed() calls as that would not be
// necessary, and would be slower.
// - For now, users who are shared a folder with have full access to all items
// within that folder. Except that they cannot delete the root folder if they
// are not the owner, so there's a check in this case.
async function itemFromPath(userId: Uuid, itemModel: ItemModel, path: SubPath, mustExists = true): Promise<Item> {
const name = itemModel.pathToName(path.id);
const item = await itemModel.loadByName(userId, name);
if (mustExists && !item) throw new ErrorNotFound(`Not found: ${path.id}`);
return item;
}
router.get('api/items/:id', async (path: SubPath, ctx: AppContext) => {
const itemModel = ctx.joplin.models.item();
const item = await itemFromPath(ctx.joplin.owner.id, itemModel, path);
return itemModel.toApiOutput(item);
});
router.del('api/items/:id', async (path: SubPath, ctx: AppContext) => {
try {
if (path.id === 'root' || path.id === 'root:/:') {
// We use this for testing only and for safety reasons it's probably
// best to disable it on production.
if (ctx.joplin.env !== 'dev') throw new ErrorMethodNotAllowed('Deleting the root is not allowed');
await ctx.joplin.models.item().deleteAll(ctx.joplin.owner.id);
} else {
const item = await itemFromPath(ctx.joplin.owner.id, ctx.joplin.models.item(), path);
await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, item);
await ctx.joplin.models.item().deleteForUser(ctx.joplin.owner.id, item);
}
} catch (error) {
if (error instanceof ErrorNotFound) {
// That's ok - a no-op
} else {
throw error;
}
}
});
router.get('api/items/:id/content', async (path: SubPath, ctx: AppContext) => {
const itemModel = ctx.joplin.models.item();
const item = await itemFromPath(ctx.joplin.owner.id, itemModel, path);
const serializedContent = await itemModel.serializedContent(item.id);
return respondWithItemContent(ctx.response, item, serializedContent);
});
router.put('api/items/:id/content', async (path: SubPath, ctx: AppContext) => {
const results = await putItemContents(path, ctx, false);
const result = results[Object.keys(results)[0]];
if (result.error) throw result.error;
return result.item;
});
router.get('api/items/:id/delta', async (_path: SubPath, ctx: AppContext) => {
const changeModel = ctx.joplin.models.change();
return changeModel.delta(ctx.joplin.owner.id, requestDeltaPagination(ctx.query));
});
router.get('api/items/:id/children', async (path: SubPath, ctx: AppContext) => {
const itemModel = ctx.joplin.models.item();
const parentName = itemModel.pathToName(path.id);
const result = await itemModel.children(ctx.joplin.owner.id, parentName, requestPagination(ctx.query));
return result;
});
export default router;