wildduck/lib/api/storage.js
NickOvt 8e9af88a62
fix(api-storage): Added all storage endpoints to API docs generation ZMS-149 (#675)
* Added upload file api endpoint to api docs generaiton

* added list stored files api endpoint to api docs generation

* Added delete a File api endpoint to api docs generation

* added Download file api endpoint to api docs generation
2024-05-02 09:55:36 +03:00

474 lines
16 KiB
JavaScript

'use strict';
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectId = require('mongodb').ObjectId;
const tools = require('../tools');
const roles = require('../roles');
const consts = require('../consts');
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
const { userId } = require('../schemas/request/general-schemas');
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
module.exports = (db, server, storageHandler) => {
server.post(
{
path: '/users/:user/storage',
tags: ['Storage'],
summary: 'Upload file',
description: 'This method allows to upload an attachment to be linked from a draft',
validationObjs: {
requestBody: {
filename: Joi.string().empty('').max(255).description('Name of the file'),
contentType: Joi.string().empty('').max(255).description('MIME type of the file. Is detected from the file name by default'),
encoding: Joi.string()
.empty('')
.valid('base64')
.description(
'Encoding of the file content. Useful if you want to upload the file in base64 encoded format. Valid options "base64", "hex", "utf8"'
),
content: Joi.binary().max(consts.MAX_ALLOWED_MESSAGE_SIZE).empty('').required().description('File content in binary'),
cid: Joi.string().empty('').max(255).description('content ID'),
sess: sessSchema,
ip: sessIPSchema
},
queryParams: {},
pathParams: {
user: userId
},
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes,
id: Joi.string().required().description('File ID')
})
}
}
}
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const { requestBody, queryParams, pathParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...requestBody,
...queryParams,
...pathParams
});
if (!req.params.content && req.body && (Buffer.isBuffer(req.body) || typeof req.body === 'string')) {
req.params.content = req.body;
}
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).createOwn('storage'));
} else {
req.validate(roles.can(req.role).createAny('storage'));
}
let user = new ObjectId(result.value.user);
let userData;
try {
userData = await db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
address: true
}
}
);
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!userData) {
res.status(404);
return res.json({
error: 'This user does not exist',
code: 'UserNotFound'
});
}
let id = await storageHandler.add(user, result.value);
return res.json({
success: !!id,
id
});
})
);
server.get(
{
path: '/users/:user/storage',
tags: ['Storage'],
summary: 'List stored files',
validationObjs: {
requestBody: {},
queryParams: {
query: Joi.string().trim().empty('').max(255).description('partial match of a filename'),
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
page: pageNrSchema,
sess: sessSchema,
ip: sessIPSchema
},
pathParams: {
user: userId
},
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes,
total: totalRes,
page: pageRes,
previousCursor: previousCursorRes,
nextCursor: nextCursorRes,
results: Joi.array()
.items(
Joi.object({
id: Joi.string().required().description('File ID'),
filename: Joi.alternatives()
.try(Joi.string().required(), booleanSchema.required())
.required()
.description('Filename. False if none'),
contentType: Joi.alternatives()
.try(Joi.string().required(), booleanSchema.required())
.required()
.description('Content-Type of the file. False if none'),
cid: Joi.string().description('Content ID'),
size: Joi.number().required().description('File size'),
created: Joi.date().required().description('Created datestring'),
md5: Joi.string().description('md5 hash').required()
})
.required()
.$_setFlag('objectName', 'GetFilesResult')
)
.required()
.description('File listing')
})
}
}
}
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const { requestBody, queryParams, pathParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...requestBody,
...queryParams,
...pathParams
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true,
allowUnknown: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('storage'));
} else {
req.validate(roles.can(req.role).readAny('storage'));
}
let user = new ObjectId(result.value.user);
let userData;
try {
userData = await db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
address: true
}
}
);
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!userData) {
res.status(404);
return res.json({
error: 'This user does not exist',
code: 'UserNotFound'
});
}
let query = result.value.query;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
let filter = (query && {
'metadata.user': user,
filename: {
$regex: tools.escapeRegexStr(query),
$options: ''
}
}) || {
'metadata.user': user
};
let total = await db.gridfs.collection('storage.files').countDocuments(filter);
let opts = {
limit,
query: filter,
paginatedField: 'filename',
sortAscending: true
};
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
opts.previous = pagePrevious;
}
let listing;
try {
listing = await MongoPaging.find(db.gridfs.collection('storage.files'), opts);
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!listing.hasPrevious) {
page = 1;
}
let response = {
success: true,
query,
total,
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(fileData => ({
id: fileData._id.toString(),
filename: fileData.filename || false,
contentType: fileData.contentType || false,
cid: fileData.metadata?.cid,
size: fileData.length,
created: fileData.uploadDate.toISOString(),
md5: fileData.md5
}))
};
return res.json(response);
})
);
server.del(
{
path: '/users/:user/storage/:file',
tags: ['Storage'],
summary: 'Delete a File',
validationObjs: {
requestBody: {},
queryParams: {
sess: sessSchema,
ip: sessIPSchema
},
pathParams: {
user: userId,
file: Joi.string().hex().lowercase().length(24).required().description('ID of the File')
},
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes
})
}
}
}
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const { requestBody, queryParams, pathParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...requestBody,
...queryParams,
...pathParams
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
let user = new ObjectId(result.value.user);
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).deleteOwn('storage'));
} else {
req.validate(roles.can(req.role).deleteAny('storage'));
}
let file = new ObjectId(result.value.file);
await storageHandler.delete(user, file);
return res.json({
success: true
});
})
);
server.get(
{
name: 'storagefile',
path: '/users/:user/storage/:file',
tags: ['Storage'],
summary: 'Download File',
description: 'This method returns stored file contents in binary form',
responseType: 'application/octet-stream',
validationObjs: {
requestBody: {},
queryParams: {},
pathParams: {
user: userId,
file: Joi.string().hex().lowercase().length(24).required().description('ID of the File')
},
response: {
200: {
description: 'Success',
model: Joi.binary()
}
}
}
},
tools.responseWrapper(async (req, res) => {
const { requestBody, queryParams, pathParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...requestBody,
...queryParams,
...pathParams
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('storage'));
} else {
req.validate(roles.can(req.role).readAny('storage'));
}
let user = new ObjectId(result.value.user);
let file = new ObjectId(result.value.file);
let fileData;
try {
fileData = await db.gridfs.collection('storage.files').findOne({
_id: file,
'metadata.user': user
});
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!fileData) {
res.status(404);
return res.json({
error: 'This file does not exist',
code: 'FileNotFound'
});
}
res.writeHead(200, {
'Content-Type': fileData.contentType || 'application/octet-stream'
});
let stream = storageHandler.gridstore.openDownloadStream(file);
stream.once('error', err => {
try {
res.end(err.message);
} catch (err) {
//ignore
}
});
stream.pipe(res);
})
);
};