feat: Track storage usage
This commit is contained in:
@@ -80,7 +80,7 @@ const Item = React.memo(({ id, isVisible }) => {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (attachment.data.encoding === Encodings.UTF8) {
|
if (attachment.data.encoding === Encodings.UTF8) {
|
||||||
if (attachment.data.sizeInBytes <= Config.MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT) {
|
if (attachment.data.size <= Config.MAX_SIZE_TO_DISPLAY_CONTENT) {
|
||||||
content = (
|
content = (
|
||||||
<ContentViewer
|
<ContentViewer
|
||||||
src={attachment.data.url}
|
src={attachment.data.url}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*!
|
||||||
|
* Copyright (c) 2024 PLANKA Software GmbH
|
||||||
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Icon, Message } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
const FileIsTooBig = React.memo(() => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Message visible negative size="tiny">
|
||||||
|
<Icon name="file" />
|
||||||
|
{t('common.uploadFailedFileIsTooBig')}
|
||||||
|
</Message>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FileIsTooBig;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*!
|
||||||
|
* Copyright (c) 2024 PLANKA Software GmbH
|
||||||
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Icon, Message } from 'semantic-ui-react';
|
||||||
|
|
||||||
|
const NotEnoughStorage = React.memo(() => {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Message visible negative size="tiny">
|
||||||
|
<Icon name="hdd" />
|
||||||
|
{t('common.uploadFailedNotEnoughStorageSpace')}
|
||||||
|
</Message>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default NotEnoughStorage;
|
||||||
@@ -7,9 +7,13 @@ import React from 'react';
|
|||||||
import { Toaster as HotToaster, ToastBar as HotToastBar } from 'react-hot-toast';
|
import { Toaster as HotToaster, ToastBar as HotToastBar } from 'react-hot-toast';
|
||||||
|
|
||||||
import ToastTypes from '../../../constants/ToastTypes';
|
import ToastTypes from '../../../constants/ToastTypes';
|
||||||
|
import FileIsTooBig from './FileIsTooBig';
|
||||||
|
import NotEnoughStorage from './NotEnoughStorage';
|
||||||
import EmptyTrashToast from './EmptyTrashToast';
|
import EmptyTrashToast from './EmptyTrashToast';
|
||||||
|
|
||||||
const TOAST_BY_TYPE = {
|
const TOAST_BY_TYPE = {
|
||||||
|
[ToastTypes.FILE_IS_TOO_BIG]: FileIsTooBig,
|
||||||
|
[ToastTypes.NOT_ENOUGH_STORAGE]: NotEnoughStorage,
|
||||||
[ToastTypes.EMPTY_TRASH]: EmptyTrashToast,
|
[ToastTypes.EMPTY_TRASH]: EmptyTrashToast,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const CARDS_LIMIT = 50;
|
|||||||
const COMMENTS_LIMIT = 50;
|
const COMMENTS_LIMIT = 50;
|
||||||
const ACTIVITIES_LIMIT = 50;
|
const ACTIVITIES_LIMIT = 50;
|
||||||
|
|
||||||
const MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT = 256 * 1024;
|
const MAX_SIZE_TO_DISPLAY_CONTENT = 256 * 1024;
|
||||||
|
|
||||||
const IS_MAC = navigator.platform.startsWith('Mac');
|
const IS_MAC = navigator.platform.startsWith('Mac');
|
||||||
|
|
||||||
@@ -28,6 +28,6 @@ export default {
|
|||||||
CARDS_LIMIT,
|
CARDS_LIMIT,
|
||||||
COMMENTS_LIMIT,
|
COMMENTS_LIMIT,
|
||||||
ACTIVITIES_LIMIT,
|
ACTIVITIES_LIMIT,
|
||||||
MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT,
|
MAX_SIZE_TO_DISPLAY_CONTENT,
|
||||||
IS_MAC,
|
IS_MAC,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const FILE_IS_TOO_BIG = 'FILE_IS_TOO_BIG';
|
||||||
|
const NOT_ENOUGH_STORAGE = 'NOT_ENOUGH_STORAGE';
|
||||||
const EMPTY_TRASH = 'EMPTY_TRASH';
|
const EMPTY_TRASH = 'EMPTY_TRASH';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
FILE_IS_TOO_BIG,
|
||||||
|
NOT_ENOUGH_STORAGE,
|
||||||
EMPTY_TRASH,
|
EMPTY_TRASH,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -280,6 +280,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'إجراءات المستخدم',
|
userActions_title: 'إجراءات المستخدم',
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Потребителски действия',
|
userActions_title: 'Потребителски действия',
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Zadejte název pro potvrzení.',
|
typeNameToConfirm: 'Zadejte název pro potvrzení.',
|
||||||
typeTitleToConfirm: 'Zadejte titulek pro potvrzení.',
|
typeTitleToConfirm: 'Zadejte titulek pro potvrzení.',
|
||||||
unsavedChanges: 'Neuložené změny',
|
unsavedChanges: 'Neuložené změny',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Nahrané obrázky',
|
uploadedImages: 'Nahrané obrázky',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Akce uživatele',
|
userActions_title: 'Akce uživatele',
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Skriv navnet for at bekræfte.',
|
typeNameToConfirm: 'Skriv navnet for at bekræfte.',
|
||||||
typeTitleToConfirm: 'Skriv overskriften for at bekræfte.',
|
typeTitleToConfirm: 'Skriv overskriften for at bekræfte.',
|
||||||
unsavedChanges: 'Ikke-gemte ændringer',
|
unsavedChanges: 'Ikke-gemte ændringer',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Uploadede billeder',
|
uploadedImages: 'Uploadede billeder',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Brugerhandlinger',
|
userActions_title: 'Brugerhandlinger',
|
||||||
|
|||||||
@@ -309,6 +309,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Namen zur Bestätigung eingeben.',
|
typeNameToConfirm: 'Namen zur Bestätigung eingeben.',
|
||||||
typeTitleToConfirm: 'Titel zur Bestätigung eingeben.',
|
typeTitleToConfirm: 'Titel zur Bestätigung eingeben.',
|
||||||
unsavedChanges: 'Ungespeicherte Änderungen',
|
unsavedChanges: 'Ungespeicherte Änderungen',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Hochgeladene Bilder',
|
uploadedImages: 'Hochgeladene Bilder',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Benutzeraktionen',
|
userActions_title: 'Benutzeraktionen',
|
||||||
|
|||||||
@@ -311,6 +311,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Πληκτρολογήστε το όνομα για επιβεβαίωση.',
|
typeNameToConfirm: 'Πληκτρολογήστε το όνομα για επιβεβαίωση.',
|
||||||
typeTitleToConfirm: 'Πληκτρολογήστε τον τίτλο για επιβεβαίωση.',
|
typeTitleToConfirm: 'Πληκτρολογήστε τον τίτλο για επιβεβαίωση.',
|
||||||
unsavedChanges: 'Μη αποθηκευμένες αλλαγές',
|
unsavedChanges: 'Μη αποθηκευμένες αλλαγές',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Μεταφορτωμένες εικόνες',
|
uploadedImages: 'Μεταφορτωμένες εικόνες',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Ενέργειες χρήστη',
|
userActions_title: 'Ενέργειες χρήστη',
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Type the name to confirm.',
|
typeNameToConfirm: 'Type the name to confirm.',
|
||||||
typeTitleToConfirm: 'Type the title to confirm.',
|
typeTitleToConfirm: 'Type the title to confirm.',
|
||||||
unsavedChanges: 'Unsaved changes',
|
unsavedChanges: 'Unsaved changes',
|
||||||
|
uploadFailedFileIsTooBig: 'Upload failed: File is too big.',
|
||||||
|
uploadFailedNotEnoughStorageSpace: 'Upload failed: Not enough storage space.',
|
||||||
uploadedImages: 'Uploaded images',
|
uploadedImages: 'Uploaded images',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
userActions_title: 'User Actions',
|
userActions_title: 'User Actions',
|
||||||
|
|||||||
@@ -296,6 +296,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Type the name to confirm.',
|
typeNameToConfirm: 'Type the name to confirm.',
|
||||||
typeTitleToConfirm: 'Type the title to confirm.',
|
typeTitleToConfirm: 'Type the title to confirm.',
|
||||||
unsavedChanges: 'Unsaved changes',
|
unsavedChanges: 'Unsaved changes',
|
||||||
|
uploadFailedFileIsTooBig: 'Upload failed: File is too big.',
|
||||||
|
uploadFailedNotEnoughStorageSpace: 'Upload failed: Not enough storage space.',
|
||||||
uploadedImages: 'Uploaded images',
|
uploadedImages: 'Uploaded images',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
userActions_title: 'User Actions',
|
userActions_title: 'User Actions',
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Escribe el nombre para confirmar',
|
typeNameToConfirm: 'Escribe el nombre para confirmar',
|
||||||
typeTitleToConfirm: 'Escribe el título para confirmar',
|
typeTitleToConfirm: 'Escribe el título para confirmar',
|
||||||
unsavedChanges: 'Cambios no guardados',
|
unsavedChanges: 'Cambios no guardados',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Imágenes subidas',
|
uploadedImages: 'Imágenes subidas',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Acciones de Usuario',
|
userActions_title: 'Acciones de Usuario',
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Sisestage nimi, et kinnitada.',
|
typeNameToConfirm: 'Sisestage nimi, et kinnitada.',
|
||||||
typeTitleToConfirm: 'Sisestage pealkiri, et kinnitada.',
|
typeTitleToConfirm: 'Sisestage pealkiri, et kinnitada.',
|
||||||
unsavedChanges: 'Muudetud andmed',
|
unsavedChanges: 'Muudetud andmed',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Laaditud pildid',
|
uploadedImages: 'Laaditud pildid',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Kasutaja tegevused',
|
userActions_title: 'Kasutaja tegevused',
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'اقدامات کاربر',
|
userActions_title: 'اقدامات کاربر',
|
||||||
|
|||||||
@@ -295,6 +295,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Kirjoita nimi vahvistaaksesi.',
|
typeNameToConfirm: 'Kirjoita nimi vahvistaaksesi.',
|
||||||
typeTitleToConfirm: 'Kirjoita otsikko vahvistaaksesi.',
|
typeTitleToConfirm: 'Kirjoita otsikko vahvistaaksesi.',
|
||||||
unsavedChanges: 'Tallentamattomat muutokset',
|
unsavedChanges: 'Tallentamattomat muutokset',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Ladatut kuvat',
|
uploadedImages: 'Ladatut kuvat',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Käyttäjän toiminnot',
|
userActions_title: 'Käyttäjän toiminnot',
|
||||||
|
|||||||
@@ -304,6 +304,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Saissir le nom pour confirmer.',
|
typeNameToConfirm: 'Saissir le nom pour confirmer.',
|
||||||
typeTitleToConfirm: 'Saisir le titre pour confirmer.',
|
typeTitleToConfirm: 'Saisir le titre pour confirmer.',
|
||||||
unsavedChanges: 'Modifications non enregistrées',
|
unsavedChanges: 'Modifications non enregistrées',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Images téléchargées',
|
uploadedImages: 'Images téléchargées',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
userActions_title: "Actions de l'utilisateur",
|
userActions_title: "Actions de l'utilisateur",
|
||||||
|
|||||||
@@ -293,6 +293,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Írja be a nevet a megerősítéshez.',
|
typeNameToConfirm: 'Írja be a nevet a megerősítéshez.',
|
||||||
typeTitleToConfirm: 'Írja be a címet a megerősítéshez.',
|
typeTitleToConfirm: 'Írja be a címet a megerősítéshez.',
|
||||||
unsavedChanges: 'Mentetlen változtatások',
|
unsavedChanges: 'Mentetlen változtatások',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Feltöltött képek',
|
uploadedImages: 'Feltöltött képek',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
userActions_title: 'Felhasználói műveletek',
|
userActions_title: 'Felhasználói műveletek',
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Aksi Pengguna',
|
userActions_title: 'Aksi Pengguna',
|
||||||
|
|||||||
@@ -302,6 +302,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Digita il nome per confermare',
|
typeNameToConfirm: 'Digita il nome per confermare',
|
||||||
typeTitleToConfirm: 'Digita il titolo per confermare',
|
typeTitleToConfirm: 'Digita il titolo per confermare',
|
||||||
unsavedChanges: 'Modifiche non salvate',
|
unsavedChanges: 'Modifiche non salvate',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Immagini caricate',
|
uploadedImages: 'Immagini caricate',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
userActions_title: 'Azioni utente',
|
userActions_title: 'Azioni utente',
|
||||||
|
|||||||
@@ -283,6 +283,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'ユーザーのアクション',
|
userActions_title: 'ユーザーのアクション',
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: '사용자 작업',
|
userActions_title: '사용자 작업',
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Gebruikersacties',
|
userActions_title: 'Gebruikersacties',
|
||||||
|
|||||||
@@ -291,6 +291,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Wpisz nazwę aby potwierdzić.',
|
typeNameToConfirm: 'Wpisz nazwę aby potwierdzić.',
|
||||||
typeTitleToConfirm: 'Wpisz tytuł aby potwierdzić.',
|
typeTitleToConfirm: 'Wpisz tytuł aby potwierdzić.',
|
||||||
unsavedChanges: 'Niezapisane zmiany',
|
unsavedChanges: 'Niezapisane zmiany',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Wgrane obrazy',
|
uploadedImages: 'Wgrane obrazy',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Akcje Użytkownika',
|
userActions_title: 'Akcje Użytkownika',
|
||||||
|
|||||||
@@ -303,6 +303,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Digite o nome para confirmar.',
|
typeNameToConfirm: 'Digite o nome para confirmar.',
|
||||||
typeTitleToConfirm: 'Digite o título para confirmar.',
|
typeTitleToConfirm: 'Digite o título para confirmar.',
|
||||||
unsavedChanges: 'Alterações não salvas',
|
unsavedChanges: 'Alterações não salvas',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Imagens enviadas',
|
uploadedImages: 'Imagens enviadas',
|
||||||
url: 'URL',
|
url: 'URL',
|
||||||
userActions_title: 'Ações do Usuário',
|
userActions_title: 'Ações do Usuário',
|
||||||
|
|||||||
@@ -285,6 +285,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Ações do Utilizador',
|
userActions_title: 'Ações do Utilizador',
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Acțiunile utilizatorului',
|
userActions_title: 'Acțiunile utilizatorului',
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ export default {
|
|||||||
typeNameToConfirm: 'Введите имя для подтверждения',
|
typeNameToConfirm: 'Введите имя для подтверждения',
|
||||||
typeTitleToConfirm: 'Введите название для подтверждения',
|
typeTitleToConfirm: 'Введите название для подтверждения',
|
||||||
unsavedChanges: 'Несохранённые изменения',
|
unsavedChanges: 'Несохранённые изменения',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Загруженные изображения',
|
uploadedImages: 'Загруженные изображения',
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Действия с пользователем',
|
userActions_title: 'Действия с пользователем',
|
||||||
|
|||||||
@@ -283,6 +283,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Akcie na používateľovi',
|
userActions_title: 'Akcie na používateľovi',
|
||||||
|
|||||||
@@ -283,6 +283,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Корисничке радње',
|
userActions_title: 'Корисничке радње',
|
||||||
|
|||||||
@@ -280,6 +280,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Korisničke radnje',
|
userActions_title: 'Korisničke radnje',
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Användaråtgärder',
|
userActions_title: 'Användaråtgärder',
|
||||||
|
|||||||
@@ -280,6 +280,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Kullanıcı İşlemleri',
|
userActions_title: 'Kullanıcı İşlemleri',
|
||||||
|
|||||||
@@ -297,6 +297,8 @@ export default {
|
|||||||
typeNameToConfirm: "Введіть ім'я для підтвердження.",
|
typeNameToConfirm: "Введіть ім'я для підтвердження.",
|
||||||
typeTitleToConfirm: 'Введіть назву, щоб підтвердити.',
|
typeTitleToConfirm: 'Введіть назву, щоб підтвердити.',
|
||||||
unsavedChanges: 'Незбережені зміни',
|
unsavedChanges: 'Незбережені зміни',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: 'Завантажені зображення',
|
uploadedImages: 'Завантажені зображення',
|
||||||
url: 'Посилання',
|
url: 'Посилання',
|
||||||
userActions_title: 'Дії користувача',
|
userActions_title: 'Дії користувача',
|
||||||
|
|||||||
@@ -279,6 +279,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: 'Foydalanuvchi Amallari',
|
userActions_title: 'Foydalanuvchi Amallari',
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ export default {
|
|||||||
typeNameToConfirm: '输入名称以确认',
|
typeNameToConfirm: '输入名称以确认',
|
||||||
typeTitleToConfirm: '输入标题以确认',
|
typeTitleToConfirm: '输入标题以确认',
|
||||||
unsavedChanges: '未保存的更改',
|
unsavedChanges: '未保存的更改',
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: '已上传图片',
|
uploadedImages: '已上传图片',
|
||||||
url: '网址',
|
url: '网址',
|
||||||
userActions_title: '用户操作',
|
userActions_title: '用户操作',
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ export default {
|
|||||||
typeNameToConfirm: null,
|
typeNameToConfirm: null,
|
||||||
typeTitleToConfirm: null,
|
typeTitleToConfirm: null,
|
||||||
unsavedChanges: null,
|
unsavedChanges: null,
|
||||||
|
uploadFailedFileIsTooBig: null,
|
||||||
|
uploadFailedNotEnoughStorageSpace: null,
|
||||||
uploadedImages: null,
|
uploadedImages: null,
|
||||||
url: null,
|
url: null,
|
||||||
userActions_title: '使用者操作',
|
userActions_title: '使用者操作',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
import truncate from 'lodash/truncate';
|
import truncate from 'lodash/truncate';
|
||||||
import { call, put, select } from 'redux-saga/effects';
|
import { call, put, select } from 'redux-saga/effects';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
import request from '../request';
|
import request from '../request';
|
||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
@@ -13,6 +14,7 @@ import actions from '../../../actions';
|
|||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import { createLocalId } from '../../../utils/local-id';
|
import { createLocalId } from '../../../utils/local-id';
|
||||||
import { AttachmentTypes } from '../../../constants/Enums';
|
import { AttachmentTypes } from '../../../constants/Enums';
|
||||||
|
import ToastTypes from '../../../constants/ToastTypes';
|
||||||
|
|
||||||
export function* createAttachment(cardId, data) {
|
export function* createAttachment(cardId, data) {
|
||||||
const localId = yield call(createLocalId);
|
const localId = yield call(createLocalId);
|
||||||
@@ -41,6 +43,22 @@ export function* createAttachment(cardId, data) {
|
|||||||
: call(request, api.createAttachment, cardId, nextData));
|
: call(request, api.createAttachment, cardId, nextData));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yield put(actions.createAttachment.failure(localId, error));
|
yield put(actions.createAttachment.failure(localId, error));
|
||||||
|
|
||||||
|
if (error.code === 'E_UNPROCESSABLE_ENTITY') {
|
||||||
|
let toastType;
|
||||||
|
if (error.message.startsWith('Upload limit')) {
|
||||||
|
toastType = ToastTypes.FILE_IS_TOO_BIG;
|
||||||
|
} else if (error.message === 'Storage limit reached') {
|
||||||
|
toastType = ToastTypes.NOT_ENOUGH_STORAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastType) {
|
||||||
|
yield call(toast, {
|
||||||
|
type: toastType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ services:
|
|||||||
# - LOG_LEVEL=warn
|
# - LOG_LEVEL=warn
|
||||||
|
|
||||||
# - TRUST_PROXY=true
|
# - TRUST_PROXY=true
|
||||||
|
# - MAX_UPLOAD_FILE_SIZE=
|
||||||
# - TOKEN_EXPIRES_IN=365 # In days
|
# - TOKEN_EXPIRES_IN=365 # In days
|
||||||
|
|
||||||
# related: https://github.com/knex/knex/issues/2354
|
# related: https://github.com/knex/knex/issues/2354
|
||||||
@@ -39,6 +40,7 @@ services:
|
|||||||
# - DEFAULT_ADMIN_USERNAME=demo
|
# - DEFAULT_ADMIN_USERNAME=demo
|
||||||
|
|
||||||
# - INTERNAL_ACCESS_TOKEN=
|
# - INTERNAL_ACCESS_TOKEN=
|
||||||
|
# - STORAGE_LIMIT=
|
||||||
# - ACTIVE_USERS_LIMIT=
|
# - ACTIVE_USERS_LIMIT=
|
||||||
|
|
||||||
# Set to true to show more detailed authentication error messages.
|
# Set to true to show more detailed authentication error messages.
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ services:
|
|||||||
# - LOG_LEVEL=warn
|
# - LOG_LEVEL=warn
|
||||||
|
|
||||||
# - TRUST_PROXY=true
|
# - TRUST_PROXY=true
|
||||||
|
# - MAX_UPLOAD_FILE_SIZE=
|
||||||
# - TOKEN_EXPIRES_IN=365 # In days
|
# - TOKEN_EXPIRES_IN=365 # In days
|
||||||
|
|
||||||
# related: https://github.com/knex/knex/issues/2354
|
# related: https://github.com/knex/knex/issues/2354
|
||||||
@@ -53,6 +54,7 @@ services:
|
|||||||
# - DEFAULT_ADMIN_USERNAME=demo
|
# - DEFAULT_ADMIN_USERNAME=demo
|
||||||
|
|
||||||
# - INTERNAL_ACCESS_TOKEN=
|
# - INTERNAL_ACCESS_TOKEN=
|
||||||
|
# - STORAGE_LIMIT=
|
||||||
# - ACTIVE_USERS_LIMIT=
|
# - ACTIVE_USERS_LIMIT=
|
||||||
|
|
||||||
# Set to true to show more detailed authentication error messages.
|
# Set to true to show more detailed authentication error messages.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ SECRET_KEY=notsecretkey
|
|||||||
# LOG_FILE=
|
# LOG_FILE=
|
||||||
|
|
||||||
# TRUST_PROXY=true
|
# TRUST_PROXY=true
|
||||||
|
# MAX_UPLOAD_FILE_SIZE=
|
||||||
# TOKEN_EXPIRES_IN=365 # In days
|
# TOKEN_EXPIRES_IN=365 # In days
|
||||||
|
|
||||||
# related: https://github.com/knex/knex/issues/2354
|
# related: https://github.com/knex/knex/issues/2354
|
||||||
@@ -30,6 +31,7 @@ SECRET_KEY=notsecretkey
|
|||||||
# DEFAULT_ADMIN_USERNAME=demo
|
# DEFAULT_ADMIN_USERNAME=demo
|
||||||
|
|
||||||
# INTERNAL_ACCESS_TOKEN=
|
# INTERNAL_ACCESS_TOKEN=
|
||||||
|
# STORAGE_LIMIT=
|
||||||
# ACTIVE_USERS_LIMIT=
|
# ACTIVE_USERS_LIMIT=
|
||||||
|
|
||||||
# Set to true to show more detailed authentication error messages.
|
# Set to true to show more detailed authentication error messages.
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ module.exports = {
|
|||||||
throw Errors.INVALID_SIGNATURE;
|
throw Errors.INVALID_SIGNATURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
user = await User.qm.updateOne(user.id, {
|
({ user } = await User.qm.updateOne(user.id, {
|
||||||
termsSignature,
|
termsSignature,
|
||||||
termsAcceptedAt: new Date().toISOString(),
|
termsAcceptedAt: new Date().toISOString(),
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await Config.qm.getOneMain();
|
const config = await Config.qm.getOneMain();
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ module.exports = {
|
|||||||
if (inputs.type === Attachment.Types.FILE) {
|
if (inputs.type === Attachment.Types.FILE) {
|
||||||
let files;
|
let files;
|
||||||
try {
|
try {
|
||||||
files = await sails.helpers.utils.receiveFile('file', this.req);
|
files = await sails.helpers.utils.receiveFile(this.req.file('file'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return exits.uploadError(error.message); // TODO: add error
|
return exits.uploadError(error.message); // TODO: add error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ module.exports = {
|
|||||||
|
|
||||||
let files;
|
let files;
|
||||||
try {
|
try {
|
||||||
files = await sails.helpers.utils.receiveFile('file', this.req);
|
files = await sails.helpers.utils.receiveFile(this.req.file('file'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return exits.uploadError(error.message); // TODO: add 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 { currentUser } = this.req;
|
||||||
|
|
||||||
const project = await Project.qm.getOneById(inputs.projectId);
|
const project = await Project.qm.getOneById(inputs.projectId);
|
||||||
@@ -78,7 +78,7 @@ module.exports = {
|
|||||||
if (inputs.importType) {
|
if (inputs.importType) {
|
||||||
let files;
|
let files;
|
||||||
try {
|
try {
|
||||||
files = await sails.helpers.utils.receiveFile('importFile', this.req);
|
files = await sails.helpers.utils.receiveFile(this.req.file('importFile'), false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return exits.uploadError(error.message); // TODO: add error
|
return exits.uploadError(error.message); // TODO: add error
|
||||||
}
|
}
|
||||||
@@ -114,11 +114,11 @@ module.exports = {
|
|||||||
request: this.req,
|
request: this.req,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return exits.success({
|
||||||
item: board,
|
item: board,
|
||||||
included: {
|
included: {
|
||||||
boardMemberships: [boardMembership],
|
boardMemberships: [boardMembership],
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ module.exports = {
|
|||||||
let readStream;
|
let readStream;
|
||||||
try {
|
try {
|
||||||
readStream = await fileManager.read(
|
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) {
|
} catch (error) {
|
||||||
throw Errors.FILE_ATTACHMENT_NOT_FOUND;
|
throw Errors.FILE_ATTACHMENT_NOT_FOUND;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ module.exports = {
|
|||||||
let readStream;
|
let readStream;
|
||||||
try {
|
try {
|
||||||
readStream = await fileManager.read(
|
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) {
|
} catch (error) {
|
||||||
throw Errors.FILE_ATTACHMENT_NOT_FOUND;
|
throw Errors.FILE_ATTACHMENT_NOT_FOUND;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ module.exports = {
|
|||||||
|
|
||||||
let files;
|
let files;
|
||||||
try {
|
try {
|
||||||
files = await sails.helpers.utils.receiveFile('file', this.req);
|
files = await sails.helpers.utils.receiveFile(this.req.file('file'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return exits.uploadError(error.message); // TODO: add 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, {
|
const { attachment, uploadedFile } = await Attachment.qm.deleteOne(inputs.record.id);
|
||||||
isFile: inputs.record.type === Attachment.Types.FILE,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
if (fileReference) {
|
if (uploadedFile) {
|
||||||
sails.helpers.attachments.removeUnreferencedFiles(fileReference);
|
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
sails.sockets.broadcast(
|
sails.sockets.broadcast(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ module.exports = {
|
|||||||
...inputs.record,
|
...inputs.record,
|
||||||
data: {
|
data: {
|
||||||
..._.omit(inputs.record.data, [
|
..._.omit(inputs.record.data, [
|
||||||
'fileReferenceId',
|
'uploadedFileId',
|
||||||
'filename',
|
'filename',
|
||||||
'image.thumbnailsExtension',
|
'image.thumbnailsExtension',
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const mime = require('mime');
|
|||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
|
|
||||||
const filenamify = require('../../../utils/filenamify');
|
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 = {
|
module.exports = {
|
||||||
inputs: {
|
inputs: {
|
||||||
@@ -23,17 +23,22 @@ module.exports = {
|
|||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
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 filename = filenamify(inputs.file.filename);
|
||||||
|
|
||||||
const mimeType = mime.getType(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 buffer;
|
||||||
let encoding = null;
|
let encoding = null;
|
||||||
|
|
||||||
if (sizeInBytes <= MAX_SIZE_IN_BYTES_TO_GET_ENCODING) {
|
if (size <= MAX_SIZE_TO_GET_ENCODING) {
|
||||||
try {
|
try {
|
||||||
buffer = await fsPromises.readFile(inputs.file.fd);
|
buffer = await fsPromises.readFile(inputs.file.fd);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -52,10 +57,10 @@ module.exports = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
fileReferenceId,
|
uploadedFileId,
|
||||||
filename,
|
filename,
|
||||||
mimeType,
|
mimeType,
|
||||||
sizeInBytes,
|
size,
|
||||||
encoding,
|
encoding,
|
||||||
image: null,
|
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) {
|
if (backgroundImage) {
|
||||||
sails.helpers.backgroundImages.removeRelatedFiles(backgroundImage);
|
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFile);
|
||||||
|
|
||||||
const projectRelatedUserIds = await scoper.getProjectRelatedUserIds();
|
const projectRelatedUserIds = await scoper.getProjectRelatedUserIds();
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ module.exports = {
|
|||||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
..._.omit(inputs.record, ['dirname', 'extension']),
|
..._.omit(inputs.record, ['uploadedFileId', 'extension']),
|
||||||
url: `${fileManager.buildUrl(`${sails.config.custom.backgroundImagesPathSegment}/${inputs.record.dirname}/original.${inputs.record.extension}`)}`,
|
url: `${fileManager.buildUrl(`${sails.config.custom.backgroundImagesPathSegment}/${inputs.record.uploadedFileId}/original.${inputs.record.extension}`)}`,
|
||||||
thumbnailUrls: {
|
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
|
* 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 { rimraf } = require('rimraf');
|
||||||
const mime = require('mime');
|
const mime = require('mime');
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -32,8 +32,16 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let metadata;
|
let metadata;
|
||||||
|
let originalBuffer;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
metadata = await image.metadata();
|
metadata = await image.metadata();
|
||||||
|
|
||||||
|
if (metadata.orientation && metadata.orientation > 4) {
|
||||||
|
image = image.rotate();
|
||||||
|
}
|
||||||
|
|
||||||
|
originalBuffer = await image.toBuffer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await rimraf(inputs.file.fd);
|
await rimraf(inputs.file.fd);
|
||||||
throw 'fileIsNotImage';
|
throw 'fileIsNotImage';
|
||||||
@@ -41,20 +49,19 @@ module.exports = {
|
|||||||
|
|
||||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
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 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 {
|
try {
|
||||||
const originalBuffer = await image.toBuffer();
|
|
||||||
sizeInBytes = originalBuffer.length;
|
|
||||||
|
|
||||||
await fileManager.save(
|
await fileManager.save(
|
||||||
`${dirPathSegment}/original.${extension}`,
|
`${dirPathSegment}/original.${extension}`,
|
||||||
originalBuffer,
|
originalBuffer,
|
||||||
@@ -82,6 +89,7 @@ module.exports = {
|
|||||||
|
|
||||||
await fileManager.deleteDir(dirPathSegment);
|
await fileManager.deleteDir(dirPathSegment);
|
||||||
await rimraf(inputs.file.fd);
|
await rimraf(inputs.file.fd);
|
||||||
|
await UploadedFile.qm.deleteOne(uploadedFileId);
|
||||||
|
|
||||||
throw 'fileIsNotImage';
|
throw 'fileIsNotImage';
|
||||||
}
|
}
|
||||||
@@ -89,9 +97,9 @@ module.exports = {
|
|||||||
await rimraf(inputs.file.fd);
|
await rimraf(inputs.file.fd);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dirname,
|
uploadedFileId,
|
||||||
extension,
|
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,
|
cardId: cardIdOrIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
sails.helpers.attachments.removeUnreferencedFiles(fileReferences);
|
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFiles);
|
||||||
|
|
||||||
const customFieldGroups = await CustomFieldGroup.qm.delete({
|
const customFieldGroups = await CustomFieldGroup.qm.delete({
|
||||||
cardId: cardIdOrIds,
|
cardId: cardIdOrIds,
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ module.exports = {
|
|||||||
projectId: projectIdOrIds,
|
projectId: projectIdOrIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
const backgroundImages = await BackgroundImage.qm.delete({
|
const { uploadedFiles } = await BackgroundImage.qm.delete({
|
||||||
projectId: projectIdOrIds,
|
projectId: projectIdOrIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
sails.helpers.backgroundImages.removeRelatedFiles(backgroundImages);
|
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFiles);
|
||||||
|
|
||||||
const baseCustomFieldGroups = await BaseCustomFieldGroup.qm.delete({
|
const baseCustomFieldGroups = await BaseCustomFieldGroup.qm.delete({
|
||||||
projectId: projectIdOrIds,
|
projectId: projectIdOrIds,
|
||||||
|
|||||||
@@ -23,10 +23,12 @@ module.exports = {
|
|||||||
inputs.record,
|
inputs.record,
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = await User.qm.deleteOne(inputs.record.id);
|
const { user, uploadedFile } = await User.qm.deleteOne(inputs.record.id);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
sails.helpers.users.removeRelatedFiles(user);
|
if (uploadedFile) {
|
||||||
|
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFile);
|
||||||
|
}
|
||||||
|
|
||||||
const scoper = sails.helpers.users.makeScoper(user);
|
const scoper = sails.helpers.users.makeScoper(user);
|
||||||
scoper.boardMemberships = boardMemberships;
|
scoper.boardMemberships = boardMemberships;
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ module.exports = {
|
|||||||
'termsAcceptedAt',
|
'termsAcceptedAt',
|
||||||
]),
|
]),
|
||||||
avatar: inputs.record.avatar && {
|
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: {
|
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),
|
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
|
* 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 { rimraf } = require('rimraf');
|
||||||
const mime = require('mime');
|
const mime = require('mime');
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -32,8 +32,16 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let metadata;
|
let metadata;
|
||||||
|
let originalBuffer;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
metadata = await image.metadata();
|
metadata = await image.metadata();
|
||||||
|
|
||||||
|
if (metadata.orientation && metadata.orientation > 4) {
|
||||||
|
image = image.rotate();
|
||||||
|
}
|
||||||
|
|
||||||
|
originalBuffer = await image.toBuffer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await rimraf(inputs.file.fd);
|
await rimraf(inputs.file.fd);
|
||||||
throw 'fileIsNotImage';
|
throw 'fileIsNotImage';
|
||||||
@@ -41,20 +49,19 @@ module.exports = {
|
|||||||
|
|
||||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
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 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 {
|
try {
|
||||||
const originalBuffer = await image.toBuffer();
|
|
||||||
sizeInBytes = originalBuffer.length;
|
|
||||||
|
|
||||||
await fileManager.save(
|
await fileManager.save(
|
||||||
`${dirPathSegment}/original.${extension}`,
|
`${dirPathSegment}/original.${extension}`,
|
||||||
originalBuffer,
|
originalBuffer,
|
||||||
@@ -81,6 +88,7 @@ module.exports = {
|
|||||||
|
|
||||||
await fileManager.deleteDir(dirPathSegment);
|
await fileManager.deleteDir(dirPathSegment);
|
||||||
await rimraf(inputs.file.fd);
|
await rimraf(inputs.file.fd);
|
||||||
|
await UploadedFile.qm.deleteOne(uploadedFileId);
|
||||||
|
|
||||||
throw 'fileIsNotImage';
|
throw 'fileIsNotImage';
|
||||||
}
|
}
|
||||||
@@ -88,9 +96,9 @@ module.exports = {
|
|||||||
await rimraf(inputs.file.fd);
|
await rimraf(inputs.file.fd);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dirname,
|
uploadedFileId,
|
||||||
extension,
|
extension,
|
||||||
sizeInBytes,
|
size,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -69,8 +69,10 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let user;
|
let user;
|
||||||
|
let uploadedFile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
user = await User.qm.updateOne(inputs.record.id, values);
|
({ user, uploadedFile } = await User.qm.updateOne(inputs.record.id, values));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'E_UNIQUE') {
|
if (error.code === 'E_UNIQUE') {
|
||||||
throw 'emailAlreadyInUse';
|
throw 'emailAlreadyInUse';
|
||||||
@@ -91,10 +93,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (inputs.record.avatar) {
|
if (uploadedFile) {
|
||||||
if (!user.avatar || user.avatar.dirname !== inputs.record.avatar.dirname) {
|
sails.helpers.utils.removeUnreferencedUploadedFiles(uploadedFile);
|
||||||
sails.helpers.users.removeRelatedFiles(inputs.record);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isUndefined(values.password) || isDeactivatedChangeToTrue) {
|
if (!_.isUndefined(values.password) || isDeactivatedChangeToTrue) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const icoToPng = require('ico-to-png');
|
|||||||
const sharp = require('sharp');
|
const sharp = require('sharp');
|
||||||
|
|
||||||
const FETCH_TIMEOUT = 4000;
|
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 FAVICON_TAGS_REGEX = /<link [^>]*rel="([^"]* )?icon( [^"]*)?"[^>]*>/gi;
|
||||||
const HREF_REGEX = /href="(.*?)"/i;
|
const HREF_REGEX = /href="(.*?)"/i;
|
||||||
@@ -39,7 +39,7 @@ const readResponse = async (response) => {
|
|||||||
chunks.push(value);
|
chunks.push(value);
|
||||||
receivedLength += value.length;
|
receivedLength += value.length;
|
||||||
|
|
||||||
if (receivedLength > MAX_RESPONSE_LENGTH_IN_BYTES) {
|
if (receivedLength > MAX_RESPONSE_LENGTH) {
|
||||||
reader.cancel();
|
reader.cancel();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -133,6 +133,12 @@ module.exports = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const availableStorage = await sails.helpers.utils.getAvailableStorage();
|
||||||
|
|
||||||
|
if (availableStorage !== null && readedResponse.buffer.length >= availableStorage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let image = sharp(readedResponse.buffer);
|
let image = sharp(readedResponse.buffer);
|
||||||
|
|
||||||
let metadata;
|
let metadata;
|
||||||
|
|||||||
@@ -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 util = require('util');
|
||||||
const { v4: uuid } = require('uuid');
|
|
||||||
|
|
||||||
module.exports = {
|
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: {
|
inputs: {
|
||||||
paramName: {
|
file: {
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
description: 'The MIME multi-part parameter containing the file to receive.',
|
|
||||||
},
|
|
||||||
req: {
|
|
||||||
type: 'ref',
|
type: 'ref',
|
||||||
required: true,
|
required: true,
|
||||||
description: 'The request to receive the file from.',
|
},
|
||||||
|
enforceStorageLimit: {
|
||||||
|
type: 'boolean',
|
||||||
|
defaultsTo: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
async fn(inputs, exits) {
|
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) =>
|
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(
|
return exits.success(
|
||||||
await upload({
|
await upload({
|
||||||
|
maxBytes,
|
||||||
dirname: sails.config.custom.uploadsTempPath,
|
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
|
// eslint-disable-next-line class-methods-use-this
|
||||||
async getSizeInBytes(filePathSegment) {
|
async getSize(filePathSegment) {
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = await fs.promises.stat(buildPath(filePathSegment));
|
result = await fs.promises.stat(buildPath(filePathSegment));
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class S3FileManager {
|
|||||||
return result.Body;
|
return result.Body;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSizeInBytes(filePathSegment) {
|
async getSize(filePathSegment) {
|
||||||
const headObjectCommand = new HeadObjectCommand({
|
const headObjectCommand = new HeadObjectCommand({
|
||||||
Bucket: sails.config.custom.s3Bucket,
|
Bucket: sails.config.custom.s3Bucket,
|
||||||
Key: filePathSegment,
|
Key: filePathSegment,
|
||||||
|
|||||||
@@ -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);
|
const arrayOfFileValues = arrayOfValues.filter(({ type }) => type === Attachment.Types.FILE);
|
||||||
|
|
||||||
if (arrayOfFileValues.length > 0) {
|
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 uploadedFileIdsByTotal = Object.entries(arrayOfValuesByUploadedFileId).reduce(
|
||||||
|
(result, [uploadedFileId, arrayOfValuesItem]) => ({
|
||||||
const fileReferenceIdsByTotal = Object.entries(arrayOfValuesByFileReferenceId).reduce(
|
|
||||||
(result, [fileReferenceId, arrayOfValuesItem]) => ({
|
|
||||||
...result,
|
...result,
|
||||||
[arrayOfValuesItem.length]: [...(result[arrayOfValuesItem.length] || []), fileReferenceId],
|
[arrayOfValuesItem.length]: [...(result[arrayOfValuesItem.length] || []), uploadedFileId],
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
return sails.getDatastore().transaction(async (db) => {
|
return sails.getDatastore().transaction(async (db) => {
|
||||||
const queryValues = [];
|
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]) => {
|
Object.entries(uploadedFileIdsByTotal).forEach(([total, uploadedFileIdsItem]) => {
|
||||||
const inValues = fileReferenceIdsItem.map((fileReferenceId) => {
|
const inValues = uploadedFileIdsItem.map((uploadedFileId) => {
|
||||||
queryValues.push(fileReferenceId);
|
queryValues.push(uploadedFileId);
|
||||||
return `$${queryValues.length}`;
|
return `$${queryValues.length}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -39,25 +38,25 @@ const create = (arrayOfValues) => {
|
|||||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
||||||
});
|
});
|
||||||
|
|
||||||
const inValues = fileReferenceIds.map((fileReferenceId) => {
|
const inValues = uploadedFileIds.map((uploadedFileId) => {
|
||||||
queryValues.push(fileReferenceId);
|
queryValues.push(uploadedFileId);
|
||||||
return `$${queryValues.length}`;
|
return `$${queryValues.length}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
queryValues.push(new Date().toISOString());
|
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 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) {
|
if (nextUploadedFileIds.length < uploadedFileIds.length) {
|
||||||
const nextFileReferenceIdsSet = new Set(nextFileReferenceIds);
|
const nextUploadedFileIdsSet = new Set(nextUploadedFileIds);
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
arrayOfValues = arrayOfValues.filter(
|
arrayOfValues = arrayOfValues.filter(
|
||||||
(values) =>
|
(values) =>
|
||||||
values.type !== Attachment.Types.FILE ||
|
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) => {
|
const createOne = (values) => {
|
||||||
if (values.type === Attachment.Types.FILE) {
|
if (values.type === Attachment.Types.FILE) {
|
||||||
const { fileReferenceId } = values.data;
|
|
||||||
|
|
||||||
return sails.getDatastore().transaction(async (db) => {
|
return sails.getDatastore().transaction(async (db) => {
|
||||||
const attachment = await Attachment.create({ ...values })
|
const attachment = await Attachment.create({ ...values })
|
||||||
.fetch()
|
.fetch()
|
||||||
@@ -79,13 +76,13 @@ const createOne = (values) => {
|
|||||||
|
|
||||||
const queryResult = await sails
|
const queryResult = await sails
|
||||||
.sendNativeQuery(
|
.sendNativeQuery(
|
||||||
'UPDATE file_reference SET total = total + 1, updated_at = $1 WHERE id = $2 AND total IS NOT NULL',
|
'UPDATE uploaded_file SET references_total = references_total + 1, updated_at = $1 WHERE id = $2 AND references_total IS NOT NULL',
|
||||||
[new Date().toISOString(), fileReferenceId],
|
[new Date().toISOString(), values.data.uploadedFileId],
|
||||||
)
|
)
|
||||||
.usingConnection(db);
|
.usingConnection(db);
|
||||||
|
|
||||||
if (queryResult.rowCount === 0) {
|
if (queryResult.rowCount === 0) {
|
||||||
throw 'fileReferenceNotFound';
|
throw 'uploadedFileNotFound';
|
||||||
}
|
}
|
||||||
|
|
||||||
return attachment;
|
return attachment;
|
||||||
@@ -129,24 +126,24 @@ const delete_ = (criteria) =>
|
|||||||
const attachments = await Attachment.destroy(criteria).fetch().usingConnection(db);
|
const attachments = await Attachment.destroy(criteria).fetch().usingConnection(db);
|
||||||
const fileAttachments = attachments.filter(({ type }) => type === Attachment.Types.FILE);
|
const fileAttachments = attachments.filter(({ type }) => type === Attachment.Types.FILE);
|
||||||
|
|
||||||
let fileReferences = [];
|
let uploadedFiles = [];
|
||||||
if (fileAttachments.length > 0) {
|
if (fileAttachments.length > 0) {
|
||||||
const attachmentsByFileReferenceId = _.groupBy(fileAttachments, 'data.fileReferenceId');
|
const attachmentsByUploadedFileId = _.groupBy(fileAttachments, 'data.uploadedFileId');
|
||||||
|
|
||||||
const fileReferenceIdsByTotal = Object.entries(attachmentsByFileReferenceId).reduce(
|
const uploadedFileIdsByTotal = Object.entries(attachmentsByUploadedFileId).reduce(
|
||||||
(result, [fileReferenceId, attachmentsItem]) => ({
|
(result, [uploadedFileId, attachmentsItem]) => ({
|
||||||
...result,
|
...result,
|
||||||
[attachmentsItem.length]: [...(result[attachmentsItem.length] || []), fileReferenceId],
|
[attachmentsItem.length]: [...(result[attachmentsItem.length] || []), uploadedFileId],
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const queryValues = [];
|
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]) => {
|
Object.entries(uploadedFileIdsByTotal).forEach(([total, uploadedFileIds]) => {
|
||||||
const inValues = fileReferenceIds.map((fileReferenceId) => {
|
const inValues = uploadedFileIds.map((uploadedFileId) => {
|
||||||
queryValues.push(fileReferenceId);
|
queryValues.push(uploadedFileId);
|
||||||
return `$${queryValues.length}`;
|
return `$${queryValues.length}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,11 +151,11 @@ const delete_ = (criteria) =>
|
|||||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
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]) => {
|
Object.entries(uploadedFileIdsByTotal).forEach(([total, uploadedFileIds]) => {
|
||||||
const inValues = fileReferenceIds.map((fileReferenceId) => {
|
const inValues = uploadedFileIds.map((uploadedFileId) => {
|
||||||
queryValues.push(fileReferenceId);
|
queryValues.push(uploadedFileId);
|
||||||
return `$${queryValues.length}`;
|
return `$${queryValues.length}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,56 +163,58 @@ const delete_ = (criteria) =>
|
|||||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
||||||
});
|
});
|
||||||
|
|
||||||
const inValues = Object.keys(attachmentsByFileReferenceId).map((fileReferenceId) => {
|
const inValues = Object.keys(attachmentsByUploadedFileId).map((uploadedFileId) => {
|
||||||
queryValues.push(fileReferenceId);
|
queryValues.push(uploadedFileId);
|
||||||
return `$${queryValues.length}`;
|
return `$${queryValues.length}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
queryValues.push(new Date().toISOString());
|
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);
|
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 {
|
return { attachments, uploadedFiles };
|
||||||
attachments,
|
|
||||||
fileReferences,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteOne = async (criteria, { isFile } = {}) => {
|
const deleteOne = (criteria) =>
|
||||||
let fileReference = null;
|
sails.getDatastore().transaction(async (db) => {
|
||||||
|
const attachment = await Attachment.destroyOne(criteria).usingConnection(db);
|
||||||
|
|
||||||
if (isFile) {
|
let uploadedFile;
|
||||||
return sails.getDatastore().transaction(async (db) => {
|
if (attachment.type === Attachment.Types.FILE) {
|
||||||
const attachment = await Attachment.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(), attachment.data.uploadedFileId],
|
||||||
|
)
|
||||||
|
.usingConnection(db);
|
||||||
|
|
||||||
if (attachment.type === Attachment.Types.FILE) {
|
const [row] = queryResult.rows;
|
||||||
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);
|
|
||||||
|
|
||||||
[fileReference] = queryResult.rows;
|
uploadedFile = {
|
||||||
}
|
id: row.id,
|
||||||
|
type: row.type,
|
||||||
return {
|
mimeType: row.mime_type,
|
||||||
attachment,
|
size: row.size,
|
||||||
fileReference,
|
referencesTotal: row.references_total,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const attachment = await Attachment.destroyOne(criteria);
|
return { attachment, uploadedFile };
|
||||||
|
});
|
||||||
return {
|
|
||||||
attachment,
|
|
||||||
fileReference,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
create,
|
create,
|
||||||
|
|||||||
@@ -7,7 +7,25 @@ const defaultFind = (criteria) => BackgroundImage.find(criteria).sort('id');
|
|||||||
|
|
||||||
/* Query methods */
|
/* 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);
|
const getByIds = (ids) => defaultFind(ids);
|
||||||
|
|
||||||
@@ -34,9 +52,99 @@ const getOneById = (id, { projectId } = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// 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 = {
|
module.exports = {
|
||||||
createOne,
|
createOne,
|
||||||
|
|||||||
@@ -217,18 +217,12 @@ const update = async (criteria, values) => {
|
|||||||
.usingConnection(db);
|
.usingConnection(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { cards, tasks };
|
||||||
cards,
|
|
||||||
tasks,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const cards = await Card.update(criteria).set(values).fetch();
|
const cards = await Card.update(criteria).set(values).fetch();
|
||||||
|
return { cards };
|
||||||
return {
|
|
||||||
cards,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateOne = async (criteria, values) => {
|
const updateOne = async (criteria, values) => {
|
||||||
@@ -250,18 +244,12 @@ const updateOne = async (criteria, values) => {
|
|||||||
.usingConnection(db);
|
.usingConnection(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { card, tasks };
|
||||||
card,
|
|
||||||
tasks,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const card = await Card.updateOne(criteria).set({ ...values });
|
const card = await Card.updateOne(criteria).set({ ...values });
|
||||||
|
return { card };
|
||||||
return {
|
|
||||||
card,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ const createOrUpdateOne = async (values) => {
|
|||||||
new Date().toISOString(),
|
new Date().toISOString(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [customFieldValue] = queryResult.rows;
|
const [row] = queryResult.rows;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: customFieldValue.id,
|
id: row.id,
|
||||||
cardId: customFieldValue.card_id,
|
cardId: row.card_id,
|
||||||
customFieldGroupId: customFieldValue.custom_field_group_id,
|
customFieldGroupId: row.custom_field_group_id,
|
||||||
customFieldId: customFieldValue.custom_field_id,
|
customFieldId: row.custom_field_id,
|
||||||
content: customFieldValue.content,
|
content: row.content,
|
||||||
createdAt: customFieldValue.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: customFieldValue.updated_at,
|
updatedAt: row.updated_at,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
* 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);
|
const defaultFind = (criteria, { sort = 'id' } = {}) => List.find(criteria).sort(sort);
|
||||||
|
|
||||||
/* Query methods */
|
/* Query methods */
|
||||||
@@ -48,8 +52,20 @@ const getOneTrashByBoardId = (boardId) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateOne = async (criteria, values) => {
|
const updateOne = async (criteria, values) => {
|
||||||
if (values.type) {
|
if (!_.isUndefined(values.type)) {
|
||||||
return sails.getDatastore().transaction(async (db) => {
|
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)
|
const list = await List.updateOne(criteria)
|
||||||
.set({ ...values })
|
.set({ ...values })
|
||||||
.usingConnection(db);
|
.usingConnection(db);
|
||||||
@@ -58,7 +74,7 @@ const updateOne = async (criteria, values) => {
|
|||||||
let tasks = [];
|
let tasks = [];
|
||||||
|
|
||||||
if (list) {
|
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];
|
const typeState = List.TYPE_STATE_BY_TYPE[list.type];
|
||||||
|
|
||||||
let isClosed;
|
let isClosed;
|
||||||
@@ -94,19 +110,12 @@ const updateOne = async (criteria, values) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { list, cards, tasks };
|
||||||
list,
|
|
||||||
cards,
|
|
||||||
tasks,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = await List.updateOne(criteria).set({ ...values });
|
const list = await List.updateOne(criteria).set({ ...values });
|
||||||
|
return { list };
|
||||||
return {
|
|
||||||
list,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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
|
* 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');
|
const defaultFind = (criteria) => User.find(criteria).sort('id');
|
||||||
|
|
||||||
/* Query methods */
|
/* Query methods */
|
||||||
|
|
||||||
const createOne = (values) => {
|
const createOne = (values) => {
|
||||||
if (sails.config.custom.activeUsersLimit) {
|
if (!_.isNil(sails.config.custom.activeUsersLimit)) {
|
||||||
return sails.getDatastore().transaction(async (db) => {
|
return sails.getDatastore().transaction(async (db) => {
|
||||||
const queryResult = await sails
|
const queryResult = await sails
|
||||||
.sendNativeQuery('SELECT NULL FROM user_account WHERE is_deactivated = $1 FOR UPDATE', [
|
.sendNativeQuery('SELECT NULL FROM user_account WHERE is_deactivated = $1 FOR UPDATE', [
|
||||||
@@ -62,29 +78,119 @@ const getOneActiveByEmailOrUsername = (emailOrUsername) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateOne = (criteria, values) => {
|
const updateOne = async (criteria, values) => {
|
||||||
if (values.isDeactivated === false && sails.config.custom.activeUsersLimit) {
|
const enforceActiveLimit =
|
||||||
return sails.getDatastore().transaction(async (db) => {
|
values.isDeactivated === false && !_.isNil(sails.config.custom.activeUsersLimit);
|
||||||
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) {
|
if (!_.isUndefined(values.avatar) || enforceActiveLimit) {
|
||||||
throw 'activeLimitReached';
|
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 })
|
.set({ ...values })
|
||||||
.usingConnection(db);
|
.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 = {
|
module.exports = {
|
||||||
createOne,
|
createOne,
|
||||||
|
|||||||
@@ -16,18 +16,13 @@ module.exports = {
|
|||||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||||
|
|
||||||
dirname: {
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
extension: {
|
extension: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
sizeInBytes: {
|
size: {
|
||||||
type: 'string', // TODO: should be number somehow
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
columnName: 'size_in_bytes',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||||
@@ -38,6 +33,11 @@ module.exports = {
|
|||||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||||
|
|
||||||
|
uploadedFileId: {
|
||||||
|
model: 'UploadedFile',
|
||||||
|
required: true,
|
||||||
|
columnName: 'uploaded_file_id',
|
||||||
|
},
|
||||||
projectId: {
|
projectId: {
|
||||||
model: 'Project',
|
model: 'Project',
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -4,22 +4,40 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FileReference.js
|
* StorageUsage.js
|
||||||
*
|
*
|
||||||
* @description :: A model definition represents a database table/collection.
|
* @description :: A model definition represents a database table/collection.
|
||||||
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const MAIN_ID = '1';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
MAIN_ID,
|
||||||
|
|
||||||
attributes: {
|
attributes: {
|
||||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||||
|
|
||||||
total: {
|
total: {
|
||||||
type: 'number',
|
type: 'string',
|
||||||
allowNull: true,
|
required: true,
|
||||||
defaultsTo: 0,
|
},
|
||||||
|
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',
|
||||||
};
|
};
|
||||||
@@ -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 { URL } = require('url');
|
||||||
|
const bytes = require('bytes');
|
||||||
const sails = require('sails');
|
const sails = require('sails');
|
||||||
|
|
||||||
const version = require('../version');
|
const version = require('../version');
|
||||||
|
|
||||||
const envToNumber = (value) => {
|
const envToNumber = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
const number = parseInt(value, 10);
|
const number = parseInt(value, 10);
|
||||||
return Number.isNaN(number) ? null : number;
|
return Number.isNaN(number) ? null : number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const envToBytes = (value) => value && bytes(value);
|
||||||
|
|
||||||
const envToArray = (value) => (value ? value.split(',') : []);
|
const envToArray = (value) => (value ? value.split(',') : []);
|
||||||
|
|
||||||
const parsedBasedUrl = new URL(process.env.BASE_URL);
|
const parsedBasedUrl = new URL(process.env.BASE_URL);
|
||||||
@@ -35,6 +42,7 @@ module.exports.custom = {
|
|||||||
baseUrlPath: parsedBasedUrl.pathname,
|
baseUrlPath: parsedBasedUrl.pathname,
|
||||||
baseUrlSecure: parsedBasedUrl.protocol === 'https:',
|
baseUrlSecure: parsedBasedUrl.protocol === 'https:',
|
||||||
|
|
||||||
|
maxUploadFileSize: envToBytes(process.env.MAX_UPLOAD_FILE_SIZE),
|
||||||
tokenExpiresIn: (parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365) * 24 * 60 * 60,
|
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.
|
// 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(),
|
process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(),
|
||||||
|
|
||||||
internalAccessToken: process.env.INTERNAL_ACCESS_TOKEN,
|
internalAccessToken: process.env.INTERNAL_ACCESS_TOKEN,
|
||||||
|
storageLimit: envToBytes(process.env.STORAGE_LIMIT),
|
||||||
activeUsersLimit: envToNumber(process.env.ACTIVE_USERS_LIMIT),
|
activeUsersLimit: envToNumber(process.env.ACTIVE_USERS_LIMIT),
|
||||||
|
|
||||||
showDetailedAuthErrors: process.env.SHOW_DETAILED_AUTH_ERRORS === 'true',
|
showDetailedAuthErrors: process.env.SHOW_DETAILED_AUTH_ERRORS === 'true',
|
||||||
|
|
||||||
s3Endpoint: process.env.S3_ENDPOINT,
|
s3Endpoint: process.env.S3_ENDPOINT,
|
||||||
|
|||||||
+2
-2
@@ -4,10 +4,10 @@ const AccessTokenSteps = {
|
|||||||
|
|
||||||
const POSITION_GAP = 65536;
|
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 = {
|
module.exports = {
|
||||||
AccessTokenSteps,
|
AccessTokenSteps,
|
||||||
POSITION_GAP,
|
POSITION_GAP,
|
||||||
MAX_SIZE_IN_BYTES_TO_GET_ENCODING,
|
MAX_SIZE_TO_GET_ENCODING,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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';
|
||||||
|
`);
|
||||||
|
};
|
||||||
+8
-10
@@ -18,7 +18,7 @@ const rc = require('sails/accessible/rc');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const knexfile = require('./knexfile');
|
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 = {
|
const PrevActionTypes = {
|
||||||
COMMENT_CARD: 'commentCard',
|
COMMENT_CARD: 'commentCard',
|
||||||
@@ -611,7 +611,7 @@ const upgradeUserAvatars = async () => {
|
|||||||
const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${dirname}`;
|
const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${dirname}`;
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
const sizeInBytes = await fileManager.getSizeInBytes(
|
const size = await fileManager.getSize(
|
||||||
`${dirPathSegment}/original.${user.avatar.extension}`,
|
`${dirPathSegment}/original.${user.avatar.extension}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -619,7 +619,7 @@ const upgradeUserAvatars = async () => {
|
|||||||
.update({
|
.update({
|
||||||
avatar: knex.raw("?? || jsonb_build_object('sizeInBytes', ?::bigint)", [
|
avatar: knex.raw("?? || jsonb_build_object('sizeInBytes', ?::bigint)", [
|
||||||
'avatar',
|
'avatar',
|
||||||
sizeInBytes,
|
size,
|
||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
.where('id', user.id);
|
.where('id', user.id);
|
||||||
@@ -690,13 +690,13 @@ const upgradeBackgroundImages = async () => {
|
|||||||
const dirPathSegment = `${sails.config.custom.backgroundImagesPathSegment}/${dirname}`;
|
const dirPathSegment = `${sails.config.custom.backgroundImagesPathSegment}/${dirname}`;
|
||||||
|
|
||||||
if (backgroundImage) {
|
if (backgroundImage) {
|
||||||
const sizeInBytes = await fileManager.getSizeInBytes(
|
const size = await fileManager.getSize(
|
||||||
`${dirPathSegment}/original.${backgroundImage.extension}`,
|
`${dirPathSegment}/original.${backgroundImage.extension}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await knex('background_image')
|
await knex('background_image')
|
||||||
.update({
|
.update({
|
||||||
size_in_bytes: sizeInBytes,
|
size_in_bytes: size,
|
||||||
})
|
})
|
||||||
.where('id', backgroundImage.id);
|
.where('id', backgroundImage.id);
|
||||||
} else {
|
} else {
|
||||||
@@ -777,12 +777,10 @@ const upgradeFileAttachments = async () => {
|
|||||||
'id',
|
'id',
|
||||||
);
|
);
|
||||||
|
|
||||||
const sizeInBytes = await fileManager.getSizeInBytes(
|
const size = await fileManager.getSize(`${dirPathSegment}/${attachment.data.filename}`);
|
||||||
`${dirPathSegment}/${attachment.data.filename}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
let encoding = null;
|
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(
|
const readStream = await fileManager.read(
|
||||||
`${dirPathSegment}/${attachment.data.filename}`,
|
`${dirPathSegment}/${attachment.data.filename}`,
|
||||||
);
|
);
|
||||||
@@ -795,7 +793,7 @@ const upgradeFileAttachments = async () => {
|
|||||||
.update({
|
.update({
|
||||||
data: trx.raw(
|
data: trx.raw(
|
||||||
"?? || jsonb_build_object('fileReferenceId', ?::text, 'sizeInBytes', ?::bigint, 'encoding', ?::text)",
|
"?? || jsonb_build_object('fileReferenceId', ?::text, 'sizeInBytes', ?::bigint, 'encoding', ?::text)",
|
||||||
['data', id, sizeInBytes, encoding],
|
['data', id, size, encoding],
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.where('id', attachment.id);
|
.where('id', attachment.id);
|
||||||
|
|||||||
Generated
+1
@@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.726.1",
|
"@aws-sdk/client-s3": "3.726.1",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"bytes": "^3.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"dotenv-cli": "^7.4.4",
|
"dotenv-cli": "^7.4.4",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.726.1",
|
"@aws-sdk/client-s3": "3.726.1",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"bytes": "^3.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"dotenv-cli": "^7.4.4",
|
"dotenv-cli": "^7.4.4",
|
||||||
|
|||||||
@@ -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