Skip to content

Commit

Permalink
feat: allow multiple uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
bjarneo committed Aug 30, 2022
1 parent 015ccb8 commit 80a11d4
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 70 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions config/default.cjs
Expand Up @@ -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 = '',
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions server.js
Expand Up @@ -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';
Expand All @@ -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,
},
});
Expand All @@ -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' });
Expand Down
11 changes: 7 additions & 4 deletions src/client/api/secret.js
Expand Up @@ -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) => {
Expand Down
50 changes: 27 additions & 23 deletions 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);
});
};
41 changes: 27 additions & 14 deletions src/client/routes/home/index.js
Expand Up @@ -18,6 +18,7 @@ import {
Divider,
FileButton,
NumberInput,
Badge,
} from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -126,7 +127,7 @@ const Home = () => {
setPassword('');
setEncryptionKey('');
setAllowedIp('');
setFile('');
setFiles([]);
setTitle('');
setPreventBurn(false);
setFormData(new FormData());
Expand All @@ -147,20 +148,23 @@ const Home = () => {
event.preventDefault();

formData.append('text', text);
formData.append('file', file);
formData.append('title', title);
formData.append('password', password);
formData.append('ttl', ttl);
formData.append('allowedIp', allowedIp);
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);

Expand Down Expand Up @@ -358,7 +362,12 @@ const Home = () => {

<Group grow={isMobile}>
{enableFileUpload && (
<FileButton onChange={setFile} disabled={!isLoggedIn}>
<FileButton
onChange={setFiles}
disabled={!isLoggedIn}
styles={groupMobileStyle}
multiple
>
{(props) => (
<Button
{...props}
Expand All @@ -374,7 +383,7 @@ const Home = () => {
},
})}
>
Upload file
Upload files
</Button>
)}
</FileButton>
Expand All @@ -385,14 +394,18 @@ const Home = () => {
Sign in to upload files
</Text>
)}

{file && (
<Text size="sm" align="center" mt="sm">
Picked file: {file.name}
</Text>
)}
</Group>

{files.length > 0 && (
<Group>
{files.map((file) => (
<Badge color="orange" key={file.name}>
{file.name}
</Badge>
))}
</Group>
)}

{secretId && (
<Group grow>
<TextInput
Expand Down
16 changes: 8 additions & 8 deletions src/client/routes/secret/index.js
Expand Up @@ -27,7 +27,7 @@ const Secret = () => {
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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -99,12 +99,12 @@ const Secret = () => {
setPassword(event.target.value);
};

const onFileDownload = (event) => {
const onFilesDownload = (event) => {
event.preventDefault();

downloadFile(
{
...file,
files,
encryptionKey,
secretId,
},
Expand Down Expand Up @@ -192,7 +192,7 @@ const Secret = () => {
</Button>
)}

{file && !isDownloaded && (
{files && !isDownloaded && (
<Button
styles={() => ({
root: {
Expand All @@ -204,11 +204,11 @@ const Secret = () => {
},
},
})}
onClick={onFileDownload}
onClick={onFilesDownload}
disabled={!secretId}
leftIcon={<IconDownload size={14} />}
>
Download the file
Download files
</Button>
)}

Expand Down
Expand Up @@ -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);
Expand All @@ -31,4 +31,4 @@ async function uploadFiles(fastify) {
});
}

export default uploadFiles;
export default downloadFiles;
10 changes: 5 additions & 5 deletions src/server/controllers/secret.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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') {
Expand Down
27 changes: 19 additions & 8 deletions src/server/decorators/attachment-upload.js
Expand Up @@ -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 });
}
}
});
});
4 changes: 2 additions & 2 deletions src/server/services/redis.js
Expand Up @@ -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) {
Expand Down

0 comments on commit 80a11d4

Please sign in to comment.