diff --git a/README.md b/README.md
index f95096c..0b99d32 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,7 @@ Have a look at the Dockerfile for a full example of how to run this application.
- `SECRET_REDIS_PASSWORD` Default: "" - Your redis password
- `SECRET_JWT_SECRET` Default: good_luck_have_fun - Override this for the secret signin JWT tokens for log in
- `SECRET_FILE_SIZE` Default: 4 - Set the allowed upload file size in mb.
+- `SECRET_FILE_LIMIT` Default: 3 - Set the amount of files allowed to be uploaded
- `SECRET_ENABLE_FILE_UPLOAD` Default: true - Enable or disable file upload
- `SECRET_DO_SPACES_ENDPOINT` Default: "" - The Spaces/s3 endpoint
- `SECRET_DO_SPACES_KEY` Default: "" - The Spaces/s3 key
diff --git a/config/default.cjs b/config/default.cjs
index 92e8813..0557169 100644
--- a/config/default.cjs
+++ b/config/default.cjs
@@ -11,6 +11,7 @@ const {
SECRET_JWT_SECRET = 'good_luck_have_fun',
SECRET_ENABLE_FILE_UPLOAD = 'true',
SECRET_FILE_SIZE = 4, // 4 mb
+ SECRET_FILE_LIMIT = 3, // maximum of 3 files allowed to be uploaded
SECRET_DO_SPACES_ENDPOINT = 'https://fra1.digitaloceanspaces.com',
SECRET_DO_SPACES_KEY = '',
SECRET_DO_SPACES_SECRET = '',
@@ -30,6 +31,7 @@ const config = {
file: {
size: SECRET_FILE_SIZE,
adapter: !!SECRET_DO_SPACES_SECRET ? 'do' : 'disk',
+ limit: SECRET_FILE_LIMIT,
},
redis: {
host: SECRET_REDIS_HOST,
diff --git a/server.js b/server.js
index fb31633..f9fb7d8 100644
--- a/server.js
+++ b/server.js
@@ -16,7 +16,7 @@ import keyGeneration from './src/server/decorators/key-generation.js';
import authenticationRoute from './src/server/controllers/authentication.js';
import accountRoute from './src/server/controllers/account.js';
-import uploadRoute from './src/server/controllers/upload.js';
+import downloadROute from './src/server/controllers/download.js';
import secretRoute from './src/server/controllers/secret.js';
import statsRoute from './src/server/controllers/stats.js';
import healthzRoute from './src/server/controllers/healthz.js';
@@ -38,7 +38,7 @@ fastify.register(cors, { origin: config.get('cors') });
fastify.register(multipart, {
attachFieldsToBody: true,
limits: {
- files: 1,
+ files: config.get('file.limit'),
fileSize: MAX_FILE_BYTES,
},
});
@@ -59,7 +59,7 @@ fastify.register(authenticationRoute, {
fastify.register(accountRoute, {
prefix: '/api/account',
});
-fastify.register(uploadRoute, { prefix: '/api/upload' });
+fastify.register(downloadROute, { prefix: '/api/download' });
fastify.register(secretRoute, { prefix: '/api/secret' });
fastify.register(statsRoute, { prefix: '/api/stats' });
fastify.register(healthzRoute, { prefix: '/api/healthz' });
diff --git a/src/client/api/secret.js b/src/client/api/secret.js
index 672a9ab..18ab4ba 100644
--- a/src/client/api/secret.js
+++ b/src/client/api/secret.js
@@ -14,11 +14,14 @@ export const createSecret = async (formData = {}, token = '') => {
},
});
}
+ try {
+ const data = await fetch(`${config.get('api.host')}/secret`, options);
+ const json = await data.json();
- const data = await fetch(`${config.get('api.host')}/secret`, options);
- const json = await data.json();
-
- return { ...json, statusCode: data.status };
+ return { ...json, statusCode: data.status };
+ } catch (e) {
+ console.error(e);
+ }
};
export const getSecret = async (secretId, encryptionKey, password) => {
diff --git a/src/client/api/upload.js b/src/client/api/upload.js
index 2503370..1fc949d 100644
--- a/src/client/api/upload.js
+++ b/src/client/api/upload.js
@@ -1,30 +1,34 @@
import config from '../config';
export const downloadFile = async (fileData, token) => {
- const { key, encryptionKey, ext, mime, secretId } = fileData;
+ const { files, encryptionKey, secretId } = fileData;
- const data = await fetch(`${config.get('api.host')}/upload/download`, {
- method: 'POST',
- cache: 'no-cache',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${token}`,
- },
- body: JSON.stringify({ key, encryptionKey, ext, mime, secretId }),
- });
+ files.forEach(async (file) => {
+ const { key, ext, mime } = file;
+
+ const data = await fetch(`${config.get('api.host')}/download/`, {
+ method: 'POST',
+ cache: 'no-cache',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({ key, encryptionKey, ext, mime, secretId }),
+ });
- if (data.status === 401) {
- return {
- statusCode: 401,
- };
- }
+ if (data.status === 401) {
+ return {
+ statusCode: 401,
+ };
+ }
- const blob = await data.blob();
- const imageUrl = window.URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = imageUrl;
- link.download = `${secretId}.${ext}`;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
+ const blob = await data.blob();
+ const imageUrl = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = imageUrl;
+ link.download = `${secretId}.${ext}`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ });
};
diff --git a/src/client/routes/home/index.js b/src/client/routes/home/index.js
index 7369c65..3486d3d 100644
--- a/src/client/routes/home/index.js
+++ b/src/client/routes/home/index.js
@@ -18,6 +18,7 @@ import {
Divider,
FileButton,
NumberInput,
+ Badge,
} from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import {
@@ -46,7 +47,7 @@ const Home = () => {
const [title, setTitle] = useState('');
const [maxViews, setMaxViews] = useState(1);
const [enableFileUpload] = useState(config.get('settings.enableFileUpload', false));
- const [file, setFile] = useState(null);
+ const [files, setFiles] = useState([]);
const [ttl, setTTL] = useState(14400);
const [password, setPassword] = useState('');
const [enablePassword, setOnEnablePassword] = useState(false);
@@ -126,7 +127,7 @@ const Home = () => {
setPassword('');
setEncryptionKey('');
setAllowedIp('');
- setFile('');
+ setFiles([]);
setTitle('');
setPreventBurn(false);
setFormData(new FormData());
@@ -147,7 +148,6 @@ const Home = () => {
event.preventDefault();
formData.append('text', text);
- formData.append('file', file);
formData.append('title', title);
formData.append('password', password);
formData.append('ttl', ttl);
@@ -155,12 +155,16 @@ const Home = () => {
formData.append('preventBurn', preventBurn);
formData.append('maxViews', maxViews);
+ files.forEach((file) => formData.append('files[]', file));
+
const json = await createSecret(formData, getToken());
if (json.statusCode !== 201) {
- setError(
- json.error === 'Payload Too Large' ? 'The file size is too large' : json.error
- );
+ if (json.message === 'request file too large, please check multipart config') {
+ setError('The file size is too large');
+ } else {
+ setError(json.error);
+ }
setCreatingSecret(false);
@@ -358,7 +362,12 @@ const Home = () => {
{enableFileUpload && (
-
+
{(props) => (
)}
@@ -385,14 +394,18 @@ const Home = () => {
Sign in to upload files
)}
-
- {file && (
-
- Picked file: {file.name}
-
- )}
+ {files.length > 0 && (
+
+ {files.map((file) => (
+
+ {file.name}
+
+ ))}
+
+ )}
+
{secretId && (
{
const [isSecretOpen, setIsSecretOpen] = useState(false);
const [password, setPassword] = useState('');
const [isPasswordRequired, setIsPasswordRequired] = useState(false);
- const [file, setFile] = useState(null);
+ const [files, setFiles] = useState(null);
const [isDownloaded, setIsDownloaded] = useState(false);
const [error, setError] = useState(null);
const [hasConvertedBase64ToPlain, setHasConvertedBase64ToPlain] = useState(false);
@@ -64,8 +64,8 @@ const Secret = () => {
setTitle(validator.unescape(json.title));
}
- if (json.file) {
- setFile(json.file);
+ if (json.files) {
+ setFiles(json.files);
}
if (json.preventBurn) {
@@ -99,12 +99,12 @@ const Secret = () => {
setPassword(event.target.value);
};
- const onFileDownload = (event) => {
+ const onFilesDownload = (event) => {
event.preventDefault();
downloadFile(
{
- ...file,
+ files,
encryptionKey,
secretId,
},
@@ -192,7 +192,7 @@ const Secret = () => {
)}
- {file && !isDownloaded && (
+ {files && !isDownloaded && (
)}
diff --git a/src/server/controllers/upload.js b/src/server/controllers/download.js
similarity index 88%
rename from src/server/controllers/upload.js
rename to src/server/controllers/download.js
index fcf2427..cdcf69f 100644
--- a/src/server/controllers/upload.js
+++ b/src/server/controllers/download.js
@@ -3,8 +3,8 @@ import fileAdapter from '../services/file-adapter.js';
import * as redis from '../services/redis.js';
// https://stackabuse.com/uploading-files-to-aws-s3-with-node-js
-async function uploadFiles(fastify) {
- fastify.post('/download', async (request, reply) => {
+async function downloadFiles(fastify) {
+ fastify.post('/', async (request, reply) => {
const { key, encryptionKey, secretId, ext, mime } = request.body;
const fileKey = sanitize(key);
@@ -31,4 +31,4 @@ async function uploadFiles(fastify) {
});
}
-export default uploadFiles;
+export default downloadFiles;
diff --git a/src/server/controllers/secret.js b/src/server/controllers/secret.js
index 6e30a55..a2b02e5 100644
--- a/src/server/controllers/secret.js
+++ b/src/server/controllers/secret.js
@@ -44,8 +44,8 @@ async function getSecretRoute(request, reply) {
}
}
- if (data.file) {
- Object.assign(result, { file: JSON.parse(data.file) });
+ if (data.files) {
+ Object.assign(result, { files: JSON.parse(data.files) });
}
if (data.title) {
@@ -96,7 +96,7 @@ async function secret(fastify) {
},
async (req, reply) => {
const { text, title, ttl, password, allowedIp, preventBurn, maxViews } = req.body;
- const { encryptionKey, secretId, file } = req.secret;
+ const { encryptionKey, secretId, files } = req.secret;
if (Buffer.byteLength(text?.value) > config.get('api.maxTextSize')) {
return reply.code(413).send({
@@ -133,8 +133,8 @@ async function secret(fastify) {
Object.assign(data, { password: await hash(validator.escape(password.value)) });
}
- if (file) {
- Object.assign(data, { file });
+ if (files) {
+ Object.assign(data, { files });
}
if (preventBurn?.value === 'true') {
diff --git a/src/server/decorators/attachment-upload.js b/src/server/decorators/attachment-upload.js
index a0cfa08..fb2402e 100644
--- a/src/server/decorators/attachment-upload.js
+++ b/src/server/decorators/attachment-upload.js
@@ -6,20 +6,31 @@ import fileAdapter from '../services/file-adapter.js';
export default fp(async (fastify) => {
fastify.decorate('attachment', async (req, reply) => {
- const file = await req.body.file;
+ const reqFiles = await req.body['files[]'];
const { encryptionKey } = req.secret;
- if (file.mimetype) {
- const fileData = await file.toBuffer();
+ const files = (reqFiles?.length ? reqFiles : [reqFiles]).filter(Boolean);
- const metadata = await fileTypeFromBuffer(fileData);
+ if (files?.length) {
+ req.secret.files = [];
- const mime = metadata?.mime ? metadata.mime : file.mimetype.toString();
- const ext = metadata?.ext ? metadata.ext : path.extname(file.filename).replace('.', '');
+ // yeah, for loop, I know. Could easily be reduce or what not
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
- const imageData = await fileAdapter.upload(encryptionKey, fileData);
+ const fileData = await file.toBuffer();
- Object.assign(req.secret, { file: { ext, mime, key: imageData.key } });
+ const metadata = await fileTypeFromBuffer(fileData);
+
+ const mime = metadata?.mime ? metadata.mime : file.mimetype.toString();
+ const ext = metadata?.ext
+ ? metadata.ext
+ : path.extname(file.filename).replace('.', '');
+
+ const imageData = await fileAdapter.upload(encryptionKey, fileData);
+
+ req.secret.files.push({ ext, mime, key: imageData.key });
+ }
}
});
});
diff --git a/src/server/services/redis.js b/src/server/services/redis.js
index 1e0e223..b420891 100644
--- a/src/server/services/redis.js
+++ b/src/server/services/redis.js
@@ -43,8 +43,8 @@ export async function createSecret(data, ttl) {
prepare.push(...['allowed_ip', data.allowedIp]);
}
- if (data.file) {
- prepare.push(...['file', JSON.stringify(data.file)]);
+ if (data.files) {
+ prepare.push(...['files', JSON.stringify(data.files)]);
}
if (data.preventBurn) {