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:
parent
bc58c1b56e
commit
bf40fa9cd2
16 changed files with 90 additions and 38 deletions
|
@ -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"]
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export interface ConfigCore {
|
||||
return_https: boolean;
|
||||
temp_directory: string;
|
||||
secret: string;
|
||||
host: string;
|
||||
port: number;
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 }));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue