feat: many things (#351)

* remove source from final image

* move check state to ClearStorage

* use inspect for fancy colors

* newlines are now possible! yay!

* Catch user's leave if uploading

* feat?: Temp directory can be specified by the user.
Default is /tmp/zipline (or os equivalent)

* fix: ignore onDash config, use only ?compress query

---------

Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com>
This commit is contained in:
Jayvin Hernandez 2023-03-31 22:25:00 -07:00 committed by GitHub
parent bc58c1b56e
commit bf40fa9cd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 90 additions and 38 deletions

View file

@ -69,7 +69,8 @@ COPY --from=builder /zipline/node_modules/@prisma/client ./node_modules/@prisma/
# Copy Startup Script
COPY docker-entrypoint.sh /zipline
# Make Startup Script Executable
RUN chmod a+x /zipline/docker-entrypoint.sh
RUN chmod a+x /zipline/docker-entrypoint.sh && rm -rf /zipline/src
# Set the entrypoint to the startup script
ENTRYPOINT ["tini", "--", "/zipline/docker-entrypoint.sh"]

View file

@ -3,8 +3,10 @@ import { closeAllModals, openConfirmModal } from '@mantine/modals';
import { showNotification, updateNotification } from '@mantine/notifications';
import { IconFiles, IconFilesOff } from '@tabler/icons-react';
import useFetch from 'hooks/useFetch';
import { useState } from 'react';
export default function ClearStorage({ open, setOpen, check, setCheck }) {
export default function ClearStorage({ open, setOpen }) {
const [check, setCheck] = useState(false);
const handleDelete = async (datasource: boolean, orphaned?: boolean) => {
showNotification({
id: 'clear-uploads',
@ -38,7 +40,10 @@ export default function ClearStorage({ open, setOpen, check, setCheck }) {
return (
<Modal
opened={open}
onClose={() => setOpen(false)}
onClose={() => {
setOpen(false);
setCheck(() => false);
}}
title={<Title size='sm'>Are you sure you want to clear all uploads in the database?</Title>}
>
<Checkbox

View file

@ -4,7 +4,7 @@ import { Icon2fa, IconBarcodeOff, IconCheck } from '@tabler/icons-react';
import useFetch from 'hooks/useFetch';
import { useEffect, useState } from 'react';
export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
export function TotpModal({ opened, onClose, deleteTotp, setUser }) {
const [secret, setSecret] = useState('');
const [qrCode, setQrCode] = useState('');
const [disabled, setDisabled] = useState(false);
@ -52,8 +52,7 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
icon: <Icon2fa size='1rem' />,
});
setTotpEnabled(false);
setUser((user) => ({ ...user, totpSecret: null }));
onClose();
}
@ -83,8 +82,7 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
icon: <Icon2fa size='1rem' />,
});
setTotpEnabled(true);
setUser((user) => ({ ...user, totpSecret: secret }));
onClose();
}

View file

@ -89,7 +89,6 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
const [file, setFile] = useState<File | null>(null);
const [fileDataURL, setFileDataURL] = useState(user.avatar ?? null);
const [totpEnabled, setTotpEnabled] = useState(!!user.totpSecret);
const [checked, setCheck] = useState(false);
const getDataURL = (f: File): Promise<string> => {
return new Promise((res, rej) => {
@ -355,7 +354,8 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
useEffect(() => {
getExports();
interval.start();
}, [totpEnabled]);
setTotpEnabled(() => !!user.totpSecret);
}, [user]);
return (
<>
@ -450,7 +450,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
opened={totpOpen}
onClose={() => setTotpOpen(false)}
deleteTotp={totpEnabled}
setTotpEnabled={setTotpEnabled}
setUser={setUser}
/>
</Box>
)}
@ -626,7 +626,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
<ShareX user={user} open={shareXOpen} setOpen={setShareXOpen} />
<Flameshot user={user} open={flameshotOpen} setOpen={setFlameshotOpen} />
<ClearStorage open={clrStorOpen} setOpen={setClrStorOpen} check={checked} setCheck={setCheck} />
<ClearStorage open={clrStorOpen} setOpen={setClrStorOpen} />
</>
);
}

View file

@ -13,8 +13,11 @@ import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import showFilesModal from './showFilesModal';
import useUploadOptions from './useUploadOptions';
import { useRouter } from 'next/router';
export default function File({ chunks: chunks_config }) {
const router = useRouter();
const clipboard = useClipboard();
const modals = useModals();
const user = useRecoilValue(userSelector);
@ -25,6 +28,24 @@ export default function File({ chunks: chunks_config }) {
const [options, setOpened, OptionsModal] = useUploadOptions();
const beforeUnload = (e: BeforeUnloadEvent) => {
if (loading) {
e.preventDefault();
e.returnValue = "Are you sure you want to leave? Your upload(s) won't be saved.";
return e.returnValue;
}
};
const beforeRouteChange = (url: string) => {
if (loading) {
const confirmed = confirm("Are you sure you want to leave? Your upload(s) won't be saved.");
if (!confirmed) {
router.events.emit('routeChangeComplete', url);
throw 'Route change aborted';
}
}
};
useEffect(() => {
const listener = (e: ClipboardEvent) => {
const item = Array.from(e.clipboardData.items).find((x) => /^image/.test(x.type));
@ -41,8 +62,14 @@ export default function File({ chunks: chunks_config }) {
};
document.addEventListener('paste', listener);
return () => document.removeEventListener('paste', listener);
}, []);
window.addEventListener('beforeunload', beforeUnload);
router.events.on('routeChangeStart', beforeRouteChange);
return () => {
window.removeEventListener('beforeunload', beforeUnload);
router.events.off('routeChangeStart', beforeRouteChange);
document.removeEventListener('paste', listener);
};
}, [loading, beforeUnload, beforeRouteChange]);
const handleChunkedFiles = async (expiresAt: Date, toChunkFiles: File[]) => {
for (let i = 0; i !== toChunkFiles.length; ++i) {

View file

@ -1,5 +1,6 @@
export interface ConfigCore {
return_https: boolean;
temp_directory: string;
secret: string;
host: string;
port: number;

View file

@ -57,6 +57,7 @@ export default function readConfig() {
const maps = [
map('CORE_RETURN_HTTPS', 'boolean', 'core.return_https'),
map('CORE_TEMP_DIRECTORY', 'path', 'core.temp_directory'),
map('CORE_SECRET', 'string', 'core.secret'),
map('CORE_HOST', 'string', 'core.host'),
map('CORE_PORT', 'number', 'core.port'),

View file

@ -3,6 +3,8 @@ import type { Config } from './Config';
import { inspect } from 'util';
import Logger from 'lib/logger';
import { humanToBytes } from 'utils/bytes';
import { tmpdir } from 'os';
import { join } from 'path';
const discord_content = s
.object({
@ -27,6 +29,7 @@ const discord_content = s
const validator = s.object({
core: s.object({
return_https: s.boolean.default(false),
temp_directory: s.string.default(join(tmpdir(), 'zipline')),
secret: s.string.lengthGreaterThanOrEqual(8),
host: s.string.default('0.0.0.0'),
port: s.number.default(3000),

View file

@ -3,7 +3,6 @@ import { createWriteStream } from 'fs';
import { ExifTool, Tags } from 'exiftool-vendored';
import datasource from 'lib/datasource';
import Logger from 'lib/logger';
import { tmpdir } from 'os';
import { join } from 'path';
import { readFile, unlink } from 'fs/promises';
@ -34,7 +33,7 @@ export async function readMetadata(filePath: string): Promise<Tags> {
export async function removeGPSData(image: File): Promise<void> {
const exiftool = new ExifTool({ cleanupChildProcs: false });
const file = join(tmpdir(), `zipline-exif-remove-${Date.now()}-${image.name}`);
const file = join(config.core.temp_directory, `zipline-exif-remove-${Date.now()}-${image.name}`);
logger.debug(`writing temp file to remove GPS data: ${file}`);
const stream = await datasource.get(image.name);

View file

@ -11,7 +11,10 @@ export type ParseValue = {
export function parseString(str: string, value: ParseValue) {
if (!str) return null;
str = str.replace(/\{link\}/gi, value.link).replace(/\{raw_link\}/gi, value.raw_link);
str = str
.replace(/\{link\}/gi, value.link)
.replace(/\{raw_link\}/gi, value.raw_link)
.replace(/\\n/g, '\n');
const re = /\{(?<type>file|url|user)\.(?<prop>\w+)(::(?<mod>\w+))?\}/gi;
let matches: RegExpMatchArray;

View file

@ -6,7 +6,6 @@ import Logger from 'lib/logger';
import prisma from 'lib/prisma';
import { readMetadata } from 'lib/utils/exif';
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
import { tmpdir } from 'os';
import { join } from 'path';
const logger = Logger.get('exif');
@ -41,7 +40,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
return res.json(data);
} else {
const file = join(tmpdir(), `zipline-exif-read-${Date.now()}-${image.name}`);
const file = join(config.core.temp_directory, `zipline-exif-read-${Date.now()}-${image.name}`);
logger.debug(`writing temp file to view metadata: ${file}`);
const stream = await datasource.get(image.name);

View file

@ -12,7 +12,6 @@ import { createInvisImage, hashPassword } from 'lib/util';
import { parseExpiry } from 'lib/utils/client';
import { removeGPSData } from 'lib/utils/exif';
import multer from 'multer';
import { tmpdir } from 'os';
import { join } from 'path';
import sharp from 'sharp';
@ -104,12 +103,12 @@ async function handler(req: NextApiReq, res: NextApiRes) {
})}`
);
const tempFile = join(tmpdir(), `zipline_partial_${identifier}_${start}_${end}`);
const tempFile = join(zconfig.core.temp_directory, `zipline_partial_${identifier}_${start}_${end}`);
logger.debug(`writing partial to disk ${tempFile}`);
await writeFile(tempFile, req.files[0].buffer);
if (lastchunk) {
const partials = await readdir(tmpdir()).then((files) =>
const partials = await readdir(zconfig.core.temp_directory).then((files) =>
files.filter((x) => x.startsWith(`zipline_partial_${identifier}`))
);
@ -124,8 +123,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
for (let i = 0; i !== readChunks.length; ++i) {
const chunkData = readChunks[i];
const buffer = await readFile(join(tmpdir(), chunkData.filename));
await unlink(join(tmpdir(), readChunks[i].filename));
const buffer = await readFile(join(zconfig.core.temp_directory, chunkData.filename));
await unlink(join(zconfig.core.temp_directory, readChunks[i].filename));
chunks.set(buffer, chunkData.start);
}

View file

@ -5,7 +5,6 @@ import datasource from 'lib/datasource';
import Logger from 'lib/logger';
import prisma from 'lib/prisma';
import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline';
import { tmpdir } from 'os';
import { join } from 'path';
const logger = Logger.get('user::export');
@ -22,7 +21,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
const zip = new Zip();
const export_name = `zipline_export_${user.id}_${Date.now()}.zip`;
const path = join(tmpdir(), export_name);
const path = join(config.core.temp_directory, export_name);
logger.debug(`creating write stream at ${path}`);
const write_stream = createWriteStream(path);
@ -121,18 +120,18 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
const parts = export_name.split('_');
if (Number(parts[2]) !== user.id) return res.unauthorized('cannot access export owned by another user');
const stream = createReadStream(join(tmpdir(), export_name));
const stream = createReadStream(join(config.core.temp_directory, export_name));
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="${export_name}"`);
stream.pipe(res);
} else {
const files = await readdir(tmpdir());
const files = await readdir(config.core.temp_directory);
const exp = files.filter((f) => f.startsWith('zipline_export_'));
const exports = [];
for (let i = 0; i !== exp.length; ++i) {
const name = exp[i];
const stats = await stat(join(tmpdir(), name));
const stats = await stat(join(config.core.temp_directory, name));
if (Number(exp[i].split('_')[2]) !== user.id) continue;
exports.push({ name, size: stats.size });

View file

@ -1,7 +1,6 @@
import { Box, Button, Modal, PasswordInput } from '@mantine/core';
import type { File } from '@prisma/client';
import AnchorNext from 'components/AnchorNext';
import config from 'lib/config';
import exts from 'lib/exts';
import prisma from 'lib/prisma';
import { parseString } from 'lib/utils/parser';
@ -17,18 +16,15 @@ export default function EmbeddedFile({
user,
pass,
prismRender,
onDash,
compress,
}: {
file: File & { imageProps?: HTMLImageElement };
user: UserExtended;
pass: boolean;
prismRender: boolean;
onDash: boolean;
compress?: boolean;
}) {
const dataURL = (route: string) =>
`${route}/${encodeURI(file.name)}?compress=${compress == null ? onDash : compress}`;
const dataURL = (route: string) => `${route}/${encodeURI(file.name)}?compress=${compress ?? false}`;
const router = useRouter();
const [opened, setOpened] = useState(pass);
@ -268,7 +264,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
file,
user,
pass: file.password ? true : false,
onDash: config.core.compression.on_dashboard,
compress,
},
};

View file

@ -1,3 +1,4 @@
import config from 'lib/config';
import { inspect } from 'util';
console.log(JSON.stringify(config, null, 2));
console.log(inspect(config, { depth: Infinity, colors: true }));

View file

@ -1,6 +1,7 @@
import { FastifyInstance } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import { mkdir } from 'fs/promises';
import { existsSync } from 'fs';
import { mkdir, readdir } from 'fs/promises';
import type { Config } from 'lib/config/Config';
async function configPlugin(fastify: FastifyInstance, config: Config) {
@ -16,7 +17,9 @@ async function configPlugin(fastify: FastifyInstance, config: Config) {
.error(
'The config file is located at `.env.local`, or if using docker-compose you can change the variables in the `docker-compose.yml` file.'
)
.error('It is recomended to use a secret that is alphanumeric and randomized.')
.error(
'It is recomended to use a secret that is alphanumeric and randomized. If you include special characters, surround the secret with quotes.'
)
.error('A way you can generate this is through a password manager you may have.');
process.exit(1);
@ -26,6 +29,24 @@ async function configPlugin(fastify: FastifyInstance, config: Config) {
await mkdir(config.datasource.local.directory, { recursive: true });
}
if (!existsSync(config.core.temp_directory)) {
await mkdir(config.core.temp_directory, { recursive: true });
} else {
const files = await readdir(config.core.temp_directory);
if (
files.filter((x: string) => x.startsWith('zipline_partial_') || x.startsWith('zipline-exif-read-'))
.length > 0
)
fastify.logger
.error("Found temporary files in Zipline's temp directory.")
.error('This can happen if Zipline crashes or is stopped while chunking a file.')
.error(
'If you are sure that no files are currently being processed, you can delete the files in the temp directory.'
)
.error('The temp directory is located at: ' + config.core.temp_directory)
.error('If you are unsure, you can safely ignore this message.');
}
return;
}