mirror of
https://github.com/plankanban/planka.git
synced 2025-12-24 17:25:00 +03:00
feat: Track storage usage
This commit is contained in:
@@ -10,6 +10,7 @@ SECRET_KEY=notsecretkey
|
||||
# LOG_FILE=
|
||||
|
||||
# TRUST_PROXY=true
|
||||
# MAX_UPLOAD_FILE_SIZE=
|
||||
# TOKEN_EXPIRES_IN=365 # In days
|
||||
|
||||
# related: https://github.com/knex/knex/issues/2354
|
||||
@@ -30,6 +31,7 @@ SECRET_KEY=notsecretkey
|
||||
# DEFAULT_ADMIN_USERNAME=demo
|
||||
|
||||
# INTERNAL_ACCESS_TOKEN=
|
||||
# STORAGE_LIMIT=
|
||||
# ACTIVE_USERS_LIMIT=
|
||||
|
||||
# Set to true to show more detailed authentication error messages.
|
||||
|
||||
@@ -91,10 +91,10 @@ module.exports = {
|
||||
throw Errors.INVALID_SIGNATURE;
|
||||
}
|
||||
|
||||
user = await User.qm.updateOne(user.id, {
|
||||
({ user } = await User.qm.updateOne(user.id, {
|
||||
termsSignature,
|
||||
termsAcceptedAt: new Date().toISOString(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
const config = await Config.qm.getOneMain();
|
||||
|
||||
@@ -91,7 +91,7 @@ module.exports = {
|
||||
if (inputs.type === Attachment.Types.FILE) {
|
||||
let files;
|
||||
try {
|
||||
files = await sails.helpers.utils.receiveFile('file', this.req);
|
||||
files = await sails.helpers.utils.receiveFile(this.req.file('file'));
|
||||
} catch (error) {
|
||||
return exits.uploadError(error.message); // TODO: add error
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ module.exports = {
|
||||
|
||||
let files;
|
||||
try {
|
||||
files = await sails.helpers.utils.receiveFile('file', this.req);
|
||||
files = await sails.helpers.utils.receiveFile(this.req.file('file'));
|
||||
} catch (error) {
|
||||
return exits.uploadError(error.message); // TODO: add error
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
async fn(inputs, exits) {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const project = await Project.qm.getOneById(inputs.projectId);
|
||||
@@ -78,7 +78,7 @@ module.exports = {
|
||||
if (inputs.importType) {
|
||||
let files;
|
||||
try {
|
||||
files = await sails.helpers.utils.receiveFile('importFile', this.req);
|
||||
files = await sails.helpers.utils.receiveFile(this.req.file('importFile'), false);
|
||||
} catch (error) {
|
||||
return exits.uploadError(error.message); // TODO: add error
|
||||
}
|
||||
@@ -114,11 +114,11 @@ module.exports = {
|
||||
request: this.req,
|
||||
});
|
||||
|
||||
return {
|
||||
return exits.success({
|
||||
item: board,
|
||||
included: {
|
||||
boardMemberships: [boardMembership],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ module.exports = {
|
||||
let readStream;
|
||||
try {
|
||||
readStream = await fileManager.read(
|
||||
`${sails.config.custom.attachmentsPathSegment}/${attachment.data.fileReferenceId}/thumbnails/${inputs.fileName}.${inputs.fileExtension}`,
|
||||
`${sails.config.custom.attachmentsPathSegment}/${attachment.data.uploadedFileId}/thumbnails/${inputs.fileName}.${inputs.fileExtension}`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw Errors.FILE_ATTACHMENT_NOT_FOUND;
|
||||
|
||||
@@ -72,7 +72,7 @@ module.exports = {
|
||||
let readStream;
|
||||
try {
|
||||
readStream = await fileManager.read(
|
||||
`${sails.config.custom.attachmentsPathSegment}/${attachment.data.fileReferenceId}/${attachment.data.filename}`,
|
||||
`${sails.config.custom.attachmentsPathSegment}/${attachment.data.uploadedFileId}/${attachment.data.filename}`,
|
||||
);
|
||||
} catch (error) {
|
||||
throw Errors.FILE_ATTACHMENT_NOT_FOUND;
|
||||
|
||||
@@ -58,7 +58,7 @@ module.exports = {
|
||||
|
||||
let files;
|
||||
try {
|
||||
files = await sails.helpers.utils.receiveFile('file', this.req);
|
||||
files = await sails.helpers.utils.receiveFile(this.req.file('file'));
|
||||
} catch (error) {
|
||||
return exits.uploadError(error.message); // TODO: add error
|
||||
}
|
||||
|
||||
@@ -51,13 +51,11 @@ module.exports = {
|
||||
});
|
||||
}
|
||||
|
||||
const { attachment, fileReference } = await Attachment.qm.deleteOne(inputs.record.id, {
|
||||
isFile: inputs.record.type === Attachment.Types.FILE,
|
||||
});
|
||||
const { attachment, uploadedFile } = await Attachment.qm.deleteOne(inputs.record.id);
|
||||
|
||||
if (attachment) {
|
||||
if (fileReference) {
|
||||
sails.helpers.attachments.removeUnreferencedFiles(fileReference);
|
||||
if (uploadedFile) {
|
||||
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFile);
|
||||
}
|
||||
|
||||
sails.sockets.broadcast(
|
||||
|
||||
@@ -20,7 +20,7 @@ module.exports = {
|
||||
...inputs.record,
|
||||
data: {
|
||||
..._.omit(inputs.record.data, [
|
||||
'fileReferenceId',
|
||||
'uploadedFileId',
|
||||
'filename',
|
||||
'image.thumbnailsExtension',
|
||||
]),
|
||||
|
||||
@@ -10,7 +10,7 @@ const mime = require('mime');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const filenamify = require('../../../utils/filenamify');
|
||||
const { MAX_SIZE_IN_BYTES_TO_GET_ENCODING } = require('../../../constants');
|
||||
const { MAX_SIZE_TO_GET_ENCODING } = require('../../../constants');
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
@@ -23,17 +23,22 @@ module.exports = {
|
||||
async fn(inputs) {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
const { id: fileReferenceId } = await FileReference.create().fetch();
|
||||
const dirPathSegment = `${sails.config.custom.attachmentsPathSegment}/${fileReferenceId}`;
|
||||
const filename = filenamify(inputs.file.filename);
|
||||
|
||||
const mimeType = mime.getType(filename);
|
||||
const sizeInBytes = inputs.file.size;
|
||||
const { size } = inputs.file;
|
||||
|
||||
const { id: uploadedFileId } = await UploadedFile.qm.createOne({
|
||||
mimeType,
|
||||
size,
|
||||
type: UploadedFile.Types.ATTACHMENT,
|
||||
});
|
||||
|
||||
const dirPathSegment = `${sails.config.custom.attachmentsPathSegment}/${uploadedFileId}`;
|
||||
|
||||
let buffer;
|
||||
let encoding = null;
|
||||
|
||||
if (sizeInBytes <= MAX_SIZE_IN_BYTES_TO_GET_ENCODING) {
|
||||
if (size <= MAX_SIZE_TO_GET_ENCODING) {
|
||||
try {
|
||||
buffer = await fsPromises.readFile(inputs.file.fd);
|
||||
} catch (error) {
|
||||
@@ -52,10 +57,10 @@ module.exports = {
|
||||
);
|
||||
|
||||
const data = {
|
||||
fileReferenceId,
|
||||
uploadedFileId,
|
||||
filename,
|
||||
mimeType,
|
||||
sizeInBytes,
|
||||
size,
|
||||
encoding,
|
||||
image: null,
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
fileReferenceOrFileReferences: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const fileReferences = _.isPlainObject(inputs.fileReferenceOrFileReferences)
|
||||
? [inputs.fileReferenceOrFileReferences]
|
||||
: inputs.fileReferenceOrFileReferences;
|
||||
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
fileReferences.forEach(async (fileReference) => {
|
||||
if (fileReference.total !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fileManager.deleteDir(
|
||||
`${sails.config.custom.attachmentsPathSegment}/${fileReference.id}`,
|
||||
);
|
||||
|
||||
await FileReference.destroyOne(fileReference.id);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -43,10 +43,10 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
|
||||
const backgroundImage = await BackgroundImage.qm.deleteOne(inputs.record.id);
|
||||
const { backgroundImage, uploadedFile } = await BackgroundImage.qm.deleteOne(inputs.record.id);
|
||||
|
||||
if (backgroundImage) {
|
||||
sails.helpers.backgroundImages.removeRelatedFiles(backgroundImage);
|
||||
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFile);
|
||||
|
||||
const projectRelatedUserIds = await scoper.getProjectRelatedUserIds();
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ module.exports = {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
return {
|
||||
..._.omit(inputs.record, ['dirname', 'extension']),
|
||||
url: `${fileManager.buildUrl(`${sails.config.custom.backgroundImagesPathSegment}/${inputs.record.dirname}/original.${inputs.record.extension}`)}`,
|
||||
..._.omit(inputs.record, ['uploadedFileId', 'extension']),
|
||||
url: `${fileManager.buildUrl(`${sails.config.custom.backgroundImagesPathSegment}/${inputs.record.uploadedFileId}/original.${inputs.record.extension}`)}`,
|
||||
thumbnailUrls: {
|
||||
outside360: `${fileManager.buildUrl(`${sails.config.custom.backgroundImagesPathSegment}/${inputs.record.dirname}/outside-360.${inputs.record.extension}`)}`,
|
||||
outside360: `${fileManager.buildUrl(`${sails.config.custom.backgroundImagesPathSegment}/${inputs.record.uploadedFileId}/outside-360.${inputs.record.extension}`)}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
const { rimraf } = require('rimraf');
|
||||
const mime = require('mime');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sharp = require('sharp');
|
||||
|
||||
module.exports = {
|
||||
@@ -32,8 +32,16 @@ module.exports = {
|
||||
});
|
||||
|
||||
let metadata;
|
||||
let originalBuffer;
|
||||
|
||||
try {
|
||||
metadata = await image.metadata();
|
||||
|
||||
if (metadata.orientation && metadata.orientation > 4) {
|
||||
image = image.rotate();
|
||||
}
|
||||
|
||||
originalBuffer = await image.toBuffer();
|
||||
} catch (error) {
|
||||
await rimraf(inputs.file.fd);
|
||||
throw 'fileIsNotImage';
|
||||
@@ -41,20 +49,19 @@ module.exports = {
|
||||
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
const dirname = uuid();
|
||||
const dirPathSegment = `${sails.config.custom.backgroundImagesPathSegment}/${dirname}`;
|
||||
|
||||
if (metadata.orientation && metadata.orientation > 4) {
|
||||
image = image.rotate();
|
||||
}
|
||||
|
||||
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
|
||||
const size = originalBuffer.length;
|
||||
|
||||
const { id: uploadedFileId } = await UploadedFile.qm.createOne({
|
||||
mimeType,
|
||||
size,
|
||||
id: uuid(),
|
||||
type: UploadedFile.Types.BACKGROUND_IMAGE,
|
||||
});
|
||||
|
||||
const dirPathSegment = `${sails.config.custom.backgroundImagesPathSegment}/${uploadedFileId}`;
|
||||
|
||||
let sizeInBytes;
|
||||
try {
|
||||
const originalBuffer = await image.toBuffer();
|
||||
sizeInBytes = originalBuffer.length;
|
||||
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/original.${extension}`,
|
||||
originalBuffer,
|
||||
@@ -82,6 +89,7 @@ module.exports = {
|
||||
|
||||
await fileManager.deleteDir(dirPathSegment);
|
||||
await rimraf(inputs.file.fd);
|
||||
await UploadedFile.qm.deleteOne(uploadedFileId);
|
||||
|
||||
throw 'fileIsNotImage';
|
||||
}
|
||||
@@ -89,9 +97,9 @@ module.exports = {
|
||||
await rimraf(inputs.file.fd);
|
||||
|
||||
return {
|
||||
dirname,
|
||||
uploadedFileId,
|
||||
extension,
|
||||
sizeInBytes,
|
||||
size,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
recordOrRecords: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const backgroundImages = _.isPlainObject(inputs.recordOrRecords)
|
||||
? [inputs.recordOrRecords]
|
||||
: inputs.recordOrRecords;
|
||||
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
backgroundImages.forEach(async (backgroundImage) => {
|
||||
await fileManager.deleteDir(
|
||||
`${sails.config.custom.backgroundImagesPathSegment}/${backgroundImage.dirname}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -48,11 +48,11 @@ module.exports = {
|
||||
},
|
||||
);
|
||||
|
||||
const { fileReferences } = await Attachment.qm.delete({
|
||||
const { uploadedFiles } = await Attachment.qm.delete({
|
||||
cardId: cardIdOrIds,
|
||||
});
|
||||
|
||||
sails.helpers.attachments.removeUnreferencedFiles(fileReferences);
|
||||
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFiles);
|
||||
|
||||
const customFieldGroups = await CustomFieldGroup.qm.delete({
|
||||
cardId: cardIdOrIds,
|
||||
|
||||
@@ -29,11 +29,11 @@ module.exports = {
|
||||
projectId: projectIdOrIds,
|
||||
});
|
||||
|
||||
const backgroundImages = await BackgroundImage.qm.delete({
|
||||
const { uploadedFiles } = await BackgroundImage.qm.delete({
|
||||
projectId: projectIdOrIds,
|
||||
});
|
||||
|
||||
sails.helpers.backgroundImages.removeRelatedFiles(backgroundImages);
|
||||
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFiles);
|
||||
|
||||
const baseCustomFieldGroups = await BaseCustomFieldGroup.qm.delete({
|
||||
projectId: projectIdOrIds,
|
||||
|
||||
@@ -23,10 +23,12 @@ module.exports = {
|
||||
inputs.record,
|
||||
);
|
||||
|
||||
const user = await User.qm.deleteOne(inputs.record.id);
|
||||
const { user, uploadedFile } = await User.qm.deleteOne(inputs.record.id);
|
||||
|
||||
if (user) {
|
||||
sails.helpers.users.removeRelatedFiles(user);
|
||||
if (uploadedFile) {
|
||||
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFile);
|
||||
}
|
||||
|
||||
const scoper = sails.helpers.users.makeScoper(user);
|
||||
scoper.boardMemberships = boardMemberships;
|
||||
|
||||
@@ -28,9 +28,9 @@ module.exports = {
|
||||
'termsAcceptedAt',
|
||||
]),
|
||||
avatar: inputs.record.avatar && {
|
||||
url: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}/original.${inputs.record.avatar.extension}`)}`,
|
||||
url: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.uploadedFileId}/original.${inputs.record.avatar.extension}`)}`,
|
||||
thumbnailUrls: {
|
||||
cover180: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}/cover-180.${inputs.record.avatar.extension}`)}`,
|
||||
cover180: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.uploadedFileId}/cover-180.${inputs.record.avatar.extension}`)}`,
|
||||
},
|
||||
},
|
||||
termsType: sails.hooks.terms.getTypeByUserRole(inputs.record.role),
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
const { rimraf } = require('rimraf');
|
||||
const mime = require('mime');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sharp = require('sharp');
|
||||
|
||||
module.exports = {
|
||||
@@ -32,8 +32,16 @@ module.exports = {
|
||||
});
|
||||
|
||||
let metadata;
|
||||
let originalBuffer;
|
||||
|
||||
try {
|
||||
metadata = await image.metadata();
|
||||
|
||||
if (metadata.orientation && metadata.orientation > 4) {
|
||||
image = image.rotate();
|
||||
}
|
||||
|
||||
originalBuffer = await image.toBuffer();
|
||||
} catch (error) {
|
||||
await rimraf(inputs.file.fd);
|
||||
throw 'fileIsNotImage';
|
||||
@@ -41,20 +49,19 @@ module.exports = {
|
||||
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
const dirname = uuid();
|
||||
const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${dirname}`;
|
||||
|
||||
if (metadata.orientation && metadata.orientation > 4) {
|
||||
image = image.rotate();
|
||||
}
|
||||
|
||||
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
|
||||
const size = originalBuffer.length;
|
||||
|
||||
const { id: uploadedFileId } = await UploadedFile.qm.createOne({
|
||||
mimeType,
|
||||
size,
|
||||
id: uuid(),
|
||||
type: UploadedFile.Types.USER_AVATAR,
|
||||
});
|
||||
|
||||
const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${uploadedFileId}`;
|
||||
|
||||
let sizeInBytes;
|
||||
try {
|
||||
const originalBuffer = await image.toBuffer();
|
||||
sizeInBytes = originalBuffer.length;
|
||||
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/original.${extension}`,
|
||||
originalBuffer,
|
||||
@@ -81,6 +88,7 @@ module.exports = {
|
||||
|
||||
await fileManager.deleteDir(dirPathSegment);
|
||||
await rimraf(inputs.file.fd);
|
||||
await UploadedFile.qm.deleteOne(uploadedFileId);
|
||||
|
||||
throw 'fileIsNotImage';
|
||||
}
|
||||
@@ -88,9 +96,9 @@ module.exports = {
|
||||
await rimraf(inputs.file.fd);
|
||||
|
||||
return {
|
||||
dirname,
|
||||
uploadedFileId,
|
||||
extension,
|
||||
sizeInBytes,
|
||||
size,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -69,8 +69,10 @@ module.exports = {
|
||||
}
|
||||
|
||||
let user;
|
||||
let uploadedFile;
|
||||
|
||||
try {
|
||||
user = await User.qm.updateOne(inputs.record.id, values);
|
||||
({ user, uploadedFile } = await User.qm.updateOne(inputs.record.id, values));
|
||||
} catch (error) {
|
||||
if (error.code === 'E_UNIQUE') {
|
||||
throw 'emailAlreadyInUse';
|
||||
@@ -91,10 +93,8 @@ module.exports = {
|
||||
}
|
||||
|
||||
if (user) {
|
||||
if (inputs.record.avatar) {
|
||||
if (!user.avatar || user.avatar.dirname !== inputs.record.avatar.dirname) {
|
||||
sails.helpers.users.removeRelatedFiles(inputs.record);
|
||||
}
|
||||
if (uploadedFile) {
|
||||
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFile);
|
||||
}
|
||||
|
||||
if (!_.isUndefined(values.password) || isDeactivatedChangeToTrue) {
|
||||
|
||||
@@ -8,7 +8,7 @@ const icoToPng = require('ico-to-png');
|
||||
const sharp = require('sharp');
|
||||
|
||||
const FETCH_TIMEOUT = 4000;
|
||||
const MAX_RESPONSE_LENGTH_IN_BYTES = 1024 * 1024;
|
||||
const MAX_RESPONSE_LENGTH = 1024 * 1024;
|
||||
|
||||
const FAVICON_TAGS_REGEX = /<link [^>]*rel="([^"]* )?icon( [^"]*)?"[^>]*>/gi;
|
||||
const HREF_REGEX = /href="(.*?)"/i;
|
||||
@@ -39,7 +39,7 @@ const readResponse = async (response) => {
|
||||
chunks.push(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
if (receivedLength > MAX_RESPONSE_LENGTH_IN_BYTES) {
|
||||
if (receivedLength > MAX_RESPONSE_LENGTH) {
|
||||
reader.cancel();
|
||||
|
||||
return {
|
||||
@@ -133,6 +133,12 @@ module.exports = {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableStorage = await sails.helpers.utils.getAvailableStorage();
|
||||
|
||||
if (availableStorage !== null && readedResponse.buffer.length >= availableStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
let image = sharp(readedResponse.buffer);
|
||||
|
||||
let metadata;
|
||||
|
||||
17
server/api/helpers/utils/get-available-storage.js
Normal file
17
server/api/helpers/utils/get-available-storage.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
async fn() {
|
||||
const { storageLimit } = sails.config.custom;
|
||||
|
||||
if (_.isNil(storageLimit)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const storageUsage = await StorageUsage.qm.getOneMain();
|
||||
return BigInt(storageLimit) - BigInt(storageUsage.total);
|
||||
},
|
||||
};
|
||||
@@ -4,37 +4,55 @@
|
||||
*/
|
||||
|
||||
const util = require('util');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
module.exports = {
|
||||
friendlyName: 'Receive uploaded file from request',
|
||||
|
||||
description:
|
||||
'Store a file uploaded from a MIME-multipart request part. The resulting file will have a unique UUID-based name with the same extension.',
|
||||
|
||||
inputs: {
|
||||
paramName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The MIME multi-part parameter containing the file to receive.',
|
||||
},
|
||||
req: {
|
||||
file: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
description: 'The request to receive the file from.',
|
||||
},
|
||||
enforceStorageLimit: {
|
||||
type: 'boolean',
|
||||
defaultsTo: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs, exits) {
|
||||
const { maxUploadFileSize } = sails.config.custom;
|
||||
|
||||
let availableStorage = null;
|
||||
if (inputs.enforceStorageLimit) {
|
||||
availableStorage = await sails.helpers.utils.getAvailableStorage();
|
||||
}
|
||||
|
||||
let maxBytes = _.isNil(maxUploadFileSize) ? null : maxUploadFileSize;
|
||||
if (availableStorage !== null) {
|
||||
if (maxBytes) {
|
||||
maxBytes = availableStorage < maxBytes ? availableStorage : maxBytes;
|
||||
} else {
|
||||
maxBytes = availableStorage;
|
||||
}
|
||||
}
|
||||
|
||||
const upload = util.promisify((options, callback) =>
|
||||
inputs.req.file(inputs.paramName).upload(options, (error, files) => callback(error, files)),
|
||||
inputs.file.upload(options, (error, files) => {
|
||||
if (
|
||||
error &&
|
||||
error.code === 'E_EXCEEDS_UPLOAD_LIMIT' &&
|
||||
availableStorage !== null &&
|
||||
(_.isNil(maxUploadFileSize) || error.maxBytes < maxUploadFileSize)
|
||||
) {
|
||||
return callback(new Error('Storage limit reached'), files);
|
||||
}
|
||||
|
||||
return callback(error, files);
|
||||
}),
|
||||
);
|
||||
|
||||
return exits.success(
|
||||
await upload({
|
||||
maxBytes,
|
||||
dirname: sails.config.custom.uploadsTempPath,
|
||||
saveAs: uuid(),
|
||||
maxBytes: null,
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { Types } = require('../../models/UploadedFile');
|
||||
|
||||
const PATH_SEGMENT_BY_TYPE = {
|
||||
[Types.USER_AVATAR]: sails.config.custom.userAvatarsPathSegment,
|
||||
[Types.BACKGROUND_IMAGE]: sails.config.custom.backgroundImagesPathSegment,
|
||||
[Types.ATTACHMENT]: sails.config.custom.attachmentsPathSegment,
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
uploadedFileOrUploadedFiles: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const uploadedFiles = _.isPlainObject(inputs.uploadedFileOrUploadedFiles)
|
||||
? [inputs.uploadedFileOrUploadedFiles]
|
||||
: inputs.uploadedFileOrUploadedFiles;
|
||||
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
// TODO: optimize?
|
||||
uploadedFiles.forEach(async (uploadedFile) => {
|
||||
if (uploadedFile.referencesTotal !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fileManager.deleteDir(`${PATH_SEGMENT_BY_TYPE[uploadedFile.type]}/${uploadedFile.id}`);
|
||||
await UploadedFile.qm.deleteOne(uploadedFile.id);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -44,7 +44,7 @@ class LocalFileManager {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async getSizeInBytes(filePathSegment) {
|
||||
async getSize(filePathSegment) {
|
||||
let result;
|
||||
try {
|
||||
result = await fs.promises.stat(buildPath(filePathSegment));
|
||||
|
||||
@@ -52,7 +52,7 @@ class S3FileManager {
|
||||
return result.Body;
|
||||
}
|
||||
|
||||
async getSizeInBytes(filePathSegment) {
|
||||
async getSize(filePathSegment) {
|
||||
const headObjectCommand = new HeadObjectCommand({
|
||||
Bucket: sails.config.custom.s3Bucket,
|
||||
Key: filePathSegment,
|
||||
|
||||
36
server/api/hooks/query-methods/helpers.js
Normal file
36
server/api/hooks/query-methods/helpers.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const makeWhereQueryBuilder = (Model) => (criteria) => {
|
||||
if (_.isPlainObject(criteria)) {
|
||||
if (Object.keys(criteria).length === 0) {
|
||||
throw new Error('Empty criteria');
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
const values = [];
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of Object.entries(criteria)) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const columnName = Model._transformer._transformations[key];
|
||||
|
||||
if (!columnName) {
|
||||
throw new Error('Unknown column');
|
||||
}
|
||||
|
||||
parts.push(`${columnName} = $${index + 1}`);
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
return [parts.join(' AND '), values];
|
||||
}
|
||||
|
||||
return ['id = $1', [criteria]];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
makeWhereQueryBuilder,
|
||||
};
|
||||
@@ -13,25 +13,24 @@ const create = (arrayOfValues) => {
|
||||
const arrayOfFileValues = arrayOfValues.filter(({ type }) => type === Attachment.Types.FILE);
|
||||
|
||||
if (arrayOfFileValues.length > 0) {
|
||||
const arrayOfValuesByFileReferenceId = _.groupBy(arrayOfFileValues, 'data.fileReferenceId');
|
||||
const arrayOfValuesByUploadedFileId = _.groupBy(arrayOfFileValues, 'data.uploadedFileId');
|
||||
const uploadedFileIds = Object.keys(arrayOfValuesByUploadedFileId);
|
||||
|
||||
const fileReferenceIds = Object.keys(arrayOfValuesByFileReferenceId);
|
||||
|
||||
const fileReferenceIdsByTotal = Object.entries(arrayOfValuesByFileReferenceId).reduce(
|
||||
(result, [fileReferenceId, arrayOfValuesItem]) => ({
|
||||
const uploadedFileIdsByTotal = Object.entries(arrayOfValuesByUploadedFileId).reduce(
|
||||
(result, [uploadedFileId, arrayOfValuesItem]) => ({
|
||||
...result,
|
||||
[arrayOfValuesItem.length]: [...(result[arrayOfValuesItem.length] || []), fileReferenceId],
|
||||
[arrayOfValuesItem.length]: [...(result[arrayOfValuesItem.length] || []), uploadedFileId],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
return sails.getDatastore().transaction(async (db) => {
|
||||
const queryValues = [];
|
||||
let query = `UPDATE file_reference SET total = total + CASE `;
|
||||
let query = `UPDATE uploaded_file SET references_total = references_total + CASE `;
|
||||
|
||||
Object.entries(fileReferenceIdsByTotal).forEach(([total, fileReferenceIdsItem]) => {
|
||||
const inValues = fileReferenceIdsItem.map((fileReferenceId) => {
|
||||
queryValues.push(fileReferenceId);
|
||||
Object.entries(uploadedFileIdsByTotal).forEach(([total, uploadedFileIdsItem]) => {
|
||||
const inValues = uploadedFileIdsItem.map((uploadedFileId) => {
|
||||
queryValues.push(uploadedFileId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
@@ -39,25 +38,25 @@ const create = (arrayOfValues) => {
|
||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
||||
});
|
||||
|
||||
const inValues = fileReferenceIds.map((fileReferenceId) => {
|
||||
queryValues.push(fileReferenceId);
|
||||
const inValues = uploadedFileIds.map((uploadedFileId) => {
|
||||
queryValues.push(uploadedFileId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
queryValues.push(new Date().toISOString());
|
||||
query += `END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')}) AND total IS NOT NULL RETURNING id`;
|
||||
query += `END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')}) AND references_total IS NOT NULL RETURNING id`;
|
||||
|
||||
const queryResult = await sails.sendNativeQuery(query, queryValues).usingConnection(db);
|
||||
const nextFileReferenceIds = sails.helpers.utils.mapRecords(queryResult.rows);
|
||||
const nextUploadedFileIds = sails.helpers.utils.mapRecords(queryResult.rows);
|
||||
|
||||
if (nextFileReferenceIds.length < fileReferenceIds.length) {
|
||||
const nextFileReferenceIdsSet = new Set(nextFileReferenceIds);
|
||||
if (nextUploadedFileIds.length < uploadedFileIds.length) {
|
||||
const nextUploadedFileIdsSet = new Set(nextUploadedFileIds);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
arrayOfValues = arrayOfValues.filter(
|
||||
(values) =>
|
||||
values.type !== Attachment.Types.FILE ||
|
||||
nextFileReferenceIdsSet.has(values.data.fileReferenceId),
|
||||
nextUploadedFileIdsSet.has(values.data.uploadedFileId),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,8 +69,6 @@ const create = (arrayOfValues) => {
|
||||
|
||||
const createOne = (values) => {
|
||||
if (values.type === Attachment.Types.FILE) {
|
||||
const { fileReferenceId } = values.data;
|
||||
|
||||
return sails.getDatastore().transaction(async (db) => {
|
||||
const attachment = await Attachment.create({ ...values })
|
||||
.fetch()
|
||||
@@ -79,13 +76,13 @@ const createOne = (values) => {
|
||||
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE file_reference SET total = total + 1, updated_at = $1 WHERE id = $2 AND total IS NOT NULL',
|
||||
[new Date().toISOString(), fileReferenceId],
|
||||
'UPDATE uploaded_file SET references_total = references_total + 1, updated_at = $1 WHERE id = $2 AND references_total IS NOT NULL',
|
||||
[new Date().toISOString(), values.data.uploadedFileId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
if (queryResult.rowCount === 0) {
|
||||
throw 'fileReferenceNotFound';
|
||||
throw 'uploadedFileNotFound';
|
||||
}
|
||||
|
||||
return attachment;
|
||||
@@ -129,24 +126,24 @@ const delete_ = (criteria) =>
|
||||
const attachments = await Attachment.destroy(criteria).fetch().usingConnection(db);
|
||||
const fileAttachments = attachments.filter(({ type }) => type === Attachment.Types.FILE);
|
||||
|
||||
let fileReferences = [];
|
||||
let uploadedFiles = [];
|
||||
if (fileAttachments.length > 0) {
|
||||
const attachmentsByFileReferenceId = _.groupBy(fileAttachments, 'data.fileReferenceId');
|
||||
const attachmentsByUploadedFileId = _.groupBy(fileAttachments, 'data.uploadedFileId');
|
||||
|
||||
const fileReferenceIdsByTotal = Object.entries(attachmentsByFileReferenceId).reduce(
|
||||
(result, [fileReferenceId, attachmentsItem]) => ({
|
||||
const uploadedFileIdsByTotal = Object.entries(attachmentsByUploadedFileId).reduce(
|
||||
(result, [uploadedFileId, attachmentsItem]) => ({
|
||||
...result,
|
||||
[attachmentsItem.length]: [...(result[attachmentsItem.length] || []), fileReferenceId],
|
||||
[attachmentsItem.length]: [...(result[attachmentsItem.length] || []), uploadedFileId],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const queryValues = [];
|
||||
let query = 'UPDATE file_reference SET total = CASE WHEN total = CASE ';
|
||||
let query = 'UPDATE uploaded_file SET references_total = CASE WHEN references_total = CASE ';
|
||||
|
||||
Object.entries(fileReferenceIdsByTotal).forEach(([total, fileReferenceIds]) => {
|
||||
const inValues = fileReferenceIds.map((fileReferenceId) => {
|
||||
queryValues.push(fileReferenceId);
|
||||
Object.entries(uploadedFileIdsByTotal).forEach(([total, uploadedFileIds]) => {
|
||||
const inValues = uploadedFileIds.map((uploadedFileId) => {
|
||||
queryValues.push(uploadedFileId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
@@ -154,11 +151,11 @@ const delete_ = (criteria) =>
|
||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
||||
});
|
||||
|
||||
query += 'END THEN NULL ELSE total - CASE ';
|
||||
query += 'END THEN NULL ELSE references_total - CASE ';
|
||||
|
||||
Object.entries(fileReferenceIdsByTotal).forEach(([total, fileReferenceIds]) => {
|
||||
const inValues = fileReferenceIds.map((fileReferenceId) => {
|
||||
queryValues.push(fileReferenceId);
|
||||
Object.entries(uploadedFileIdsByTotal).forEach(([total, uploadedFileIds]) => {
|
||||
const inValues = uploadedFileIds.map((uploadedFileId) => {
|
||||
queryValues.push(uploadedFileId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
@@ -166,56 +163,58 @@ const delete_ = (criteria) =>
|
||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
||||
});
|
||||
|
||||
const inValues = Object.keys(attachmentsByFileReferenceId).map((fileReferenceId) => {
|
||||
queryValues.push(fileReferenceId);
|
||||
const inValues = Object.keys(attachmentsByUploadedFileId).map((uploadedFileId) => {
|
||||
queryValues.push(uploadedFileId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
queryValues.push(new Date().toISOString());
|
||||
query += `END END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')}) AND total IS NOT NULL RETURNING id, total`;
|
||||
query += `END END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')}) AND references_total IS NOT NULL RETURNING *`;
|
||||
|
||||
const queryResult = await sails.sendNativeQuery(query, queryValues).usingConnection(db);
|
||||
fileReferences = queryResult.rows;
|
||||
|
||||
uploadedFiles = queryResult.rows.map((row) => ({
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
mimeType: row.mime_type,
|
||||
size: row.size,
|
||||
referencesTotal: row.references_total,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
attachments,
|
||||
fileReferences,
|
||||
};
|
||||
return { attachments, uploadedFiles };
|
||||
});
|
||||
|
||||
const deleteOne = async (criteria, { isFile } = {}) => {
|
||||
let fileReference = null;
|
||||
const deleteOne = (criteria) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const attachment = await Attachment.destroyOne(criteria).usingConnection(db);
|
||||
|
||||
if (isFile) {
|
||||
return sails.getDatastore().transaction(async (db) => {
|
||||
const attachment = await Attachment.destroyOne(criteria).usingConnection(db);
|
||||
let uploadedFile;
|
||||
if (attachment.type === Attachment.Types.FILE) {
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE uploaded_file SET references_total = CASE WHEN references_total > 1 THEN references_total - 1 END, updated_at = $1 WHERE id = $2 RETURNING *',
|
||||
[new Date().toISOString(), attachment.data.uploadedFileId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
if (attachment.type === Attachment.Types.FILE) {
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE file_reference SET total = CASE WHEN total > 1 THEN total - 1 END, updated_at = $1 WHERE id = $2 RETURNING id, total',
|
||||
[new Date().toISOString(), attachment.data.fileReferenceId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
const [row] = queryResult.rows;
|
||||
|
||||
[fileReference] = queryResult.rows;
|
||||
}
|
||||
|
||||
return {
|
||||
attachment,
|
||||
fileReference,
|
||||
uploadedFile = {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
mimeType: row.mime_type,
|
||||
size: row.size,
|
||||
referencesTotal: row.references_total,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const attachment = await Attachment.destroyOne(criteria);
|
||||
|
||||
return {
|
||||
attachment,
|
||||
fileReference,
|
||||
};
|
||||
};
|
||||
return { attachment, uploadedFile };
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
|
||||
@@ -7,7 +7,25 @@ const defaultFind = (criteria) => BackgroundImage.find(criteria).sort('id');
|
||||
|
||||
/* Query methods */
|
||||
|
||||
const createOne = (values) => BackgroundImage.create({ ...values }).fetch();
|
||||
const createOne = (values) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const backgroundImage = await BackgroundImage.create({ ...values })
|
||||
.fetch()
|
||||
.usingConnection(db);
|
||||
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE uploaded_file SET references_total = references_total + 1, updated_at = $1 WHERE id = $2 AND references_total IS NOT NULL',
|
||||
[new Date().toISOString(), values.uploadedFileId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
if (queryResult.rowCount === 0) {
|
||||
throw 'uploadedFileNotFound';
|
||||
}
|
||||
|
||||
return backgroundImage;
|
||||
});
|
||||
|
||||
const getByIds = (ids) => defaultFind(ids);
|
||||
|
||||
@@ -34,9 +52,99 @@ const getOneById = (id, { projectId } = {}) => {
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const delete_ = (criteria) => BackgroundImage.destroy(criteria).fetch();
|
||||
const delete_ = (criteria) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const backgroundImages = await BackgroundImage.destroy(criteria).fetch().usingConnection(db);
|
||||
|
||||
const deleteOne = (criteria) => BackgroundImage.destroyOne(criteria);
|
||||
let uploadedFiles = [];
|
||||
if (backgroundImages.length > 0) {
|
||||
const backgroundImagesByUploadedFileId = _.groupBy(backgroundImages, 'uploadedFileId');
|
||||
|
||||
const uploadedFileIdsByTotal = Object.entries(backgroundImagesByUploadedFileId).reduce(
|
||||
(result, [uploadedFileId, backgroundImagesItem]) => ({
|
||||
...result,
|
||||
[backgroundImagesItem.length]: [
|
||||
...(result[backgroundImagesItem.length] || []),
|
||||
uploadedFileId,
|
||||
],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const queryValues = [];
|
||||
let query = 'UPDATE uploaded_file SET references_total = CASE WHEN references_total = CASE ';
|
||||
|
||||
Object.entries(uploadedFileIdsByTotal).forEach(([total, uploadedFileIds]) => {
|
||||
const inValues = uploadedFileIds.map((uploadedFileId) => {
|
||||
queryValues.push(uploadedFileId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
queryValues.push(total);
|
||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
||||
});
|
||||
|
||||
query += 'END THEN NULL ELSE references_total - CASE ';
|
||||
|
||||
Object.entries(uploadedFileIdsByTotal).forEach(([total, uploadedFileIds]) => {
|
||||
const inValues = uploadedFileIds.map((uploadedFileId) => {
|
||||
queryValues.push(uploadedFileId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
queryValues.push(total);
|
||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
||||
});
|
||||
|
||||
const inValues = Object.keys(backgroundImagesByUploadedFileId).map((uploadedFileId) => {
|
||||
queryValues.push(uploadedFileId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
queryValues.push(new Date().toISOString());
|
||||
query += `END END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')}) AND references_total IS NOT NULL RETURNING *`;
|
||||
|
||||
const queryResult = await sails.sendNativeQuery(query, queryValues).usingConnection(db);
|
||||
|
||||
uploadedFiles = queryResult.rows.map((row) => ({
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
mimeType: row.mime_type,
|
||||
size: row.size,
|
||||
referencesTotal: row.references_total,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
}
|
||||
|
||||
return { backgroundImages, uploadedFiles };
|
||||
});
|
||||
|
||||
const deleteOne = (criteria) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const backgroundImage = await BackgroundImage.destroyOne(criteria).usingConnection(db);
|
||||
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE uploaded_file SET references_total = CASE WHEN references_total > 1 THEN references_total - 1 END, updated_at = $1 WHERE id = $2 RETURNING *',
|
||||
[new Date().toISOString(), backgroundImage.uploadedFileId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
const [row] = queryResult.rows;
|
||||
|
||||
uploadedFile = {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
mimeType: row.mime_type,
|
||||
size: row.size,
|
||||
referencesTotal: row.references_total,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
|
||||
return { backgroundImage, uploadedFile };
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createOne,
|
||||
|
||||
@@ -217,18 +217,12 @@ const update = async (criteria, values) => {
|
||||
.usingConnection(db);
|
||||
}
|
||||
|
||||
return {
|
||||
cards,
|
||||
tasks,
|
||||
};
|
||||
return { cards, tasks };
|
||||
});
|
||||
}
|
||||
|
||||
const cards = await Card.update(criteria).set(values).fetch();
|
||||
|
||||
return {
|
||||
cards,
|
||||
};
|
||||
return { cards };
|
||||
};
|
||||
|
||||
const updateOne = async (criteria, values) => {
|
||||
@@ -250,18 +244,12 @@ const updateOne = async (criteria, values) => {
|
||||
.usingConnection(db);
|
||||
}
|
||||
|
||||
return {
|
||||
card,
|
||||
tasks,
|
||||
};
|
||||
return { card, tasks };
|
||||
});
|
||||
}
|
||||
|
||||
const card = await Card.updateOne(criteria).set({ ...values });
|
||||
|
||||
return {
|
||||
card,
|
||||
};
|
||||
return { card };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
|
||||
@@ -26,16 +26,16 @@ const createOrUpdateOne = async (values) => {
|
||||
new Date().toISOString(),
|
||||
]);
|
||||
|
||||
const [customFieldValue] = queryResult.rows;
|
||||
const [row] = queryResult.rows;
|
||||
|
||||
return {
|
||||
id: customFieldValue.id,
|
||||
cardId: customFieldValue.card_id,
|
||||
customFieldGroupId: customFieldValue.custom_field_group_id,
|
||||
customFieldId: customFieldValue.custom_field_id,
|
||||
content: customFieldValue.content,
|
||||
createdAt: customFieldValue.created_at,
|
||||
updatedAt: customFieldValue.updated_at,
|
||||
id: row.id,
|
||||
cardId: row.card_id,
|
||||
customFieldGroupId: row.custom_field_group_id,
|
||||
customFieldId: row.custom_field_id,
|
||||
content: row.content,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { makeWhereQueryBuilder } = require('../helpers');
|
||||
|
||||
const buildWhereQuery = makeWhereQueryBuilder(List);
|
||||
|
||||
const defaultFind = (criteria, { sort = 'id' } = {}) => List.find(criteria).sort(sort);
|
||||
|
||||
/* Query methods */
|
||||
@@ -48,8 +52,20 @@ const getOneTrashByBoardId = (boardId) =>
|
||||
});
|
||||
|
||||
const updateOne = async (criteria, values) => {
|
||||
if (values.type) {
|
||||
if (!_.isUndefined(values.type)) {
|
||||
return sails.getDatastore().transaction(async (db) => {
|
||||
const [whereQuery, whereQueryValues] = buildWhereQuery(criteria);
|
||||
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(`SELECT type FROM list WHERE ${whereQuery} FOR UPDATE`, whereQueryValues)
|
||||
.usingConnection(db);
|
||||
|
||||
if (queryResult.rowCount === 0) {
|
||||
return { list: null };
|
||||
}
|
||||
|
||||
const [{ type: prevType }] = queryResult.rows;
|
||||
|
||||
const list = await List.updateOne(criteria)
|
||||
.set({ ...values })
|
||||
.usingConnection(db);
|
||||
@@ -58,7 +74,7 @@ const updateOne = async (criteria, values) => {
|
||||
let tasks = [];
|
||||
|
||||
if (list) {
|
||||
const prevTypeState = List.TYPE_STATE_BY_TYPE[prevList.type];
|
||||
const prevTypeState = List.TYPE_STATE_BY_TYPE[prevType];
|
||||
const typeState = List.TYPE_STATE_BY_TYPE[list.type];
|
||||
|
||||
let isClosed;
|
||||
@@ -94,19 +110,12 @@ const updateOne = async (criteria, values) => {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
cards,
|
||||
tasks,
|
||||
};
|
||||
return { list, cards, tasks };
|
||||
});
|
||||
}
|
||||
|
||||
const list = await List.updateOne(criteria).set({ ...values });
|
||||
|
||||
return {
|
||||
list,
|
||||
};
|
||||
return { list };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
|
||||
12
server/api/hooks/query-methods/models/StorageUsage.js
Normal file
12
server/api/hooks/query-methods/models/StorageUsage.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/* Query methods */
|
||||
|
||||
const getOneMain = () => StorageUsage.findOne(StorageUsage.MAIN_ID);
|
||||
|
||||
module.exports = {
|
||||
getOneMain,
|
||||
};
|
||||
50
server/api/hooks/query-methods/models/UploadedFile.js
Normal file
50
server/api/hooks/query-methods/models/UploadedFile.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const COLUMN_NAME_BY_TYPE = {
|
||||
[UploadedFile.Types.USER_AVATAR]: 'user_avatars',
|
||||
[UploadedFile.Types.BACKGROUND_IMAGE]: 'background_images',
|
||||
[UploadedFile.Types.ATTACHMENT]: 'attachments',
|
||||
};
|
||||
|
||||
/* Query methods */
|
||||
|
||||
const createOne = (values) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const uploadedFile = await UploadedFile.create({ ...values })
|
||||
.fetch()
|
||||
.usingConnection(db);
|
||||
|
||||
const columnName = COLUMN_NAME_BY_TYPE[uploadedFile.type];
|
||||
|
||||
await sails
|
||||
.sendNativeQuery(
|
||||
`UPDATE storage_usage SET total = total + $1, ${columnName} = ${columnName} + $1, updated_at = $2 WHERE id = $3`,
|
||||
[uploadedFile.size, new Date().toISOString(), StorageUsage.MAIN_ID],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
return uploadedFile;
|
||||
});
|
||||
|
||||
const deleteOne = (criteria) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const uploadedFile = await UploadedFile.destroyOne(criteria).usingConnection(db);
|
||||
const columnName = COLUMN_NAME_BY_TYPE[uploadedFile.type];
|
||||
|
||||
await sails
|
||||
.sendNativeQuery(
|
||||
`UPDATE storage_usage SET total = total - $1, ${columnName} = ${columnName} - $1, updated_at = $2 WHERE id = $3`,
|
||||
[uploadedFile.size, new Date().toISOString(), StorageUsage.MAIN_ID],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
return uploadedFile;
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createOne,
|
||||
deleteOne,
|
||||
};
|
||||
@@ -3,12 +3,28 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { makeWhereQueryBuilder } = require('../helpers');
|
||||
|
||||
const hasAvatarChanged = (avatar, prevAvatar) => {
|
||||
if (!avatar && !prevAvatar) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!avatar || !prevAvatar) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return avatar.uploadedFileId !== prevAvatar.uploadedFileId;
|
||||
};
|
||||
|
||||
const buildWhereQuery = makeWhereQueryBuilder(User);
|
||||
|
||||
const defaultFind = (criteria) => User.find(criteria).sort('id');
|
||||
|
||||
/* Query methods */
|
||||
|
||||
const createOne = (values) => {
|
||||
if (sails.config.custom.activeUsersLimit) {
|
||||
if (!_.isNil(sails.config.custom.activeUsersLimit)) {
|
||||
return sails.getDatastore().transaction(async (db) => {
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery('SELECT NULL FROM user_account WHERE is_deactivated = $1 FOR UPDATE', [
|
||||
@@ -62,29 +78,119 @@ const getOneActiveByEmailOrUsername = (emailOrUsername) => {
|
||||
});
|
||||
};
|
||||
|
||||
const updateOne = (criteria, values) => {
|
||||
if (values.isDeactivated === false && sails.config.custom.activeUsersLimit) {
|
||||
return sails.getDatastore().transaction(async (db) => {
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery('SELECT NULL FROM user_account WHERE is_deactivated = $1 FOR UPDATE', [
|
||||
false,
|
||||
])
|
||||
.usingConnection(db);
|
||||
const updateOne = async (criteria, values) => {
|
||||
const enforceActiveLimit =
|
||||
values.isDeactivated === false && !_.isNil(sails.config.custom.activeUsersLimit);
|
||||
|
||||
if (queryResult.rowCount >= sails.config.custom.activeUsersLimit) {
|
||||
throw 'activeLimitReached';
|
||||
if (!_.isUndefined(values.avatar) || enforceActiveLimit) {
|
||||
return sails.getDatastore().transaction(async (db) => {
|
||||
if (enforceActiveLimit) {
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery('SELECT NULL FROM user_account WHERE is_deactivated = $1 FOR UPDATE', [
|
||||
false,
|
||||
])
|
||||
.usingConnection(db);
|
||||
|
||||
if (queryResult.rowCount >= sails.config.custom.activeUsersLimit) {
|
||||
throw 'activeLimitReached';
|
||||
}
|
||||
}
|
||||
|
||||
return User.updateOne(criteria)
|
||||
let prevAvatar;
|
||||
if (!_.isUndefined(values.avatar)) {
|
||||
const [whereQuery, whereQueryValues] = buildWhereQuery(criteria);
|
||||
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
`SELECT avatar FROM user_account WHERE ${whereQuery} FOR UPDATE`,
|
||||
whereQueryValues,
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
if (queryResult.rowCount === 0) {
|
||||
return { user: null };
|
||||
}
|
||||
|
||||
[{ avatar: prevAvatar }] = queryResult.rows;
|
||||
}
|
||||
|
||||
const user = await User.updateOne(criteria)
|
||||
.set({ ...values })
|
||||
.usingConnection(db);
|
||||
|
||||
let uploadedFile;
|
||||
if (hasAvatarChanged(user.avatar, prevAvatar)) {
|
||||
if (prevAvatar) {
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE uploaded_file SET references_total = CASE WHEN references_total > 1 THEN references_total - 1 END, updated_at = $1 WHERE id = $2 RETURNING *',
|
||||
[new Date().toISOString(), prevAvatar.uploadedFileId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
const [row] = queryResult.rows;
|
||||
|
||||
uploadedFile = {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
mimeType: row.mime_type,
|
||||
size: row.size,
|
||||
referencesTotal: row.references_total,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
if (user.avatar) {
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE uploaded_file SET references_total = references_total + 1, updated_at = $1 WHERE id = $2 AND references_total IS NOT NULL',
|
||||
[new Date().toISOString(), user.avatar.uploadedFileId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
if (queryResult.rowCount === 0) {
|
||||
throw 'uploadedFileNotFound';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { user, uploadedFile };
|
||||
});
|
||||
}
|
||||
|
||||
return User.updateOne(criteria).set({ ...values });
|
||||
const user = await User.updateOne(criteria).set({ ...values });
|
||||
return { user };
|
||||
};
|
||||
|
||||
const deleteOne = (criteria) => User.destroyOne(criteria);
|
||||
const deleteOne = (criteria) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const user = await User.destroyOne(criteria).usingConnection(db);
|
||||
|
||||
let uploadedFile;
|
||||
if (user.avatar) {
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE uploaded_file SET references_total = CASE WHEN references_total > 1 THEN references_total - 1 END, updated_at = $1 WHERE id = $2 RETURNING *',
|
||||
[new Date().toISOString(), user.avatar.uploadedFileId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
const [row] = queryResult.rows;
|
||||
|
||||
uploadedFile = {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
mimeType: row.mime_type,
|
||||
size: row.size,
|
||||
referencesTotal: row.references_total,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
return { user, uploadedFile };
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createOne,
|
||||
|
||||
@@ -16,18 +16,13 @@ module.exports = {
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
|
||||
dirname: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
sizeInBytes: {
|
||||
type: 'string', // TODO: should be number somehow
|
||||
size: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
columnName: 'size_in_bytes',
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
@@ -38,6 +33,11 @@ module.exports = {
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
|
||||
uploadedFileId: {
|
||||
model: 'UploadedFile',
|
||||
required: true,
|
||||
columnName: 'uploaded_file_id',
|
||||
},
|
||||
projectId: {
|
||||
model: 'Project',
|
||||
required: true,
|
||||
|
||||
@@ -4,22 +4,40 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* FileReference.js
|
||||
* StorageUsage.js
|
||||
*
|
||||
* @description :: A model definition represents a database table/collection.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||
*/
|
||||
|
||||
const MAIN_ID = '1';
|
||||
|
||||
module.exports = {
|
||||
MAIN_ID,
|
||||
|
||||
attributes: {
|
||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
|
||||
total: {
|
||||
type: 'number',
|
||||
allowNull: true,
|
||||
defaultsTo: 0,
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
userAvatars: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
columnName: 'user_avatars',
|
||||
},
|
||||
backgroundImages: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
columnName: 'background_images',
|
||||
},
|
||||
attachments: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
columnName: 'attachments',
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
@@ -31,5 +49,5 @@ module.exports = {
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
},
|
||||
|
||||
tableName: 'file_reference',
|
||||
tableName: 'storage_usage',
|
||||
};
|
||||
59
server/api/models/UploadedFile.js
Normal file
59
server/api/models/UploadedFile.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* UploadedFile.js
|
||||
*
|
||||
* @description :: A model definition represents a database table/collection.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||
*/
|
||||
|
||||
const Types = {
|
||||
USER_AVATAR: 'userAvatar',
|
||||
BACKGROUND_IMAGE: 'backgroundImage',
|
||||
ATTACHMENT: 'attachment',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Types,
|
||||
|
||||
attributes: {
|
||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
|
||||
type: {
|
||||
type: 'string',
|
||||
isIn: Object.values(Types),
|
||||
required: true,
|
||||
},
|
||||
referencesTotal: {
|
||||
type: 'number',
|
||||
allowNull: true,
|
||||
defaultsTo: 0,
|
||||
columnName: 'references_total',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
allowNull: true,
|
||||
columnName: 'mime_type',
|
||||
},
|
||||
size: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
// ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
|
||||
|
||||
// ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
|
||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||
},
|
||||
|
||||
tableName: 'uploaded_file',
|
||||
};
|
||||
@@ -9,15 +9,22 @@
|
||||
*/
|
||||
|
||||
const { URL } = require('url');
|
||||
const bytes = require('bytes');
|
||||
const sails = require('sails');
|
||||
|
||||
const version = require('../version');
|
||||
|
||||
const envToNumber = (value) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const number = parseInt(value, 10);
|
||||
return Number.isNaN(number) ? null : number;
|
||||
};
|
||||
|
||||
const envToBytes = (value) => value && bytes(value);
|
||||
|
||||
const envToArray = (value) => (value ? value.split(',') : []);
|
||||
|
||||
const parsedBasedUrl = new URL(process.env.BASE_URL);
|
||||
@@ -35,6 +42,7 @@ module.exports.custom = {
|
||||
baseUrlPath: parsedBasedUrl.pathname,
|
||||
baseUrlSecure: parsedBasedUrl.protocol === 'https:',
|
||||
|
||||
maxUploadFileSize: envToBytes(process.env.MAX_UPLOAD_FILE_SIZE),
|
||||
tokenExpiresIn: (parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365) * 24 * 60 * 60,
|
||||
|
||||
// Location to receive uploaded files in. Default (non-string value) is a Sails-specific location.
|
||||
@@ -51,7 +59,9 @@ module.exports.custom = {
|
||||
process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(),
|
||||
|
||||
internalAccessToken: process.env.INTERNAL_ACCESS_TOKEN,
|
||||
storageLimit: envToBytes(process.env.STORAGE_LIMIT),
|
||||
activeUsersLimit: envToNumber(process.env.ACTIVE_USERS_LIMIT),
|
||||
|
||||
showDetailedAuthErrors: process.env.SHOW_DETAILED_AUTH_ERRORS === 'true',
|
||||
|
||||
s3Endpoint: process.env.S3_ENDPOINT,
|
||||
|
||||
@@ -4,10 +4,10 @@ const AccessTokenSteps = {
|
||||
|
||||
const POSITION_GAP = 65536;
|
||||
|
||||
const MAX_SIZE_IN_BYTES_TO_GET_ENCODING = 8 * 1024 * 1024;
|
||||
const MAX_SIZE_TO_GET_ENCODING = 8 * 1024 * 1024;
|
||||
|
||||
module.exports = {
|
||||
AccessTokenSteps,
|
||||
POSITION_GAP,
|
||||
MAX_SIZE_IN_BYTES_TO_GET_ENCODING,
|
||||
MAX_SIZE_TO_GET_ENCODING,
|
||||
};
|
||||
|
||||
161
server/db/migrations/20250820144730_track_storage_usage.js
Normal file
161
server/db/migrations/20250820144730_track_storage_usage.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const mime = require('mime');
|
||||
|
||||
exports.up = async (knex) => {
|
||||
await knex.schema.createTable('storage_usage', (table) => {
|
||||
/* Columns */
|
||||
|
||||
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
|
||||
|
||||
table.bigInteger('total').notNullable();
|
||||
table.bigInteger('user_avatars').notNullable();
|
||||
table.bigInteger('background_images').notNullable();
|
||||
table.bigInteger('attachments').notNullable();
|
||||
|
||||
table.timestamp('created_at', true);
|
||||
table.timestamp('updated_at', true);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('file_reference', (table) => {
|
||||
table.dropPrimary();
|
||||
table.dropIndex('total');
|
||||
});
|
||||
|
||||
await knex.schema.renameTable('file_reference', 'uploaded_file');
|
||||
|
||||
await knex.schema.alterTable('uploaded_file', (table) => {
|
||||
/* Columns */
|
||||
|
||||
table.text('type').notNullable().defaultTo('attachment');
|
||||
table.text('mime_type');
|
||||
table.bigInteger('size').notNullable().defaultTo(0);
|
||||
|
||||
/* Modifications */
|
||||
|
||||
table.text('id').primary().defaultTo(knex.raw('next_id()')).alter();
|
||||
table.renameColumn('total', 'references_total');
|
||||
|
||||
/* Indexes */
|
||||
|
||||
table.index('type');
|
||||
table.index('references_total');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('uploaded_file', (table) => {
|
||||
table.text('type').notNullable().alter();
|
||||
table.bigInteger('size').notNullable().alter();
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE user_account
|
||||
SET avatar = avatar - 'dirname' - 'sizeInBytes' || jsonb_build_object('uploadedFileId', avatar->'dirname', 'size', avatar->'sizeInBytes')
|
||||
WHERE avatar IS NOT NULL;
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable('background_image', (table) => {
|
||||
table.renameColumn('dirname', 'uploaded_file_id');
|
||||
table.renameColumn('size_in_bytes', 'size');
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE attachment
|
||||
SET data = data - 'fileReferenceId' - 'sizeInBytes' || jsonb_build_object('uploadedFileId', data->'fileReferenceId', 'size', data->'sizeInBytes')
|
||||
WHERE type = 'file';
|
||||
`);
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE uploaded_file
|
||||
SET
|
||||
type = 'attachment',
|
||||
mime_type = attachment.data->>'mimeType',
|
||||
size = (attachment.data->>'size')::bigint
|
||||
FROM attachment
|
||||
WHERE (attachment.data->>'uploadedFileId')::text = uploaded_file.id AND attachment.type = 'file';
|
||||
`);
|
||||
|
||||
const users = await knex('user_account').whereNotNull('avatar');
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
await knex.batchInsert(
|
||||
'uploaded_file',
|
||||
users.map(({ avatar }) => ({
|
||||
createdAt,
|
||||
id: avatar.uploadedFileId,
|
||||
type: 'userAvatar',
|
||||
referencesTotal: 1,
|
||||
mimeType: mime.getType(avatar.extension),
|
||||
size: avatar.size,
|
||||
})),
|
||||
);
|
||||
|
||||
const backgroundImages = await knex('background_image');
|
||||
|
||||
await knex.batchInsert(
|
||||
'uploaded_file',
|
||||
backgroundImages.map((backgroundImage) => ({
|
||||
id: backgroundImage.uploaded_file_id,
|
||||
type: 'backgroundImage',
|
||||
referencesTotal: 1,
|
||||
mimeType: mime.getType(backgroundImage.extension),
|
||||
size: backgroundImage.size,
|
||||
createdAt: backgroundImage.created_at,
|
||||
})),
|
||||
);
|
||||
|
||||
return knex.raw(`
|
||||
INSERT INTO storage_usage (id, total, user_avatars, background_images, attachments, created_at)
|
||||
SELECT
|
||||
1 AS id,
|
||||
COALESCE(SUM(size), 0) AS total,
|
||||
COALESCE(SUM(CASE WHEN type = 'userAvatar' THEN size ELSE 0 END), 0) AS user_avatars,
|
||||
COALESCE(SUM(CASE WHEN type = 'backgroundImage' THEN size ELSE 0 END), 0) AS background_images,
|
||||
COALESCE(SUM(CASE WHEN type = 'attachment' THEN size ELSE 0 END), 0) AS attachments,
|
||||
timezone('UTC', now()) AS created_at
|
||||
FROM uploaded_file;
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = async (knex) => {
|
||||
await knex.schema.dropTable('storage_usage');
|
||||
|
||||
await knex('uploaded_file').delete().whereNot('type', 'attachment');
|
||||
|
||||
await knex.schema.alterTable('uploaded_file', (table) => {
|
||||
table.dropPrimary();
|
||||
table.dropIndex('references_total');
|
||||
});
|
||||
|
||||
await knex.schema.renameTable('uploaded_file', 'file_reference');
|
||||
|
||||
await knex.schema.alterTable('file_reference', (table) => {
|
||||
table.dropColumn('type');
|
||||
table.dropColumn('mime_type');
|
||||
table.dropColumn('size');
|
||||
|
||||
table.bigInteger('id').primary().defaultTo(knex.raw('next_id()')).alter();
|
||||
table.renameColumn('references_total', 'total');
|
||||
|
||||
table.index('total');
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE user_account
|
||||
SET avatar = avatar - 'uploadedFileId' - 'size' || jsonb_build_object('dirname', avatar->'uploadedFileId', 'sizeInBytes', avatar->'size')
|
||||
WHERE avatar IS NOT NULL;
|
||||
`);
|
||||
|
||||
await knex.schema.alterTable('background_image', (table) => {
|
||||
table.renameColumn('uploaded_file_id', 'dirname');
|
||||
table.renameColumn('size', 'size_in_bytes');
|
||||
});
|
||||
|
||||
return knex.raw(`
|
||||
UPDATE attachment
|
||||
SET data = data - 'uploadedFileId' - 'size' || jsonb_build_object('fileReferenceId', data->'uploadedFileId', 'sizeInBytes', data->'size')
|
||||
WHERE type = 'file';
|
||||
`);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ const rc = require('sails/accessible/rc');
|
||||
const _ = require('lodash');
|
||||
|
||||
const knexfile = require('./knexfile');
|
||||
const { MAX_SIZE_IN_BYTES_TO_GET_ENCODING, POSITION_GAP } = require('../constants');
|
||||
const { MAX_SIZE_TO_GET_ENCODING, POSITION_GAP } = require('../constants');
|
||||
|
||||
const PrevActionTypes = {
|
||||
COMMENT_CARD: 'commentCard',
|
||||
@@ -611,7 +611,7 @@ const upgradeUserAvatars = async () => {
|
||||
const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${dirname}`;
|
||||
|
||||
if (user) {
|
||||
const sizeInBytes = await fileManager.getSizeInBytes(
|
||||
const size = await fileManager.getSize(
|
||||
`${dirPathSegment}/original.${user.avatar.extension}`,
|
||||
);
|
||||
|
||||
@@ -619,7 +619,7 @@ const upgradeUserAvatars = async () => {
|
||||
.update({
|
||||
avatar: knex.raw("?? || jsonb_build_object('sizeInBytes', ?::bigint)", [
|
||||
'avatar',
|
||||
sizeInBytes,
|
||||
size,
|
||||
]),
|
||||
})
|
||||
.where('id', user.id);
|
||||
@@ -690,13 +690,13 @@ const upgradeBackgroundImages = async () => {
|
||||
const dirPathSegment = `${sails.config.custom.backgroundImagesPathSegment}/${dirname}`;
|
||||
|
||||
if (backgroundImage) {
|
||||
const sizeInBytes = await fileManager.getSizeInBytes(
|
||||
const size = await fileManager.getSize(
|
||||
`${dirPathSegment}/original.${backgroundImage.extension}`,
|
||||
);
|
||||
|
||||
await knex('background_image')
|
||||
.update({
|
||||
size_in_bytes: sizeInBytes,
|
||||
size_in_bytes: size,
|
||||
})
|
||||
.where('id', backgroundImage.id);
|
||||
} else {
|
||||
@@ -777,12 +777,10 @@ const upgradeFileAttachments = async () => {
|
||||
'id',
|
||||
);
|
||||
|
||||
const sizeInBytes = await fileManager.getSizeInBytes(
|
||||
`${dirPathSegment}/${attachment.data.filename}`,
|
||||
);
|
||||
const size = await fileManager.getSize(`${dirPathSegment}/${attachment.data.filename}`);
|
||||
|
||||
let encoding = null;
|
||||
if (sizeInBytes && sizeInBytes <= MAX_SIZE_IN_BYTES_TO_GET_ENCODING) {
|
||||
if (size && size <= MAX_SIZE_TO_GET_ENCODING) {
|
||||
const readStream = await fileManager.read(
|
||||
`${dirPathSegment}/${attachment.data.filename}`,
|
||||
);
|
||||
@@ -795,7 +793,7 @@ const upgradeFileAttachments = async () => {
|
||||
.update({
|
||||
data: trx.raw(
|
||||
"?? || jsonb_build_object('fileReferenceId', ?::text, 'sizeInBytes', ?::bigint, 'encoding', ?::text)",
|
||||
['data', id, sizeInBytes, encoding],
|
||||
['data', id, size, encoding],
|
||||
),
|
||||
})
|
||||
.where('id', attachment.id);
|
||||
|
||||
1
server/package-lock.json
generated
1
server/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.726.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bytes": "^3.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.6.1",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.726.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bytes": "^3.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.6.1",
|
||||
"dotenv-cli": "^7.4.4",
|
||||
|
||||
13
server/patches/skipper-disk+0.5.12.patch
Normal file
13
server/patches/skipper-disk+0.5.12.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/skipper-disk/standalone/build-progress-stream.js b/node_modules/skipper-disk/standalone/build-progress-stream.js
|
||||
index ed048dc..3cca22e 100644
|
||||
--- a/node_modules/skipper-disk/standalone/build-progress-stream.js
|
||||
+++ b/node_modules/skipper-disk/standalone/build-progress-stream.js
|
||||
@@ -110,7 +110,7 @@ module.exports = function buildProgressStream (options, __newFile, receiver__, o
|
||||
receiver__.emit('progress', currentFileProgress);
|
||||
|
||||
// and then enforce its `maxBytes`.
|
||||
- if (options.maxBytes && totalBytesWritten >= options.maxBytes) {
|
||||
+ if (!_.isNull(options.maxBytes) && totalBytesWritten >= options.maxBytes) {
|
||||
|
||||
var err = new Error();
|
||||
err.code = 'E_EXCEEDS_UPLOAD_LIMIT';
|
||||
Reference in New Issue
Block a user